diff --git a/invenio_config_tugraz/__init__.py b/invenio_config_tugraz/__init__.py index 2411e37..5a50bb0 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 .permissions import RecordIp +from .generators import RecordIp from .version import __version__ __all__ = ('__version__', 'invenioconfigtugraz', 'RecordIp') diff --git a/invenio_config_tugraz/config.py b/invenio_config_tugraz/config.py index c9329b6..c723832 100644 --- a/invenio_config_tugraz/config.py +++ b/invenio_config_tugraz/config.py @@ -188,6 +188,6 @@ RECAPTCHA_PRIVATE_KEY = None # RECORDS_PERMISSIONS_RECORD_POLICY = ( - 'invenio_config_tugraz.permissionsPolicy.TUGRAZPermissionPolicy' + 'invenio_config_tugraz.permissions.TUGRAZPermissionPolicy' ) """Access control configuration for records.""" diff --git a/invenio_config_tugraz/generators.py b/invenio_config_tugraz/generators.py new file mode 100644 index 0000000..7429679 --- /dev/null +++ b/invenio_config_tugraz/generators.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020 Mojib Wali. +# +# 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 elasticsearch_dsl.query import Q +from flask import current_app, request +from invenio_records_permissions.generators import Generator + + +class RecordIp(Generator): + """Allowed any user with accessing with the IP.""" + + # TODO: Implement + def needs(self, **kwargs): + """Enabling Needs, Set of Needs granting permission. + + If ANY of the Needs are matched, permission is granted. + + .. 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. + """ + 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, **kwargs): + """Elasticsearch filters, List of ElasticSearch query filters. + + These filters consist of additive queries mapping to what the current + user should be able to retrieve via search. + """ + return 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.py b/invenio_config_tugraz/permissions.py index f1da123..dd06f27 100644 --- a/invenio_config_tugraz/permissions.py +++ b/invenio_config_tugraz/permissions.py @@ -6,199 +6,79 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. -r"""Permission generators and policies for Invenio records. +""" +Records permission policies. -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: +Default policies for records: .. code-block:: python - from flask_principal import UserNeed + # 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. - 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: +Using Custom Generator for a policy: .. code-block:: python - from invenio_records_permissions.generators import AnyUser, RecordOwners, \ - SuperUser - from invenio_records_permissions.policies.base import BasePermissionPolicy + from invenio_rdm_records.permissions import RDMRecordPermissionPolicy + from invenio_config_tugraz.generators import RecordIp - class ExampleRecordPermissionPolicy(BasePermissionPolicy): - can_create = [AnyUser()] - can_search = [AnyUser()] - can_read = [AnyUser()] - can_update = [RecordOwners()] - can_foo_bar = [SuperUser()] + class TUGRAZPermissionPolicy(RDMRecordPermissionPolicy): -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. + # Delete access given to RecordIp only. -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. + can_delete = [RecordIp()] -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. + RECORDS_PERMISSIONS_RECORD_POLICY = TUGRAZPermissionPolicy -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 +Permissions for Invenio (RDM) Records. """ -from elasticsearch_dsl.query import Q -from invenio_records_permissions.generators import Generator +from invenio_records_permissions.generators import Admin, AnyUser, \ + AnyUserIfPublic, Disable, RecordOwners +from invenio_records_permissions.policies.base import BasePermissionPolicy + +from .generators import RecordIp -class RecordIp(Generator): - """Allowed any user with accessing with the IP.""" +class TUGRAZPermissionPolicy(BasePermissionPolicy): + """Access control configuration for records. - # TODO: Implement - def needs(self, **kwargs): - """Enabling Needs, Set of Needs granting permission. + This overrides the /api/records endpoint. - If ANY of the Needs are matched, permission is granted. + """ - .. note:: + # Read access to API given to everyone. + can_search = [AnyUser(), RecordIp()] - ``_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. - """ - return [] + # Read access given to everyone if public record/files and owners always. + can_read = [AnyUserIfPublic(), RecordOwners(), RecordIp()] - def excludes(self, **kwargs): - """Preventing Needs, Set of Needs denying permission. + # Create action given to no one (Not even superusers) bc Deposits should + # be used. + can_create = [AnyUser()] - If ANY of the Needs are matched, permission is revoked. + # Update access given to record owners. + can_update = [RecordOwners()] - .. note:: + # Delete access given to admins only. + can_delete = [Admin()] - ``_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, **kwargs): - """Elasticsearch filters, List of ElasticSearch query filters. - - These filters consist of additive queries mapping to what the current - user should be able to retrieve via search. - """ - return Q('match_all') + # Associated files permissions (which are really bucket permissions) + can_read_files = [AnyUserIfPublic(), RecordOwners()] + can_update_files = [RecordOwners()] diff --git a/invenio_config_tugraz/permissionsPolicy.py b/invenio_config_tugraz/permissionsPolicy.py deleted file mode 100644 index 3bb8af1..0000000 --- a/invenio_config_tugraz/permissionsPolicy.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2020 Mojib Wali. -# -# 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.permissions import RecordIp - - class TUGRAZPermissionPolicy(RDMRecordPermissionPolicy): - - # Delete access given to RecordIp only. - - can_delete = [RecordIp()] - - RECORDS_PERMISSIONS_RECORD_POLICY = TUGRAZPermissionPolicy - - -Permissions for Invenio (RDM) Records. -""" - -from invenio_records_permissions.generators import Admin, AnyUser, \ - AnyUserIfPublic, Disable, RecordOwners -from invenio_records_permissions.policies.base import BasePermissionPolicy - -from .permissions 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/tests/test_permissions.py b/tests/test_generators.py similarity index 88% rename from tests/test_permissions.py rename to tests/test_generators.py index 4d0721d..dda6fc1 100644 --- a/tests/test_permissions.py +++ b/tests/test_generators.py @@ -6,7 +6,7 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. -from invenio_config_tugraz.permissions import RecordIp +from invenio_config_tugraz.generators import RecordIp def test_recordip():