From 8b3b577d32f2ea0e465d4c862df0672322e0a7d8 Mon Sep 17 00:00:00 2001 From: mb Date: Thu, 20 Aug 2020 14:47:30 +0200 Subject: [PATCH 1/7] how to:implementing generator for RecordIp. --- invenio_config_tugraz/__init__.py | 3 +- invenio_config_tugraz/config.py | 40 +++++ invenio_config_tugraz/permissions.py | 238 +++++++++++++++++++++++++++ setup.py | 2 + tests/test_permissions.py | 17 ++ 5 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 invenio_config_tugraz/permissions.py create mode 100644 tests/test_permissions.py diff --git a/invenio_config_tugraz/__init__.py b/invenio_config_tugraz/__init__.py index 70ad72f..2411e37 100644 --- a/invenio_config_tugraz/__init__.py +++ b/invenio_config_tugraz/__init__.py @@ -9,6 +9,7 @@ """invenio module that adds tugraz configs.""" from .ext import invenioconfigtugraz +from .permissions import RecordIp from .version import __version__ -__all__ = ('__version__', 'invenioconfigtugraz') +__all__ = ('__version__', 'invenioconfigtugraz', 'RecordIp') diff --git a/invenio_config_tugraz/config.py b/invenio_config_tugraz/config.py index b4833e7..5cc85c1 100644 --- a/invenio_config_tugraz/config.py +++ b/invenio_config_tugraz/config.py @@ -13,6 +13,20 @@ from flask_babelex import gettext as _ INVENIO_CONFIG_TUGRAZ_SHIBBOLETH = True """Set True if SAML is configured""" +INVENIO_CONFIG_TUGRAZ_SINGLE_IP = [] +"""Allows access to users whose IP address is listed. + +INVENIO_CONFIG_TUGRAZ_SINGLE_IP = + ["127.0.0.1", "127.0.0.2"] +""" + +INVENIO_CONFIG_TUGRAZ_IP_RANGES = [] +"""Allows access to users whose range of IP address is listed. + +INVENIO_CONFIG_TUGRAZ_IP_RANGES = +[["127.0.0.2", "127.0.0.99"], ["127.0.1.3", "127.0.1.5"]] +""" + # Invenio-App # =========== # See https://invenio-app.readthedocs.io/en/latest/configuration.html @@ -163,3 +177,29 @@ to render your overriden login.html RECAPTCHA_PUBLIC_KEY = None #: Recaptcha private key (change to enable). RECAPTCHA_PRIVATE_KEY = None + +# invenio-records-permissions +# ======= +# See: +# https://invenio-records-permissions.readthedocs.io/en/latest/configuration.html +# +"""" +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()] +""" diff --git a/invenio_config_tugraz/permissions.py b/invenio_config_tugraz/permissions.py new file mode 100644 index 0000000..02fc89d --- /dev/null +++ b/invenio_config_tugraz/permissions.py @@ -0,0 +1,238 @@ +# -*- 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, policies and factories 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, Policies and Factories. You can extend or override them for maximum +control. Thankfully we provide default ones that cover most cases. + +Factories make invenio-records-permissions immediately compatible +with any Invenio module requiring permission factories (e.g., +`invenio-records-rest `_ and +`invenio-files-rest `_ ). + +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 + +In turn, to fully understand how Policies fit in an Invenio project, we have to +show where *they* are used. And *that* is in the Factories. + + +Factories +--------- + +Most authorization is enforced through permission factories in Invenio: +simple functions that return a `Permission +`_ object. Thankfully, Policies are +just that kind of object. + +Invenio-records-permissions provides you with pre-made configurable record +permission factories here: +:py:mod:`invenio_records_permissions.factories.records` . You can follow the +pattern there to create other factories you may need. + +Pre-made factories +~~~~~~~~~~~~~~~~~~ + +By setting the following configuration in your instance: + +.. code-block:: python + + RECORDS_PERMISSIONS_RECORD_POLICY = ( + 'module.to.ExampleRecordPermissionPolicy' + ) + RECORDS_REST_ENDPOINTS = { + "recid": { + # ... + # We only display key-value pairs relevant to this explanation + 'read_permission_factory_imp': 'invenio_records_permissions.factories.record_read_permission_factory', # noqa + 'list_permission_factory_imp': 'invenio_records_permissions.factories.record_search_permission_factory', # noqa + 'create_permission_factory_imp': 'invenio_records_permissions.factories.record_create_permission_factory', # noqa + 'update_permission_factory_imp': 'invenio_records_permissions.factories.record_update_permission_factory', # noqa + 'delete_permission_factory_imp': 'invenio_records_permissions.factories.record_delete_permission_factory' # noqa + } + } + +you will be using the pre-made factories that know to look for their associated +action in ``module.to.ExampleRecordPermissionPolicy``. + +Custom factories +~~~~~~~~~~~~~~~~ + +To implement your own factories, create a factory with the required signature +and return an instance of your custom PermissionPolicy object with the +appropriate action. For example: + +.. code-block:: python + + def license_delete_permission_factory(license=None): + '''Delete permission factory for license records.''' + return LicensePermissionPolicy(action='delete', license=license) + + +With that, we covered all you need to know to fully specify access control in +your instance: combine and use permission Generators, Policies and Factories. + +Custom Generators. +""" +from elasticsearch_dsl.query import Q +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.""" + return [] + + def excludes(self, **kwargs): + """Preventing Needs.""" + return [] + + def query_filter(self, **kwargs): + """Elasticsearch filters.""" + return Q('match_all') diff --git a/setup.py b/setup.py index ff8098e..d6ddf4c 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,8 @@ setup_requires = [ install_requires = [ 'Flask-BabelEx>=0.9.4', + 'invenio-records-permissions~=0.9.0', + 'elasticsearch_dsl>=7.2.1', ] diff --git a/tests/test_permissions.py b/tests/test_permissions.py new file mode 100644 index 0000000..4d0721d --- /dev/null +++ b/tests/test_permissions.py @@ -0,0 +1,17 @@ +# -*- 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. + +from invenio_config_tugraz.permissions import RecordIp + + +def test_recordip(): + generator = RecordIp() + + assert generator.needs() == [] + assert generator.excludes() == [] + assert generator.query_filter().to_dict() == {'match_all': {}} From 8a5cef77f49dd04fcb49ff0cec9d083dece5b3c7 Mon Sep 17 00:00:00 2001 From: mb Date: Thu, 20 Aug 2020 15:00:48 +0200 Subject: [PATCH 2/7] documentation:how to override the default policies for records adding custom generator to the overriden policy. --- invenio_config_tugraz/config.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/invenio_config_tugraz/config.py b/invenio_config_tugraz/config.py index 5cc85c1..46e6105 100644 --- a/invenio_config_tugraz/config.py +++ b/invenio_config_tugraz/config.py @@ -203,3 +203,21 @@ Default policies for records: 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 import RecordIp + + class TUGRAZPermissionPolicy(RDMRecordPermissionPolicy): + + # Delete access given to RecordIp only. + can_delete = [RecordIp()] + + RECORDS_PERMISSIONS_RECORD_POLICY = TUGRAZPermissionPolicy +""" From 6e3e8e24c801efea3f98193583fcc46cd1abaff9 Mon Sep 17 00:00:00 2001 From: mb Date: Wed, 26 Aug 2020 13:21:12 +0200 Subject: [PATCH 3/7] generator documentations. --- invenio_config_tugraz/permissions.py | 38 +++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/invenio_config_tugraz/permissions.py b/invenio_config_tugraz/permissions.py index 02fc89d..a46b1d4 100644 --- a/invenio_config_tugraz/permissions.py +++ b/invenio_config_tugraz/permissions.py @@ -226,13 +226,45 @@ class RecordIp(Generator): # TODO: Implement def needs(self, **kwargs): - """Enabling Needs.""" + """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.""" + """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.""" + """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') From 9bd44f269b8881abd366158fa082a0d754a4fea2 Mon Sep 17 00:00:00 2001 From: mb Date: Fri, 18 Sep 2020 11:52:31 +0200 Subject: [PATCH 4/7] not relevant to permissions. by default this variable should be set to True. --- invenio_config_tugraz/config.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/invenio_config_tugraz/config.py b/invenio_config_tugraz/config.py index 46e6105..a21978b 100644 --- a/invenio_config_tugraz/config.py +++ b/invenio_config_tugraz/config.py @@ -80,8 +80,11 @@ SECURITY_EMAIL_SENDER = "info@invenio-test.tugraz.at" SECURITY_EMAIL_SUBJECT_REGISTER = _("Welcome to RDM!") """Email subject for account registration emails.""" -MAIL_SUPPRESS_SEND = False -"""Enable email sending by default.""" +MAIL_SUPPRESS_SEND = True +"""Enable email sending by default. + +Set this to False when sending actual emails. +""" # CORS - Cross-origin resource sharing # =========== From 2b71d1af73ce2bc813594db5cdb3299c541aae69 Mon Sep 17 00:00:00 2001 From: mb Date: Tue, 22 Sep 2020 13:27:50 +0200 Subject: [PATCH 5/7] Access control configuration updated the dependencies & documentation for permissions based on new release on invenio-records-permission. --- invenio_config_tugraz/config.py | 34 ++++++++++++- invenio_config_tugraz/permissions.py | 72 ++-------------------------- setup.py | 3 +- 3 files changed, 38 insertions(+), 71 deletions(-) diff --git a/invenio_config_tugraz/config.py b/invenio_config_tugraz/config.py index a21978b..7311801 100644 --- a/invenio_config_tugraz/config.py +++ b/invenio_config_tugraz/config.py @@ -9,6 +9,11 @@ """invenio module that adds tugraz configs.""" from flask_babelex import gettext as _ +from invenio_records_permissions.generators import Admin, AnyUser, \ + AnyUserIfPublic, Disable, RecordOwners +from invenio_records_permissions.policies.base import BasePermissionPolicy + +from .permissions import RecordIp INVENIO_CONFIG_TUGRAZ_SHIBBOLETH = True """Set True if SAML is configured""" @@ -215,7 +220,7 @@ Using Custom Generator for a policy: .. code-block:: python from invenio_rdm_records.permissions import RDMRecordPermissionPolicy - from invenio_config_tugraz import RecordIp + from invenio_config_tugraz.permissions import RecordIp class TUGRAZPermissionPolicy(RDMRecordPermissionPolicy): @@ -224,3 +229,30 @@ Using Custom Generator for a policy: RECORDS_PERMISSIONS_RECORD_POLICY = TUGRAZPermissionPolicy """ + + +class TUGRAZPermissionPolicy(BasePermissionPolicy): + """Access control configuration for records.""" + + # Read access to API given to everyone. + can_search = [AnyUser()] + + # Read access given to everyone if public record/files and owners always. + can_read = [AnyUserIfPublic(), RecordOwners()] + + # Create action given to no one (Not even superusers) bc Deposits should + # be used. + can_create = [Disable()] + + # 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()] + +RECORDS_PERMISSIONS_RECORD_POLICY = TUGRAZPermissionPolicy +"""Access control configuration for records.""" diff --git a/invenio_config_tugraz/permissions.py b/invenio_config_tugraz/permissions.py index a46b1d4..f1da123 100644 --- a/invenio_config_tugraz/permissions.py +++ b/invenio_config_tugraz/permissions.py @@ -6,19 +6,14 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. -r"""Permission generators, policies and factories for Invenio records. +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, Policies and Factories. You can extend or override them for maximum +Generators and Policies. You can extend or override them for maximum control. Thankfully we provide default ones that cover most cases. -Factories make invenio-records-permissions immediately compatible -with any Invenio module requiring permission factories (e.g., -`invenio-records-rest `_ and -`invenio-files-rest `_ ). - Invenio-records-permissions conveniently structures (and relies on) functionalities from `invenio-access `_ and @@ -154,69 +149,8 @@ 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 - -In turn, to fully understand how Policies fit in an Invenio project, we have to -show where *they* are used. And *that* is in the Factories. - - -Factories ---------- - -Most authorization is enforced through permission factories in Invenio: -simple functions that return a `Permission -`_ object. Thankfully, Policies are -just that kind of object. - -Invenio-records-permissions provides you with pre-made configurable record -permission factories here: -:py:mod:`invenio_records_permissions.factories.records` . You can follow the -pattern there to create other factories you may need. - -Pre-made factories -~~~~~~~~~~~~~~~~~~ - -By setting the following configuration in your instance: - -.. code-block:: python - - RECORDS_PERMISSIONS_RECORD_POLICY = ( - 'module.to.ExampleRecordPermissionPolicy' - ) - RECORDS_REST_ENDPOINTS = { - "recid": { - # ... - # We only display key-value pairs relevant to this explanation - 'read_permission_factory_imp': 'invenio_records_permissions.factories.record_read_permission_factory', # noqa - 'list_permission_factory_imp': 'invenio_records_permissions.factories.record_search_permission_factory', # noqa - 'create_permission_factory_imp': 'invenio_records_permissions.factories.record_create_permission_factory', # noqa - 'update_permission_factory_imp': 'invenio_records_permissions.factories.record_update_permission_factory', # noqa - 'delete_permission_factory_imp': 'invenio_records_permissions.factories.record_delete_permission_factory' # noqa - } - } - -you will be using the pre-made factories that know to look for their associated -action in ``module.to.ExampleRecordPermissionPolicy``. - -Custom factories -~~~~~~~~~~~~~~~~ - -To implement your own factories, create a factory with the required signature -and return an instance of your custom PermissionPolicy object with the -appropriate action. For example: - -.. code-block:: python - - def license_delete_permission_factory(license=None): - '''Delete permission factory for license records.''' - return LicensePermissionPolicy(action='delete', license=license) - - -With that, we covered all you need to know to fully specify access control in -your instance: combine and use permission Generators, Policies and Factories. - -Custom Generators. """ + from elasticsearch_dsl.query import Q from invenio_records_permissions.generators import Generator diff --git a/setup.py b/setup.py index d6ddf4c..a3c6207 100644 --- a/setup.py +++ b/setup.py @@ -37,8 +37,9 @@ setup_requires = [ install_requires = [ 'Flask-BabelEx>=0.9.4', - 'invenio-records-permissions~=0.9.0', 'elasticsearch_dsl>=7.2.1', + 'invenio-rdm-records~=0.18.3', + 'invenio_search>=1.3.1', ] From 2f4df9167bea3bfc5695d98707cedec36bb72de4 Mon Sep 17 00:00:00 2001 From: mb Date: Wed, 23 Sep 2020 14:22:02 +0200 Subject: [PATCH 6/7] seperate file for access control configuration permissionsPolicy file will handle the access control configurations. --- invenio_config_tugraz/config.py | 71 +----------------- invenio_config_tugraz/permissionsPolicy.py | 84 ++++++++++++++++++++++ 2 files changed, 87 insertions(+), 68 deletions(-) create mode 100644 invenio_config_tugraz/permissionsPolicy.py diff --git a/invenio_config_tugraz/config.py b/invenio_config_tugraz/config.py index 7311801..c9329b6 100644 --- a/invenio_config_tugraz/config.py +++ b/invenio_config_tugraz/config.py @@ -9,11 +9,6 @@ """invenio module that adds tugraz configs.""" from flask_babelex import gettext as _ -from invenio_records_permissions.generators import Admin, AnyUser, \ - AnyUserIfPublic, Disable, RecordOwners -from invenio_records_permissions.policies.base import BasePermissionPolicy - -from .permissions import RecordIp INVENIO_CONFIG_TUGRAZ_SHIBBOLETH = True """Set True if SAML is configured""" @@ -191,68 +186,8 @@ RECAPTCHA_PRIVATE_KEY = None # See: # https://invenio-records-permissions.readthedocs.io/en/latest/configuration.html # -"""" -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 -""" - - -class TUGRAZPermissionPolicy(BasePermissionPolicy): - """Access control configuration for records.""" - - # Read access to API given to everyone. - can_search = [AnyUser()] - - # Read access given to everyone if public record/files and owners always. - can_read = [AnyUserIfPublic(), RecordOwners()] - - # Create action given to no one (Not even superusers) bc Deposits should - # be used. - can_create = [Disable()] - - # 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()] - -RECORDS_PERMISSIONS_RECORD_POLICY = TUGRAZPermissionPolicy +RECORDS_PERMISSIONS_RECORD_POLICY = ( + 'invenio_config_tugraz.permissionsPolicy.TUGRAZPermissionPolicy' +) """Access control configuration for records.""" diff --git a/invenio_config_tugraz/permissionsPolicy.py b/invenio_config_tugraz/permissionsPolicy.py new file mode 100644 index 0000000..3bb8af1 --- /dev/null +++ b/invenio_config_tugraz/permissionsPolicy.py @@ -0,0 +1,84 @@ +# -*- 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()] From b4a2f2a36b61a30514e875b075a8871bb1cc090d Mon Sep 17 00:00:00 2001 From: mb Date: Thu, 24 Sep 2020 10:42:14 +0200 Subject: [PATCH 7/7] get user remote_ip * function to check user ip. * renamed the files. --- invenio_config_tugraz/__init__.py | 2 +- invenio_config_tugraz/config.py | 2 +- invenio_config_tugraz/generators.py | 214 +++++++++++++++++ invenio_config_tugraz/permissions.py | 218 ++++-------------- invenio_config_tugraz/permissionsPolicy.py | 84 ------- ...test_permissions.py => test_generators.py} | 2 +- 6 files changed, 266 insertions(+), 256 deletions(-) create mode 100644 invenio_config_tugraz/generators.py delete mode 100644 invenio_config_tugraz/permissionsPolicy.py rename tests/{test_permissions.py => test_generators.py} (88%) 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():