Skip to content

Commit

Permalink
refactor: access column removed from owner table and now only single …
Browse files Browse the repository at this point in the history
…owner allowed
  • Loading branch information
smotornyuk committed Jun 9, 2024
1 parent d0648ca commit f8d385d
Show file tree
Hide file tree
Showing 18 changed files with 503 additions and 100 deletions.
3 changes: 1 addition & 2 deletions ckanext/files/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def files():
@click.argument("file_id")
def stream(file_id: str):
"""Stream content of the file."""
file = shared.File.get(file_id)
file = model.Session.get(shared.File, file_id)
if not file:
tk.error_shout("File not found")
raise click.Abort()
Expand Down Expand Up @@ -148,7 +148,6 @@ def scan(
item_type="file",
owner_id=stepfather.id,
owner_type="user",
access=Owner.ACCESS_FULL,
)
model.Session.add(owner)

Expand Down
6 changes: 6 additions & 0 deletions ckanext/files/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

DEFAULT_STORAGE = "ckanext.files.default_storage"
STORAGE_PREFIX = "ckanext.files.storage."
CASCADE_ACCESS = "ckanext.files.owner.cascade_access"


def default_storage() -> str:
Expand All @@ -40,3 +41,8 @@ def storages() -> dict[str, dict[str, Any]]:

storages[name][option] = v
return storages


def cascade_access() -> list[str]:
"""List of owner types that grant automatic access on owned file."""
return tk.config[CASCADE_ACCESS]
13 changes: 13 additions & 0 deletions ckanext/files/config_declaration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ groups:
description: |
Default storage used for upload when no explicit storage specified
- key: ckanext.files.owner.cascade_access
type: list
description: |
List of owner types that grant access on owned file to anyone who has
access to the owner of file. For example, if this option has value
`resource package`, anyone who passes `resource_show` auth, can see
all files owned by resource; anyone who passes `package_show`, can
see all files owned by package; anyone who passes
`package_update`/`resource_update` can modify files owned by
package/resource; anyone who passes
`package_delete`/`resource_delete` can delete files owned by
package/resoure.
- key: ckanext.files.storage.<NAME>.<OPTION>
type: dynamic
description: |
Expand Down
57 changes: 57 additions & 0 deletions ckanext/files/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
from typing import Any

from ckan.plugins import Interface
from ckan.types import Context

from ckanext.files import types

File = Multipart = Any


class IFiles(Interface):
Expand All @@ -23,3 +28,55 @@ def files_get_storage_adapters(self) -> dict[str, Any]:
"""

return {}

def files_materialize_owner(
self,
owner_type: str,
owner_id: str,
not_exist: object,
) -> Any:
"""Return owner entity.
If implementation doesn't know how to get owner, return `None`. If
implementation knows how to get owner and owner **definitely** does not
exist, return `not_exist` parameter.
Example:
>>> def files_materialize_owner(self, type, id, not_exist):
>>> if type not in known_types:
>>> return None
>>>
>>> if owner := get_known_owner(id):
>>> return owner
>>>
>>> return not_exist
"""
return None

def files_is_allowed(
self,
context: Context,
file: File | Multipart,
operation: types.AuthOperation,
) -> bool | None:
"""Decide if user is allowed to perform operation on file.
Return True/False if user allowed/not allowed. Return `None` to rely on
other plugins. If every implementation returns `None`, default logic
allows only user who owns the file to perform any operation on it. It
means, that nobody is allowed to do anything with file owner by
resource, dataset, group, etc.
Example:
>>> def files_is_allowed(self, context, file, operation) -> bool | None:
>>> if file.owner_info and file.owner_info == "resource"
>>> return is_authorized_boolean(
>>> f"resource_{operation}",
>>> context,
>>> {"id": file.owner_info.id}
>>> )
>>>
>>> return None
"""
return None
68 changes: 43 additions & 25 deletions ckanext/files/logic/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,21 +189,20 @@ def files_file_create(context: Context, data_dict: dict[str, Any]) -> dict[str,
storage_data.into_model(fileobj)
context["session"].add(fileobj)

_add_owner(context, "file", fileobj.id)
_set_user_owner(context, "file", fileobj.id)
context["session"].commit()

return fileobj.dictize(context)


def _add_owner(context: Context, item_type: str, item_id: str):
def _set_user_owner(context: Context, item_type: str, item_id: str):
user = model.User.get(context.get("user", ""))
if user:
owner = Owner(
item_id=item_id,
item_type=item_type,
owner_id=user.id,
owner_type="user",
access=Owner.ACCESS_FULL,
)
context["session"].add(owner)

Expand All @@ -222,11 +221,9 @@ def _delete_owners(context: Context, item_type: str, item_id: str):
def files_file_delete(context: Context, data_dict: dict[str, Any]) -> dict[str, Any]:
tk.check_access("files_file_delete", context, data_dict)

fileobj: File | Multipart | None = (
context["session"]
.query(File if data_dict["completed"] else Multipart)
.filter_by(id=data_dict["id"])
.one_or_none()
fileobj = context["session"].get(
File if data_dict["completed"] else Multipart,
data_dict["id"],
)
if not fileobj:
raise tk.ObjectNotFound("file")
Expand Down Expand Up @@ -257,11 +254,9 @@ def files_file_delete(context: Context, data_dict: dict[str, Any]) -> dict[str,
def files_file_show(context: Context, data_dict: dict[str, Any]) -> dict[str, Any]:
tk.check_access("files_file_show", context, data_dict)

fileobj: File | Multipart | None = (
context["session"]
.query(File if data_dict["completed"] else Multipart)
.filter_by(id=data_dict["id"])
.one_or_none()
fileobj = context["session"].get(
File if data_dict["completed"] else Multipart,
data_dict["id"],
)
if not fileobj:
raise tk.ObjectNotFound("file")
Expand All @@ -273,11 +268,9 @@ def files_file_show(context: Context, data_dict: dict[str, Any]) -> dict[str, An
def files_file_rename(context: Context, data_dict: dict[str, Any]) -> dict[str, Any]:
tk.check_access("files_file_rename", context, data_dict)

fileobj: File | Multipart | None = (
context["session"]
.query(File if data_dict["completed"] else Multipart)
.filter_by(id=data_dict["id"])
.one_or_none()
fileobj = context["session"].get(
File if data_dict["completed"] else Multipart,
data_dict["id"],
)
if not fileobj:
raise tk.ObjectNotFound("file")
Expand Down Expand Up @@ -327,7 +320,7 @@ def files_multipart_start(
data.into_model(fileobj)

context["session"].add(fileobj)
_add_owner(context, "multipart", fileobj.id)
_set_user_owner(context, "multipart", fileobj.id)
context["session"].commit()

return fileobj.dictize(context)
Expand Down Expand Up @@ -360,9 +353,7 @@ def files_multipart_update(

extras = data_dict.get("__extras", {})

fileobj: Multipart | None = (
context["session"].query(Multipart).filter_by(id=data_dict["id"]).one_or_none()
)
fileobj = context["session"].get(Multipart, data_dict["id"])
if not fileobj:
raise tk.ObjectNotFound("upload")

Expand Down Expand Up @@ -391,9 +382,7 @@ def files_multipart_complete(
extras = data_dict.get("__extras", {})

data_dict["id"]
fileobj = (
context["session"].query(Multipart).filter_by(id=data_dict["id"]).one_or_none()
)
fileobj = context["session"].get(Multipart, data_dict["id"])
if not fileobj:
raise tk.ObjectNotFound("upload")

Expand Down Expand Up @@ -421,3 +410,32 @@ def files_multipart_complete(
sess.commit()

return result.dictize(context)


@validate(schema.transfer_ownership)
def files_transfer_ownership(
context: Context,
data_dict: dict[str, Any],
) -> dict[str, Any]:
tk.check_access("files_transfer_ownership", context, data_dict)
sess = context["session"]
fileobj = context["session"].get(
File if data_dict["completed"] else Multipart,
data_dict["id"],
)
if not fileobj:
raise tk.ObjectNotFound("upload")

owner = fileobj.owner_info
if not owner:
owner = Owner(
item_id=fileobj.id,
item_type="file" if data_dict["completed"] else "multipart",
)
context["session"].add(owner)

owner.owner_id = data_dict["owner_id"]
owner.owner_type = data_dict["owner_type"]
sess.commit()

return owner.dictize(context)
Loading

0 comments on commit f8d385d

Please sign in to comment.