diff --git a/invenio_config_tugraz/__init__.py b/invenio_config_tugraz/__init__.py index a86122e..32e2f41 100644 --- a/invenio_config_tugraz/__init__.py +++ b/invenio_config_tugraz/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020-2022 Graz University of Technology. +# 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 @@ -9,7 +9,6 @@ """invenio module that adds tugraz configs.""" from .ext import InvenioConfigTugraz -from .permissions.generators import RecordIp from .utils import get_identity_from_user_by_email __version__ = "0.12.1" @@ -17,6 +16,5 @@ __version__ = "0.12.1" __all__ = ( "__version__", "InvenioConfigTugraz", - "RecordIp", "get_identity_from_user_by_email", ) diff --git a/invenio_config_tugraz/config.py b/invenio_config_tugraz/config.py index 4eae1ce..dd0ddda 100644 --- a/invenio_config_tugraz/config.py +++ b/invenio_config_tugraz/config.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020-2023 Graz University of Technology. +# 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 @@ -10,23 +10,26 @@ from invenio_i18n import gettext as _ -INVENIO_CONFIG_TUGRAZ_SHIBBOLETH = False +CONFIG_TUGRAZ_SHIBBOLETH = False """Set True if SAML is configured""" -INVENIO_CONFIG_TUGRAZ_SINGLE_IP = [] +CONFIG_TUGRAZ_SINGLE_IPS = [] """Allows access to users whose IP address is listed. -INVENIO_CONFIG_TUGRAZ_SINGLE_IP = +INVENIO_CONFIG_TUGRAZ_SINGLE_IPS = ["127.0.0.1", "127.0.0.2"] """ -INVENIO_CONFIG_TUGRAZ_IP_RANGES = [] +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"]] """ +CONFIG_TUGRAZ_IP_NETWORK = "" +"""Allows access to users who are in the IP network.""" + CONFIG_TUGRAZ_ROUTES = { "guide": "/guide", diff --git a/invenio_config_tugraz/custom_fields/__init__.py b/invenio_config_tugraz/custom_fields/__init__.py new file mode 100644 index 0000000..8995348 --- /dev/null +++ b/invenio_config_tugraz/custom_fields/__init__.py @@ -0,0 +1,15 @@ +# -*- 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. + +"""Custom fields.""" + + +from invenio_records_resources.services.custom_fields import BooleanCF + +ip_network = BooleanCF(name="ip_network") +single_ip = BooleanCF(name="single_ip") diff --git a/invenio_config_tugraz/ext.py b/invenio_config_tugraz/ext.py index be1a75a..a64962e 100644 --- a/invenio_config_tugraz/ext.py +++ b/invenio_config_tugraz/ext.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020-2022 Graz University of Technology. +# 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 @@ -9,6 +9,7 @@ """invenio module that adds tugraz configs.""" from . import config +from .custom_fields import ip_network, single_ip class InvenioConfigTugraz(object): @@ -22,6 +23,7 @@ class InvenioConfigTugraz(object): def init_app(self, app): """Flask application initialization.""" self.init_config(app) + self.add_custom_fields(app) app.extensions["invenio-config-tugraz"] = self def init_config(self, app): @@ -30,6 +32,12 @@ class InvenioConfigTugraz(object): if k.startswith("INVENIO_CONFIG_TUGRAZ_"): app.config.setdefault(k, getattr(config, k)) + def add_custom_fields(self, app): + """Add custom fields.""" + app.config.setdefault("RDM_CUSTOM_FIELDS", []) + app.config["RDM_CUSTOM_FIELDS"].append(ip_network) + app.config["RDM_CUSTOM_FIELDS"].append(single_ip) + def finalize_app(app): """Finalize app.""" diff --git a/invenio_config_tugraz/permissions/generators.py b/invenio_config_tugraz/permissions/generators.py index d27cec8..6990d3a 100644 --- a/invenio_config_tugraz/permissions/generators.py +++ b/invenio_config_tugraz/permissions/generators.py @@ -45,7 +45,11 @@ method specifies those from the actor's point-of-view in search scenarios. """ +from ipaddress import ip_address, ip_network +from typing import Any + from flask import current_app, request +from flask_principal import Need from invenio_access.permissions import any_user from invenio_records_permissions.generators import Generator from invenio_search.engine import dsl @@ -53,32 +57,27 @@ from invenio_search.engine import dsl from .roles import tugraz_authenticated_user -class RecordIp(Generator): +class RecordSingleIP(Generator): """Allowed any user with accessing with the IP.""" - def needs(self, record=None, **kwargs): - """Enabling Needs, Set of Needs granting permission.""" + def needs(self, record: dict | None = None, **__: dict) -> list[Need]: + """Set of Needs granting permission. Enabling Needs.""" 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 + # if record does not have singleip - return any_user + if not record.get("custom_fields", {}).get("single_ip", False): 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 record has singleip, and the ip of the user matches the allowed ip + if self.check_permission(): + return [any_user] + + # non of the above - return empty + return [] + + def excludes(self, **kwargs: dict) -> list[Need]: + """Set of Needs denying permission. Preventing Needs. If ANY of the Needs are matched, permission is revoked. @@ -95,33 +94,116 @@ class RecordIp(Generator): If the same Need is returned by `needs` and `excludes`, then that Need provider is disallowed. """ - return [] + try: + if ( + kwargs["record"]["custom_fields"]["single_ip"] + and not self.check_permission() + ): + return [any_user] - def query_filter(self, *args, **kwargs): - """Filters for singleip records.""" - # check if the user ip is on list - visible = self.check_permission() + except KeyError: + return [] + else: + return [] - if not visible: + def query_filter(self, *_: dict, **__: dict) -> Any: # noqa: ANN401 + """Filter for singleip records.""" + if not self.check_permission(): # 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"}) + return ~dsl.Q("match", **{"custom_fields.single_ip": True}) # 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 + def check_permission(self) -> bool: + """Check for User IP address in config variable. + + If the user ip is in the configured list return True. + """ + try: + user_ip = request.remote_addr + except RuntimeError: + return False + + single_ips = current_app.config["CONFIG_TUGRAZ_SINGLE_IPS"] + + return user_ip in single_ips + + +class AllowedFromIPNetwork(Generator): + """Allowed from ip range.""" + + def needs(self, record: dict | None = None, **__: dict) -> list[Need]: + """Set of Needs granting permission. Enabling Needs.""" + if record is None: + return [] + + # if the record doesn't have set the ip range allowance + if not record.get("custom_fields", {}).get("ip_network", False): + return [any_user] + + # if the record has set the ip_range allowance and is in the range + if self.check_permission(): + return [any_user] + + # non of the above - return empty + return [] + + def excludes(self, **kwargs: dict) -> Need: + """Set of Needs denying permission. Preventing Needs. + + 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. + """ + try: + if ( + kwargs["record"]["custom_fields"]["ip_network"] + and not self.check_permission() + ): + return [any_user] + + except KeyError: + return [] + else: + return [] + + def query_filter(self, *_: dict, **__: dict) -> Any: # noqa: ANN401 + """Filter for ip range records.""" + if not self.check_permission(): + return ~dsl.Q("match", **{"custom_fields.ip_network": True}) + + return dsl.Q("match_all") + + def check_permission(self) -> bool: + """Check for User IP address in the configured network.""" + try: + user_ip = request.remote_addr + except RuntimeError: + return False + + network = current_app.config["CONFIG_TUGRAZ_IP_NETWORK"] + + try: + return ip_address(user_ip) in ip_network(network) + except ValueError: + return False class TUGrazAuthenticatedUser(Generator): """Generates the `tugraz_authenticated_user` role-need.""" - def needs(self, **__): + def needs(self, **__: dict) -> list[Need]: """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 index 08af03d..e41f5b1 100644 --- a/invenio_config_tugraz/permissions/policies.py +++ b/invenio_config_tugraz/permissions/policies.py @@ -52,7 +52,7 @@ from invenio_records_permissions.generators import ( from invenio_records_permissions.policies.records import RecordPermissionPolicy from invenio_users_resources.services.permissions import UserManager -from .generators import TUGrazAuthenticatedUser +from .generators import AllowedFromIPNetwork, RecordSingleIP, TUGrazAuthenticatedUser class TUGrazRDMRecordPermissionPolicy(RecordPermissionPolicy): @@ -87,11 +87,18 @@ class TUGrazRDMRecordPermissionPolicy(RecordPermissionPolicy): SubmissionReviewer(), CommunityInclusionReviewers(), RecordCommunitiesAction("view"), + AllowedFromIPNetwork(), + RecordSingleIP(), ] can_tugraz_authenticated = [TUGrazAuthenticatedUser(), SystemProcess()] can_authenticated = can_tugraz_authenticated - can_all = [AnyUser(), SystemProcess()] + can_all = [ + AnyUser(), + SystemProcess(), + AllowedFromIPNetwork(), + RecordSingleIP(), + ] # # Miscellaneous diff --git a/tests/test_policies.py b/tests/test_policies.py index 786919e..5978c85 100644 --- a/tests/test_policies.py +++ b/tests/test_policies.py @@ -15,6 +15,9 @@ from invenio_config_tugraz.permissions.policies import TUGrazRDMRecordPermission ALLOWED_DIFFERENCES = { "can_authenticated", "can_create", + "can_search", + "can_view", + "can_all", "can_search_drafts", "can_tugraz_authenticated", }