perm: implement single-ip and ip-network

* with that addition it is possible to restrict records to an special ip
  or an ip network
This commit is contained in:
Christoph Ladurner
2024-07-16 14:25:07 +02:00
parent 52fb93cc43
commit 760363b4a5
7 changed files with 162 additions and 46 deletions

View File

@@ -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",
)

View File

@@ -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",

View File

@@ -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")

View File

@@ -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."""

View File

@@ -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 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:
# if record has singleip, and the ip of the user matches the allowed ip
if self.check_permission():
return [any_user]
else:
# non of the above - return empty
return []
def excludes(self, **kwargs):
"""Preventing Needs, Set of Needs denying permission.
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.
"""
try:
if (
kwargs["record"]["custom_fields"]["single_ip"]
and not self.check_permission()
):
return [any_user]
except KeyError:
return []
else:
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:
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
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
# Checks if the user IP is among single IPs
if user_ip in current_app.config["INVENIO_CONFIG_TUGRAZ_SINGLE_IP"]:
return True
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
<https://invenio-access.readthedocs.io/en/latest/api.html
#invenio_access.permissions.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]

View File

@@ -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

View File

@@ -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",
}