diff --git a/invenio_config_tugraz/__init__.py b/invenio_config_tugraz/__init__.py index d976f26..a86122e 100644 --- a/invenio_config_tugraz/__init__.py +++ b/invenio_config_tugraz/__init__.py @@ -9,7 +9,7 @@ """invenio module that adds tugraz configs.""" from .ext import InvenioConfigTugraz -from .generators import RecordIp +from .permissions.generators import RecordIp from .utils import get_identity_from_user_by_email __version__ = "0.12.1" diff --git a/invenio_config_tugraz/base_permissions.py b/invenio_config_tugraz/base_permissions.py deleted file mode 100644 index 9511c41..0000000 --- a/invenio_config_tugraz/base_permissions.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2020-2021 Graz University of Technology. -# -# invenio-config-tugraz is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -""" -Records permission policies. - -Default policies for records: - -.. code-block:: python - - # Read access given to everyone. - can_search = [AnyUser()] - # Create action given to no one (Not even superusers) bc Deposits should - # be used. - can_create = [Disable()] - # Read access given to everyone if public record/files and owners always. - can_read = [AnyUserIfPublic(), RecordOwners()] - # Update access given to record owners. - can_update = [RecordOwners()] - # Delete access given to admins only. - can_delete = [Admin()] - # Associated files permissions (which are really bucket permissions) - can_read_files = [AnyUserIfPublic(), RecordOwners()] - can_update_files = [RecordOwners()] - -How to override default policies for records. - -Using Custom Generator for a policy: - -.. code-block:: python - - from invenio_rdm_records.permissions import RDMRecordPermissionPolicy - from invenio_config_tugraz.generators import RecordIp - - class TUGRAZPermissionPolicy(RDMRecordPermissionPolicy): - - # Delete access given to RecordIp only. - - can_delete = [RecordIp()] - - RECORDS_PERMISSIONS_RECORD_POLICY = TUGRAZPermissionPolicy - - -Permissions for Invenio records. -""" - -# from invenio_records_permissions.generators import ( -# Admin, -# AnyUser, -# AnyUserIfPublic, -# RecordOwners, -# ) -# from invenio_records_permissions.policies.base import BasePermissionPolicy - -# from .generators import RecordIp - - -# class TUGRAZPermissionPolicy(BasePermissionPolicy): -# """Access control configuration for records. - -# This overrides the /api/records endpoint. - -# """ - -# # Read access to API given to everyone. -# can_search = [AnyUser(), RecordIp()] - -# # Read access given to everyone if public record/files and owners always. -# can_read = [AnyUserIfPublic(), RecordOwners(), RecordIp()] - -# # Create action given to no one (Not even superusers) bc Deposits should -# # be used. -# can_create = [AnyUser()] - -# # Update access given to record owners. -# can_update = [RecordOwners()] - -# # Delete access given to admins only. -# can_delete = [Admin()] - -# # Associated files permissions (which are really bucket permissions) -# can_read_files = [AnyUserIfPublic(), RecordOwners()] -# can_update_files = [RecordOwners()] diff --git a/invenio_config_tugraz/generators.py b/invenio_config_tugraz/generators.py deleted file mode 100644 index b9c3e56..0000000 --- a/invenio_config_tugraz/generators.py +++ /dev/null @@ -1,223 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2020-2022 Graz University of Technology. -# -# invenio-config-tugraz is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -r"""Permission generators and policies for Invenio records. - -Invenio-records-permissions provides a means to fully customize access control -for Invenio records. It does so by defining and providing three layers of -permission constructs that build on each other: -Generators and Policies. You can extend or override them for maximum -control. Thankfully we provide default ones that cover most cases. - -Invenio-records-permissions conveniently structures (and relies on) -functionalities from -`invenio-access `_ and -`flask-principal `_ . - - -Generators ----------- - -Generators are the lowest level of abstraction provided by -invenio-records-permissions. A -:py:class:`~invenio_records_permissions.generators.Generator` represents -identities via -`Needs `_ that -are allowed or disallowed to act on a kind of object. A Generator does not -specify the action, but it does specify who is allowed and the kind of object -of concern (typically records). Generators *generate* required and forbidden -Needs at the object-of-concern level and *generate* query filters -at the search-for-objects-of-concern level. - -A Generator object defines 3 methods in addition to its constructor: - -- ``needs(self, **kwargs)``: returns Needs, one of which a provider is - required to have to be allowed -- ``excludes(self, **kwargs)``: returns a list of Needs disallowing any - provider of a single one -- ``query_filter(self, **kwargs)``: returns a query filter to enable retrieval - of records - -The ``needs`` and ``excludes`` methods specify access conditions from -the point-of-view of the object-of-concern; whereas, the ``query_filter`` -method specifies those from the actor's point-of-view in search scenarios. - -A simple example of a Generator is the provided -:py:class:`~invenio_records_permissions.generators.RecordOwners` Generator: - -.. code-block:: python - - from flask_principal import UserNeed - - - class RecordOwners(Generator): - '''Allows record owners.''' - - def needs(self, record=None, **kwargs): - '''Enabling Needs.''' - return [UserNeed(owner) for owner in record.get('owners', [])] - - def query_filter(self, record=None, **kwargs): - '''Filters for current identity as owner.''' - # NOTE: implementation subject to change until permissions metadata - # settled - provides = g.identity.provides - for need in provides: - if need.method == 'id': - return Q('term', owners=need.value) - return [] - -``RecordOwners`` allows any identity providing a `UserNeed -`_ -of value found in the ``owners`` metadata of a record. The -``query_filter(self, **kwargs)`` -method outputs a query that returns all owned records of the current user. -Not included in the above, because it doesn't apply to ``RecordOwners``, is -the ``excludes(self, **kwargs)`` method. - -.. Note:: - - Exclusion has priority over inclusion. If a Need is returned by both - ``needs`` and ``excludes``, providers of that Need will be **excluded**. - -If implementation of Generators seems daunting, fear not! A collection of -them has already been implemented in -:py:mod:`~invenio_records_permissions.generators` -and they cover most cases you may have. - -To fully understand how they work, we have to show where Generators are used. -That is in the Policies. - - -Policies --------- - -Classes inheriting from -:py:class:`~invenio_records_permissions.policies.base.BasePermissionPolicy` are -referred to as Policies. They list **what actions** can be done **by whom** -over an implied category of objects (typically records). A Policy is -instantiated on a per action basis and is a descendant of `Permission -`_ in -`invenio-access `_ . -Generators are used to provide the "by whom" part and the implied category of -object. - -Here is an example of a custom record Policy: - -.. code-block:: python - - from invenio_records_permissions.generators import AnyUser, RecordOwners, \ - SuperUser - from invenio_records_permissions.policies.base import BasePermissionPolicy - - class ExampleRecordPermissionPolicy(BasePermissionPolicy): - can_create = [AnyUser()] - can_search = [AnyUser()] - can_read = [AnyUser()] - can_update = [RecordOwners()] - can_foo_bar = [SuperUser()] - -The actions are class variables of the form: ``can_`` and the -corresponding (dis-)allowed identities are a list of Generator instances. -One can define any action as long as it follows that pattern and -is verified at the moment it is undertaken. - -In the example above, any user can create, list and read records, but only -a record's owner can edit it and only super users can perform the "foo_bar" -action. - -We recommend you extend the provided -:py:class:`invenio_records_permissions.policies.records.RecordPermissionPolicy` -to customize record permissions for your instance. -This way you benefit from sane defaults. - -After you have defined your own Policy, set it in your configuration: - -.. code-block:: python - - RECORDS_PERMISSIONS_RECORD_POLICY = ( - 'module.to.ExampleRecordPermissionPolicy' - ) - -The succinct encoding of the permissions for your instance gives you - - one central location where your permissions are defined - - exact control - - great flexibility by defining your own actions, generators and policies -""" - -from flask import current_app, request -from invenio_access.permissions import any_user -from invenio_records_permissions.generators import Generator -from invenio_search.engine import dsl - - -class RecordIp(Generator): - """Allowed any user with accessing with the IP.""" - - def needs(self, record=None, **kwargs): - """Enabling Needs, Set of Needs granting permission.""" - if record is None: - return [] - - # check if singleip is in the records restriction - is_single_ip = record.get("access", {}).get("access_right") == "singleip" - - # check if the user ip is on list - visible = self.check_permission() - - if not is_single_ip: - # if record does not have singleip - return any_user - return [any_user] - # if record has singleip, then check the ip of user - if ip user is on list - return any_user - elif visible: - return [any_user] - else: - # non of the above - return empty - return [] - - def excludes(self, **kwargs): - """Preventing Needs, Set of Needs denying permission. - - If ANY of the Needs are matched, permission is revoked. - - .. note:: - - ``_load_permissions()`` method from `Permission - `_ adds by default the - ``superuser_access`` Need (if tied to a User or Role) for us. - - It also expands ActionNeeds into the Users/Roles that - provide them. - - If the same Need is returned by `needs` and `excludes`, then that - Need provider is disallowed. - """ - return [] - - def query_filter(self, *args, **kwargs): - """Filters for singleip records.""" - # check if the user ip is on list - visible = self.check_permission() - - if not visible: - # If user ip is not on the list, and If the record contains 'singleip' will not be seen - return ~dsl.Q("match", **{"access.access_right": "singleip"}) - - # Lists all records - return dsl.Q("match_all") - - def check_permission(self): - """Check for User IP address in config variable.""" - # Get user IP - user_ip = request.remote_addr - # Checks if the user IP is among single IPs - if user_ip in current_app.config["INVENIO_CONFIG_TUGRAZ_SINGLE_IP"]: - return True - return False diff --git a/invenio_config_tugraz/permissions/__init__.py b/invenio_config_tugraz/permissions/__init__.py new file mode 100644 index 0000000..9bfa424 --- /dev/null +++ b/invenio_config_tugraz/permissions/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-config-tugraz is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Permission-policies and roles, based on `flask-principal`.""" + +from .policies import TUGrazRDMRecordPermissionPolicy + +__all__ = ("TUGrazRDMRecordPermissionPolicy",) diff --git a/invenio_config_tugraz/permissions/generators.py b/invenio_config_tugraz/permissions/generators.py new file mode 100644 index 0000000..d27cec8 --- /dev/null +++ b/invenio_config_tugraz/permissions/generators.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020-2024 Graz University of Technology. +# +# invenio-config-tugraz is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +r"""Permission generators for permission policies. + +invenio's permissions build on +`flask-principal `_ . + +In `flask-principal`, an action's `Need`s are checked +against current user's `Need`s to determine permissions. + +For example, the action of deleting a record is only +permitted to users with `Need(method='role', value='admin')`. + +Not all `Need`s can be known before the app is running. +For example, permissions for reading a record depend on whether +the record is public/private, so the set of `Need`s necessary +for reading a record must be computed dynamically at runtime. +This is the use case for +invenio's :py:class:`~invenio_records_permissions.generators.Generator`: +it generates `Need`s necessary for an action at runtime. + +A `Generator` object defines 3 methods in addition to its constructor: + +- ``needs(self, **kwargs)``: returns `Need`s, one of which a provider is + required to have to be allowed +- ``excludes(self, **kwargs)``: returns a list of `Need`s disallowing any + provider of a single one +- ``query_filter(self, **kwargs)``: returns a query filter to enable retrieval + of records + +The ``needs`` and ``excludes`` methods specify access conditions from +the point-of-view of the object-of-concern; whereas, the ``query_filter`` +method specifies those from the actor's point-of-view in search scenarios. + +.. Note:: + + Exclusion has priority over inclusion. If a `Need` is returned by both + ``needs`` and ``excludes``, providers of that `Need` will be **excluded**. + +""" + +from flask import current_app, request +from invenio_access.permissions import any_user +from invenio_records_permissions.generators import Generator +from invenio_search.engine import dsl + +from .roles import tugraz_authenticated_user + + +class RecordIp(Generator): + """Allowed any user with accessing with the IP.""" + + def needs(self, record=None, **kwargs): + """Enabling Needs, Set of Needs granting permission.""" + if record is None: + return [] + + # check if singleip is in the records restriction + is_single_ip = record.get("access", {}).get("access_right") == "singleip" + + # check if the user ip is on list + visible = self.check_permission() + + if not is_single_ip: + # if record does not have singleip - return any_user + return [any_user] + # if record has singleip, then check the ip of user - if ip user is on list - return any_user + elif visible: + return [any_user] + else: + # non of the above - return empty + return [] + + def excludes(self, **kwargs): + """Preventing Needs, Set of Needs denying permission. + + If ANY of the Needs are matched, permission is revoked. + + .. note:: + + ``_load_permissions()`` method from `Permission + `_ adds by default the + ``superuser_access`` Need (if tied to a User or Role) for us. + + It also expands ActionNeeds into the Users/Roles that + provide them. + + If the same Need is returned by `needs` and `excludes`, then that + Need provider is disallowed. + """ + return [] + + def query_filter(self, *args, **kwargs): + """Filters for singleip records.""" + # check if the user ip is on list + visible = self.check_permission() + + if not visible: + # If user ip is not on the list, and If the record contains 'singleip' will not be seen + return ~dsl.Q("match", **{"access.access_right": "singleip"}) + + # Lists all records + return dsl.Q("match_all") + + def check_permission(self): + """Check for User IP address in config variable.""" + # Get user IP + user_ip = request.remote_addr + # Checks if the user IP is among single IPs + if user_ip in current_app.config["INVENIO_CONFIG_TUGRAZ_SINGLE_IP"]: + return True + return False + + +class TUGrazAuthenticatedUser(Generator): + """Generates the `tugraz_authenticated_user` role-need.""" + + def needs(self, **__): + """Generate needs to be checked against current user identity.""" + return [tugraz_authenticated_user] diff --git a/invenio_config_tugraz/permissions/policies.py b/invenio_config_tugraz/permissions/policies.py new file mode 100644 index 0000000..08af03d --- /dev/null +++ b/invenio_config_tugraz/permissions/policies.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020-2024 Graz University of Technology. +# +# invenio-config-tugraz is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""TU Graz permission-policy for RDMRecordService. + +To use, set config-variable `RDM_PERMISSION_POLICY` to `TUGrazRDMRecordPermissionPolicy`. + +Policies list **what actions** can be done **by whom** +over an implied category of objects (typically records). A Policy is +instantiated on a per action basis and is a descendant of `Permission +`_ in +`invenio-access `_ . +Generators are used to provide the "by whom" part and the implied category of +object. + +Actions are class variables of the form: ``can_`` and the +corresponding (dis-)allowed identities are a list of Generator instances. +One can define any action as long as it follows that pattern and +is verified at the moment it is undertaken. +""" + + +from invenio_administration.generators import Administration +from invenio_communities.generators import CommunityCurators +from invenio_rdm_records.services.generators import ( + AccessGrant, + CommunityInclusionReviewers, + IfDeleted, + IfExternalDOIRecord, + IfFileIsLocal, + IfNewRecord, + IfRecordDeleted, + IfRestricted, + RecordCommunitiesAction, + RecordOwners, + ResourceAccessToken, + SecretLinks, + SubmissionReviewer, +) +from invenio_records_permissions.generators import ( + AnyUser, + Disable, + IfConfig, + SystemProcess, +) +from invenio_records_permissions.policies.records import RecordPermissionPolicy +from invenio_users_resources.services.permissions import UserManager + +from .generators import TUGrazAuthenticatedUser + + +class TUGrazRDMRecordPermissionPolicy(RecordPermissionPolicy): + """Overwrite authenticatedness to mean `tugraz_authenticated` rather than *signed up*.""" + + NEED_LABEL_TO_ACTION = { + "bucket-update": "update_files", + "bucket-read": "read_files", + "object-read": "read_files", + } + + # + # General permission-groups, to be used below + # + can_manage = [ + RecordOwners(), + RecordCommunitiesAction("curate"), + AccessGrant("manage"), + SystemProcess(), + ] + can_curate = can_manage + [AccessGrant("edit"), SecretLinks("edit")] + can_review = can_curate + [SubmissionReviewer()] + can_preview = can_curate + [ + AccessGrant("preview"), + SecretLinks("preview"), + SubmissionReviewer(), + UserManager, + ] + can_view = can_preview + [ + AccessGrant("view"), + SecretLinks("view"), + SubmissionReviewer(), + CommunityInclusionReviewers(), + RecordCommunitiesAction("view"), + ] + + can_tugraz_authenticated = [TUGrazAuthenticatedUser(), SystemProcess()] + can_authenticated = can_tugraz_authenticated + can_all = [AnyUser(), SystemProcess()] + + # + # Miscellaneous + # + # Allow for querying of statistics + # - This is currently disabled because it's not needed and could potentially + # open up surface for denial of service attacks + can_query_stats = [Disable()] + + # + # Records - reading and creating + # + can_search = can_all + can_read = [IfRestricted("record", then_=can_view, else_=can_all)] + + can_read_deleted = [ + IfRecordDeleted(then_=[UserManager, SystemProcess()], else_=can_read) + ] + can_read_deleted_files = can_read_deleted + can_media_read_deleted_files = can_read_deleted_files + can_read_files = [ + IfRestricted("files", then_=can_view, else_=can_all), + ResourceAccessToken("read"), + ] + can_get_content_files = [ + IfFileIsLocal(then_=can_read_files, else_=[SystemProcess()]) + ] + can_create = can_tugraz_authenticated + + # + # Drafts + # + can_search_drafts = can_tugraz_authenticated + can_read_draft = can_preview + can_draft_read_files = can_preview + [ResourceAccessToken("read")] + can_update_draft = can_review + can_draft_create_files = can_review + can_draft_set_content_files = [ + IfFileIsLocal(then_=can_review, else_=[SystemProcess()]) + ] + can_draft_get_content_files = [ + IfFileIsLocal(then_=can_draft_read_files, else_=[SystemProcess()]) + ] + can_draft_commit_files = [IfFileIsLocal(then_=can_review, else_=[SystemProcess()])] + can_draft_update_files = can_review + can_draft_delete_files = can_review + can_manage_files = [ + IfConfig( + "RDM_ALLOW_METADATA_ONLY_RECORDS", + then_=[IfNewRecord(then_=can_tugraz_authenticated, else_=can_review)], + else_=[], + ) + ] + can_manage_record_access = [ + IfConfig( + "RDM_ALLOW_RESTRICTED_RECORDS", + then_=[IfNewRecord(then_=can_tugraz_authenticated, else_=can_review)], + else_=[], + ) + ] + + # + # PIDs + # + can_pid_create = can_review + can_pid_register = can_review + can_pid_update = can_review + can_pid_discard = can_review + can_pid_delete = can_review + + # + # Actions + # + can_edit = [IfDeleted(then_=[Disable()], else_=can_curate)] + can_delete_draft = can_curate + can_new_version = [ + IfConfig( + "RDM_ALLOW_EXTERNAL_DOI_VERSIONING", + then_=can_curate, + else_=[IfExternalDOIRecord(then_=[Disable()], else_=can_curate)], + ) + ] + can_publish = can_review + can_lift_embargo = can_manage + + # + # Record communities + # + can_add_community = can_manage + can_remove_community = [RecordOwners(), CommunityCurators(), SystemProcess()] + can_remove_record = [CommunityCurators()] + can_bulk_add = [SystemProcess()] + + # + # Media files - draft + # + can_draft_media_create_files = can_review + can_draft_media_read_files = can_review + can_draft_media_set_content_files = [ + IfFileIsLocal(then_=can_review, else_=[SystemProcess()]) + ] + can_draft_media_get_content_files = [ + IfFileIsLocal(then_=can_preview, else_=[SystemProcess()]) + ] + can_draft_media_commit_files = [ + IfFileIsLocal(then_=can_preview, else_=[SystemProcess()]) + ] + can_draft_media_delete_files = can_review + can_draft_media_update_files = can_review + + # + # Media files - record + # + can_media_read_files = [ + IfRestricted("record", then_=can_view, else_=can_all), + ResourceAccessToken("read"), + ] + can_media_get_content_files = [ + IfFileIsLocal(then_=can_read, else_=[SystemProcess()]) + ] + can_media_create_files = [Disable()] + can_media_set_content_files = [Disable()] + can_media_commit_files = [Disable()] + can_media_update_files = [Disable()] + can_media_delete_files = [Disable()] + + # + # Record deletetion + # + can_delete = [Administration(), SystemProcess()] + can_delete_files = [SystemProcess()] + can_purge = [SystemProcess()] + + # + # Quotas for records/users + # + can_manage_quota = [UserManager, SystemProcess()] + + # + # Disabled + # + # - Records/files are updated/deleted via drafts so we don't support + # using below actions. + can_update = [Disable()] + can_create_files = [Disable()] + can_set_content_files = [Disable()] + can_commit_files = [Disable()] + can_update_files = [Disable()] + + # Used to hide at the moment the `parent.is_verified` field. It should be set to + # correct permissions based on which the field will be exposed only to moderators + can_moderate = [Disable()] + + +# Plans on where to use `RecordIp` generator: +# class TUGRAZPermissionPolicy(BasePermissionPolicy): + +# # Read access to API given to everyone. +# can_search = [AnyUser(), RecordIp()] + +# # Read access given to everyone if public record/files and owners always. +# can_read = [AnyUserIfPublic(), RecordOwners(), RecordIp()] diff --git a/invenio_config_tugraz/permissions/roles.py b/invenio_config_tugraz/permissions/roles.py new file mode 100644 index 0000000..3e63254 --- /dev/null +++ b/invenio_config_tugraz/permissions/roles.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-config-tugraz is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""`RoleNeed`s for permission policies. + +To use these roles, add them to the database via: + `$ invenio roles create tugraz_authenticated_user --description "..."` +then add roles to users via: + `$ invenio roles add user@email.com tugraz_authenticated_user` +""" + +from flask_principal import RoleNeed + +# using `flask_principal.RoleNeed`` instead of `invenio_access.SystemRoleNeed`, +# because these roles are assigned by an admin rather than automatically by the system +tugraz_authenticated_user = RoleNeed("tugraz_authenticated_user") diff --git a/invenio_config_tugraz/rdm_permissions.py b/invenio_config_tugraz/rdm_permissions.py deleted file mode 100644 index 714d171..0000000 --- a/invenio_config_tugraz/rdm_permissions.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2020-2021 Graz University of Technology. -# -# invenio-config-tugraz is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -""" -Records permission policies. - -Default policies for records: - -.. code-block:: python - - # Read access given to everyone. - can_search = [AnyUser()] - # Create action given to no one (Not even superusers) bc Deposits should - # be used. - can_create = [Disable()] - # Read access given to everyone if public record/files and owners always. - can_read = [AnyUserIfPublic(), RecordOwners()] - # Update access given to record owners. - can_update = [RecordOwners()] - # Delete access given to admins only. - can_delete = [Admin()] - # Associated files permissions (which are really bucket permissions) - can_read_files = [AnyUserIfPublic(), RecordOwners()] - can_update_files = [RecordOwners()] - -How to override default policies for rdm-records. - -Using Custom Generator for a policy: - -.. code-block:: python - - from invenio_rdm_records.services import ( - BibliographicRecordServiceConfig, - RDMRecordPermissionPolicy, - ) - - from invenio_config_tugraz.generators import RecordIp - - class TUGRAZPermissionPolicy(RDMRecordPermissionPolicy): - - # Create access given to SuperUser only. - - can_create = [SuperUser()] - - RDM_RECORDS_BIBLIOGRAPHIC_SERVICE_CONFIG = TUGRAZBibliographicRecordServiceConfig - - -Permissions for Invenio (RDM) Records. -""" - -# from invenio_rdm_records.services import RDMRecordPermissionPolicy -# from invenio_rdm_records.services.config import RDMRecordServiceConfig -# from invenio_rdm_records.services.generators import IfDraft, IfRestricted, RecordOwners -# from invenio_records_permissions.generators import ( -# Admin, -# AnyUser, -# AuthenticatedUser, -# Disable, -# SuperUser, -# SystemProcess, -# ) - - -# class TUGRAZPermissionPolicy(RDMRecordPermissionPolicy): -# """Access control configuration for rdm records. - -# This overrides the origin: -# https://github.com/inveniosoftware/invenio-rdm-records/blob/master/invenio_rdm_records/services/permissions.py. -# Access control configuration for records. -# Note that even if the array is empty, the invenio_access Permission class -# always adds the ``superuser-access``, so admins will always be allowed. -# - Create action given to everyone for now. -# - Read access given to everyone if public record and given to owners -# always. (inherited) -# - Update access given to record owners. (inherited) -# - Delete access given to admins only. (inherited) -# """ - - -# class TUGRAZRDMRecordServiceConfig(RDMRecordServiceConfig): -# """Overriding BibliographicRecordServiceConfig.""" diff --git a/tests/test_generators.py b/tests/test_generators.py index da8ee28..af80226 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -10,7 +10,7 @@ from invenio_access.permissions import any_user -from invenio_config_tugraz.generators import RecordIp +from invenio_config_tugraz.permissions.generators import RecordIp def test_recordip(create_app, open_record, singleip_record): diff --git a/tests/test_policies.py b/tests/test_policies.py new file mode 100644 index 0000000..786919e --- /dev/null +++ b/tests/test_policies.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-config-tugraz is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Tests for permissions-policy.""" + +from invenio_rdm_records.services.permissions import RDMRecordPermissionPolicy + +from invenio_config_tugraz.permissions.policies import TUGrazRDMRecordPermissionPolicy + +ALLOWED_DIFFERENCES = { + "can_authenticated", + "can_create", + "can_search_drafts", + "can_tugraz_authenticated", +} + + +def test_policies_synced(): + """Make sure our permission-policy stays synced with invenio's.""" + tugraz_cans = { + name: getattr(TUGrazRDMRecordPermissionPolicy, name) + for name in dir(TUGrazRDMRecordPermissionPolicy) + if name.startswith("can_") + } + rdm_cans = { + name: getattr(RDMRecordPermissionPolicy, name) + for name in dir(RDMRecordPermissionPolicy) + if name.startswith("can_") + } + + # check whether same set of `can_`s` + if extras := set(tugraz_cans) - set(rdm_cans) - ALLOWED_DIFFERENCES: + raise KeyError( + f"TU Graz's permission-policy has additional fields over invenio-rdm's:{extras}\n" + "if this is intentional, add to ALLOWED_DIFFERENCES in test-file\n" + "otherwise remove extraneous fields from TUGrazRDMRecordPermissionPolicy" + ) + if missing := set(rdm_cans) - set(tugraz_cans): + raise KeyError( + f"invenio-rdm's permission-policy has fields unhandled by TU Graz's: {missing}\n" + "if this is intentional, add to ALLOWED_DIFFERENCES\n" + "otherwise set the corresponding fields in TUGrazRDMRecordPermissionPolicy" + ) + + # check whether same permission-generators used for same `can_` + for can_name in rdm_cans.keys() & tugraz_cans.keys(): + if can_name in ALLOWED_DIFFERENCES: + continue + + tugraz_can = tugraz_cans[can_name] + rdm_can = rdm_cans[can_name] + + # permission-Generators don't implement equality checks for their instances + # we can however compare which types (classes) of Generators are used... + if {type(gen) for gen in tugraz_can} != {type(gen) for gen in rdm_can}: + raise ValueError( + f"permission-policy for `{can_name}` differs between TU-Graz and invenio-rdm\n" + "if this is intentional, add to ALLOWED_DIFFERENCES in test-file\n" + "otherwise fix TUGrazRDMRecordPermissionPolicy" + ) + + # check whether same `NEED_LABEL_TO_ACTION` + tugraz_label_to_action = TUGrazRDMRecordPermissionPolicy.NEED_LABEL_TO_ACTION + rdm_label_to_action = RDMRecordPermissionPolicy.NEED_LABEL_TO_ACTION + for label in tugraz_label_to_action.keys() & rdm_label_to_action.keys(): + if label in ALLOWED_DIFFERENCES: + continue + if tugraz_label_to_action.get(label) != rdm_label_to_action.get(label): + raise ValueError( + f"invenio-rdm's NEED_LABEL_TO_ACTION differs from TU Graz's in {label}\n" + "if this is intentional, add to ALLOWED_DIFFERENCES in test-file\n" + "otherwise fix TUGrazRDMRecordPermissionPolicy.NEED_LABEL_TO_ACTION" + )