Skip to content

Commit

Permalink
chore: add files_file_scan action
Browse files Browse the repository at this point in the history
  • Loading branch information
smotornyuk committed Jun 26, 2024
1 parent 8b167b8 commit e930bd8
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 106 deletions.
135 changes: 100 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,22 +556,35 @@ assigned as an owner of the file. From now on, the owner can perform other
operations, such as renaming/displaying/removing with the file.
Apart from chaining auth function, to modify access rules for the file, plugin
can implement `IFiles.files_is_allowed` method.
can implement `IFiles.files_file_allows` and `IFiles.files_owner_allows`
methods.
```python
def files_is_allowed(
def files_file_allows(
self,
context: Context,
file: File | Multipart | None,
operation: types.AuthOperation,
next_owner: Any | None,
file: File | Multipart,
operation: types.FileOperation,
) -> bool | None:
...
def files_owner_allows(
self,
context: Context,
owner_type: str, owner_id: str,
operation: types.OwnerOperation,
) -> bool | None:
...
```
This method receives current action context, the file that is accessed, the
name of operation(`show`, `update`, `delete`, `file_transfer`) and the next
owner in case of file transfer.
These methods receive current action context, the tested object details, and
the name of operation(`show`, `update`, `delete`,
`file_transfer`). `files_file_allows` checks permission for accessed file. It's
usually called when user interacts with file directly. `files_owner_allows`
works with owner described by type and ID. It's usually called when user
transfer file ownership, perform bulk file operation for owner files, or just
trying to get the list of files that belongs to owner.
If method returns true/false, operation is allowed/denied. If method returns
`None`, default logic used to check access.
Expand All @@ -580,28 +593,34 @@ As already mentoined, by default, user who owns the file, can access it. But
what about different owners? What if file owned by other entity, like resource
or dataset?
Out of the box, nobody can access such files. But there are two config options
that modify this restriction. `ckanext.files.owner.cascade_access =
ENTITY_TYPE` gives access to file owned by entity if user already has access to
entity itself. Use words like `package`, `resource`, `group` instead of
`ENTITY_TYPE`.
Out of the box, nobody can access such files. But there are three config
options that modify this restriction.
`ckanext.files.owner.cascade_access = ENTITY_TYPE ANOTHER_TYPE` gives access to
file owned by entity if user already has access to entity itself. Use words
like `package`, `resource`, `group` instead of `ENTITY_TYPE`.
For example: file is owned by *resource*. If cascade access is enabled, whoever
has access to `resource_show` of the *resource*, can also see the file owned by
this resource. If user passes `resource_update` for *resource*, he can also
modify the file owned by this resource, etc.
The second option is `ckanext.files.owner.transfer_as_update`. By default,
there is no auth function that gives user cascade permission to modify
ownership of the file. But when transfer-as-update enabled together with
cascade access, any user who has `resource_update`, can also modify ownership
of the file owned by *resource*.
Be careful and do not add `user` to
Important: be careful and do not add `user` to
`ckanext.files.owner.cascade_access`. User's own files are considered private
and most likely you don't really need anyone else to be able to see or modify
these files.
The second option is `ckanext.files.owner.transfer_as_update`. When
transfer-as-update enabled, any user who has `<OWNER_TYPE>_update` permission,
can transfer own files to this `OWNER_TYPE`. Intead of using this option, you
can define `<OWNER_TYPE>_file_transfer`.
And the third option is `ckanext.files.owner.scan_as_update`. Just as with
ownership transfer, it gives user permission to list all files of the owner if
user can `<OWNER_TYPE>_update` it. Intead of using this option, you
can define `<OWNER_TYPE>_file_scan`.
### Ownership transfer
File ownership can be transfered. As there can be only one owner of the file,
Expand All @@ -611,7 +630,7 @@ To transfer ownership, use `files_transfer_ownership` action and specify `id`
of the file, `owner_id` and `owner_type` of the new owner.
You can't just transfer ownership to anyone. You either must pass
`IFiles.files_is_allowed` check for `file_transfer` operation, or pass a
`IFiles.files_owner_allows` check for `file_transfer` operation, or pass a
cascade access check for the future owner of the file when cascade access and
transfer-as-update is enabled.
Expand Down Expand Up @@ -1618,24 +1637,29 @@ class IFiles(Interface):
"""
return {}
def files_is_allowed(
def files_file_allows(
self,
context: Context,
file: File | Multipart | None,
operation: types.AuthOperation,
next_owner: Any | None,
file: File | Multipart,
operation: types.FileOperation,
) -> bool | None:
"""Decide if user is allowed to perform specified operation on the 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.
other plugins.
Default implementation relies on cascade_access config option. If owner
of file is included into cascade access, user can perform operation on
file if he can perform the same operation with file's owner.
If current owner is not affected by cascade access, user can perform
operation on file only if user owns the file.
Example:
>>> def files_is_allowed(
>>> self, context, file, operation, next_owner
>>> def files_file_allows(
>>> self, context,
>>> file: shared.File | shared.Multipart,
>>> operation: shared.types.FileOperation
>>> ) -> bool | None:
>>> if file.owner_info and file.owner_info.owner_type == "resource":
>>> return is_authorized_boolean(
Expand All @@ -1646,6 +1670,42 @@ class IFiles(Interface):
>>>
>>> return None
"""
return None
def files_owner_allows(
self,
context: Context,
owner_type: str,
owner_id: str,
operation: types.OwnerOperation,
) -> bool | None:
"""Decide if user is allowed to perform specified operation on the owner.
Return True/False if user allowed/not allowed. Return `None` to rely on
other plugins.
Default implementation relies on cascade_access config option.
If current owner is not affected by cascade access, user can perform
operations only on himself.
Example:
>>> def files_owner_allows(
>>> self, context,
>>> owner_type: str, owner_id: str,
>>> operation: shared.types.OwnerOperation
>>> ) -> bool | None:
>>> if owner_type == "resource" and operation == "file_transfer":
>>> return is_authorized_boolean(
>>> f"resource_update",
>>> context,
>>> {"id": owner_id}
>>> )
>>>
>>> return None
"""
return None
```
Expand Down Expand Up @@ -1744,12 +1804,17 @@ ckanext.files.authenticated_uploads.storages = default
# (optional, default: package resource group organization)
ckanext.files.owner.cascade_access = package resource group organization
# Use `*_update` auth function to check cascade access for ownership
# transfer. Works with `ckanext.files.owner.cascade_access`, which by itself
# will check `*_file_transfer` auth function, but switch to `*_update` when
# this flag is enabled.
# Use `<OWNER_TYPE>_update` auth function to check access for ownership
# transfer. When this flag is disabled `<OWNER_TYPE>_file_transfer` auth
# function is used.
# (optional, default: true)
ckanext.files.owner.transfer_as_update = true
# Use `<OWNER_TYPE>_update` auth function to check access when listing all
# files of the owner. When this flag is disabled `<OWNER_TYPE>_file_scan`
# auth function is used.
# (optional, default: true)
ckanext.files.owner.scan_as_update = true
```
### Storage configuration
Expand Down
11 changes: 10 additions & 1 deletion ckanext/files/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
DEFAULT_STORAGE = "ckanext.files.default_storage"
CASCADE_ACCESS = "ckanext.files.owner.cascade_access"
TRANSFER_AS_UPDATE = "ckanext.files.owner.transfer_as_update"
SCAN_AS_UPDATE = "ckanext.files.owner.scan_as_update"
AUTHENTICATED_UPLOADS = "ckanext.files.authenticated_uploads.allow"
AUTHENTICATED_STORAGES = "ckanext.files.authenticated_uploads.storages"
USER_IMAGES_STORAGE = "ckanext.files.user_images_storage"
Expand Down Expand Up @@ -63,11 +64,19 @@ def authenticated_uploads() -> bool:


def transfer_as_update() -> bool:
"""Use `*_update` auth function to check cascade access for ownership transfer."""
"""Use `*_update` auth function to check cascade access for ownership
transfer."""

return tk.config[TRANSFER_AS_UPDATE]


def scan_as_update() -> bool:
"""Use `*_update` auth function to check cascade access when listing files
of the owner."""

return tk.config[SCAN_AS_UPDATE]


def authenticated_storages() -> list[str]:
"""Names of storages that can by used by non-sysadmin users when
authenticated uploads enabled.
Expand Down
15 changes: 11 additions & 4 deletions ckanext/files/config_declaration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,17 @@ groups:
type: bool
default: true
description: |
Use `*_update` auth function to check cascade access for ownership
transfer. Works with `ckanext.files.owner.cascade_access`, which by
itself will check `*_file_transfer` auth function, but switch to
`*_update` when this flag is enabled.
Use `<OWNER_TYPE>_update` auth function to check access for
ownership transfer. When this flag is disabled
`<OWNER_TYPE>_file_transfer` auth function is used.
- key: ckanext.files.owner.scan_as_update
type: bool
default: true
description: |
Use `<OWNER_TYPE>_update` auth function to check access when
listing all files of the owner. When this flag is disabled
`<OWNER_TYPE>_file_scan` auth function is used.
- key: ckanext.files.storage.<NAME>.<OPTION>
type: dynamic
Expand Down
55 changes: 45 additions & 10 deletions ckanext/files/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,29 @@ def files_register_owner_getters(self) -> dict[str, Callable[[str], Any]]:
"""
return {}

def files_is_allowed(
def files_file_allows(
self,
context: Context,
file: File | Multipart | None,
operation: types.AuthOperation,
next_owner: Any | None,
file: File | Multipart,
operation: types.FileOperation,
) -> bool | None:
"""Decide if user is allowed to perform specified operation on the 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.
other plugins.
Default implementation relies on cascade_access config option. If owner
of file is included into cascade access, user can perform operation on
file if he can perform the same operation with file's owner.
If current owner is not affected by cascade access, user can perform
operation on file only if user owns the file.
Example:
>>> def files_is_allowed(
>>> self, context, file, operation, next_owner
>>> def files_file_allows(
>>> self, context,
>>> file: shared.File | shared.Multipart,
>>> operation: shared.types.FileOperation
>>> ) -> bool | None:
>>> if file.owner_info and file.owner_info.owner_type == "resource":
>>> return is_authorized_boolean(
Expand All @@ -72,3 +77,33 @@ def files_is_allowed(
"""
return None

def files_owner_allows(
self,
context: Context,
owner_type: str,
owner_id: str,
operation: types.OwnerOperation,
) -> bool | None:
"""Decide if user is allowed to perform specified operation on the owner.
Return True/False if user allowed/not allowed. Return `None` to rely on
other plugins.
Example:
>>> def files_owner_allows(
>>> self, context,
>>> owner_type: str, owner_id: str,
>>> operation: shared.types.OwnerOperation
>>> ) -> bool | None:
>>> if owner_type == "resource" and operation == "file_transfer":
>>> return is_authorized_boolean(
>>> f"resource_update",
>>> context,
>>> {"id": owner_id}
>>> )
>>>
>>> return None
"""
return None
43 changes: 41 additions & 2 deletions ckanext/files/logic/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,10 @@ def files_file_search( # noqa: C901, PLR0912
Returns:
* `count`: total number of files mathing filters
* `count`: total number of files matching filters
* `results`: array of dictionaries with file details.
"""

tk.check_access("files_file_search", context, data_dict)
sess = context["session"]

Expand Down Expand Up @@ -715,6 +714,46 @@ def files_multipart_complete(
return fileobj.dictize(context)


@tk.side_effect_free
@validate(schema.file_scan)
def files_file_scan(
context: Context,
data_dict: dict[str, Any],
) -> dict[str, Any]:
"""List files of the owner
This action internally calls files_file_search, but with static values of
owner filters. If owner is not specified, files filtered by current
user. If owner is specified, user must pass authorization check to see
files.
Params:
* `owner_id`: ID of the owner
* `owner_type`: type of the owner
The all other parameters are passed as-is to `files_file_search`.
Returns:
* `count`: total number of files matching filters
* `results`: array of dictionaries with file details.
"""
if not data_dict["owner_id"] and data_dict["owner_type"] == "user":
user = context.get("auth_user_obj")

if isinstance(user, model.User) or (user := model.User.get(context["user"])):
data_dict["owner_id"] = user.id


tk.check_access("files_file_scan", context, data_dict)

params = data_dict.pop("__extras", {})
params.update(data_dict)
return tk.get_action("files_file_search")({"ignore_auth": True}, params)


@validate(schema.transfer_ownership)
def files_transfer_ownership(
context: Context,
Expand Down
Loading

0 comments on commit e930bd8

Please sign in to comment.