mirror of
https://github.com/Cian-H/invenio-config-iform.git
synced 2025-12-23 05:21:57 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e4fcca0ed | ||
|
|
c934a4952b | ||
|
|
c91d056a56 | ||
|
|
583a67d0cf | ||
|
|
760363b4a5 | ||
|
|
52fb93cc43 | ||
|
|
41db3186df | ||
|
|
20bdff0b79 | ||
|
|
99705d7a25 |
33
.tx/config
33
.tx/config
@@ -1,33 +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.
|
|
||||||
|
|
||||||
# TODO: Transifex integration
|
|
||||||
#
|
|
||||||
# 1) Create message catalog:
|
|
||||||
# $ python setup.py extract_messages
|
|
||||||
# $ python setup.py init_catalog -l <lang>
|
|
||||||
# $ python setup.py compile_catalog
|
|
||||||
# 2) Ensure project has been created on Transifex under the inveniosoftware
|
|
||||||
# organisation.
|
|
||||||
# 3) Install the transifex-client
|
|
||||||
# $ pip install transifex-client
|
|
||||||
# 4) Push source (.pot) and translations (.po) to Transifex
|
|
||||||
# $ tx push -s -t
|
|
||||||
# 5) Pull translations for a single language from Transifex
|
|
||||||
# $ tx pull -l <lang>
|
|
||||||
# 6) Pull translations for all languages from Transifex
|
|
||||||
# $ tx pull -a
|
|
||||||
|
|
||||||
[main]
|
|
||||||
host = https://www.transifex.com
|
|
||||||
|
|
||||||
[invenio.invenio-config-tugraz-messages]
|
|
||||||
file_filter = invenio_config_tugraz/translations/<lang>/LC_MESSAGES/messages.po
|
|
||||||
source_file = invenio_config_tugraz/translations/messages.pot
|
|
||||||
source_lang = en
|
|
||||||
type = PO
|
|
||||||
15
CHANGES.rst
15
CHANGES.rst
@@ -8,6 +8,21 @@
|
|||||||
Changes
|
Changes
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
Version v0.12.3 (release 2024-07-25)
|
||||||
|
|
||||||
|
- fix(tugraz_authenticated): missmatch of role name
|
||||||
|
|
||||||
|
|
||||||
|
Version v0.12.2 (release 2024-07-19)
|
||||||
|
|
||||||
|
- setup: introduce ruff
|
||||||
|
- perm: implement single-ip and ip-network
|
||||||
|
- utils: add invenio_saml-compatible account-setup
|
||||||
|
- add new permission-policy, add new role
|
||||||
|
- fix deprecated `before_app_first_request`
|
||||||
|
- setup: add support for python3.11 and 3.12
|
||||||
|
|
||||||
|
|
||||||
Version v0.12.1 (release 2024-03-08)
|
Version v0.12.1 (release 2024-03-08)
|
||||||
|
|
||||||
- setup: remove upper limit of rdm-records
|
- setup: remove upper limit of rdm-records
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
# 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
|
# modify it under the terms of the MIT License; see LICENSE file for more
|
||||||
@@ -9,14 +9,12 @@
|
|||||||
"""invenio module that adds tugraz configs."""
|
"""invenio module that adds tugraz configs."""
|
||||||
|
|
||||||
from .ext import InvenioConfigTugraz
|
from .ext import InvenioConfigTugraz
|
||||||
from .generators import RecordIp
|
|
||||||
from .utils import get_identity_from_user_by_email
|
from .utils import get_identity_from_user_by_email
|
||||||
|
|
||||||
__version__ = "0.12.1"
|
__version__ = "0.12.3"
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"__version__",
|
"__version__",
|
||||||
"InvenioConfigTugraz",
|
"InvenioConfigTugraz",
|
||||||
"RecordIp",
|
|
||||||
"get_identity_from_user_by_email",
|
"get_identity_from_user_by_email",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()]
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
# 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
|
# modify it under the terms of the MIT License; see LICENSE file for more
|
||||||
@@ -10,23 +10,26 @@
|
|||||||
|
|
||||||
from invenio_i18n import gettext as _
|
from invenio_i18n import gettext as _
|
||||||
|
|
||||||
INVENIO_CONFIG_TUGRAZ_SHIBBOLETH = False
|
CONFIG_TUGRAZ_SHIBBOLETH = False
|
||||||
"""Set True if SAML is configured"""
|
"""Set True if SAML is configured"""
|
||||||
|
|
||||||
INVENIO_CONFIG_TUGRAZ_SINGLE_IP = []
|
CONFIG_TUGRAZ_SINGLE_IPS = []
|
||||||
"""Allows access to users whose IP address is listed.
|
"""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"]
|
["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.
|
"""Allows access to users whose range of IP address is listed.
|
||||||
|
|
||||||
INVENIO_CONFIG_TUGRAZ_IP_RANGES =
|
INVENIO_CONFIG_TUGRAZ_IP_RANGES =
|
||||||
[["127.0.0.2", "127.0.0.99"], ["127.0.1.3", "127.0.1.5"]]
|
[["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 = {
|
CONFIG_TUGRAZ_ROUTES = {
|
||||||
"guide": "/guide",
|
"guide": "/guide",
|
||||||
|
|||||||
15
invenio_config_tugraz/custom_fields/__init__.py
Normal file
15
invenio_config_tugraz/custom_fields/__init__.py
Normal 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")
|
||||||
@@ -1,30 +1,61 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
# 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
|
# modify it under the terms of the MIT License; see LICENSE file for more
|
||||||
# details.
|
# details.
|
||||||
|
|
||||||
"""invenio module that adds tugraz configs."""
|
"""invenio module that adds tugraz configs."""
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
from . import config
|
from . import config
|
||||||
|
from .custom_fields import ip_network, single_ip
|
||||||
|
|
||||||
|
|
||||||
class InvenioConfigTugraz(object):
|
class InvenioConfigTugraz:
|
||||||
"""invenio-config-tugraz extension."""
|
"""invenio-config-tugraz extension."""
|
||||||
|
|
||||||
def __init__(self, app=None):
|
def __init__(self, app: Flask = None) -> None:
|
||||||
"""Extension initialization."""
|
"""Extension initialization."""
|
||||||
if app:
|
if app:
|
||||||
self.init_app(app)
|
self.init_app(app)
|
||||||
|
|
||||||
def init_app(self, app):
|
def init_app(self, app: Flask) -> None:
|
||||||
"""Flask application initialization."""
|
"""Flask application initialization."""
|
||||||
self.init_config(app)
|
self.init_config(app)
|
||||||
|
self.add_custom_fields(app)
|
||||||
app.extensions["invenio-config-tugraz"] = self
|
app.extensions["invenio-config-tugraz"] = self
|
||||||
|
|
||||||
def init_config(self, app):
|
def init_config(self, app: Flask) -> None:
|
||||||
"""Initialize configuration."""
|
"""Initialize configuration."""
|
||||||
for k in dir(config):
|
for k in dir(config):
|
||||||
if k.startswith("INVENIO_CONFIG_TUGRAZ_"):
|
if k.startswith("INVENIO_CONFIG_TUGRAZ_"):
|
||||||
app.config.setdefault(k, getattr(config, k))
|
app.config.setdefault(k, getattr(config, k))
|
||||||
|
|
||||||
|
def add_custom_fields(self, app: Flask) -> None:
|
||||||
|
"""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: Flask) -> None:
|
||||||
|
"""Finalize app."""
|
||||||
|
rank_blueprint_higher(app)
|
||||||
|
|
||||||
|
|
||||||
|
def rank_blueprint_higher(app: Flask) -> None:
|
||||||
|
"""Rank this module's blueprint higher than blueprint of security module.
|
||||||
|
|
||||||
|
Needed in order to overwrite email templates.
|
||||||
|
|
||||||
|
Since the blueprints are in a dict and the order of insertion is
|
||||||
|
retained, popping and reinserting all items (except ours), ensures
|
||||||
|
our blueprint will be in front.
|
||||||
|
"""
|
||||||
|
bps = app.blueprints
|
||||||
|
for blueprint_name in list(bps.keys()):
|
||||||
|
if blueprint_name != "invenio_config_tugraz":
|
||||||
|
bps.update({blueprint_name: bps.pop(blueprint_name)})
|
||||||
|
|||||||
@@ -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 <https://invenio-access.readthedocs.io>`_ and
|
|
||||||
`flask-principal <https://pythonhosted.org/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 <https://invenio-access.readthedocs.io/en/latest/api.html#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
|
|
||||||
<https://pythonhosted.org/Flask-Principal/#flask_principal.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
|
|
||||||
<https://invenio-access.readthedocs.io/en/latest/api.html
|
|
||||||
#invenio_access.permissions.Permission>`_ in
|
|
||||||
`invenio-access <https://invenio-access.readthedocs.io>`_ .
|
|
||||||
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_<action>`` 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
|
|
||||||
<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.
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
13
invenio_config_tugraz/permissions/__init__.py
Normal file
13
invenio_config_tugraz/permissions/__init__.py
Normal file
@@ -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",)
|
||||||
209
invenio_config_tugraz/permissions/generators.py
Normal file
209
invenio_config_tugraz/permissions/generators.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# -*- 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 <https://pythonhosted.org/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 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
|
||||||
|
|
||||||
|
from .roles import tugraz_authenticated_user
|
||||||
|
|
||||||
|
|
||||||
|
class RecordSingleIP(Generator):
|
||||||
|
"""Allowed any user with accessing with the IP."""
|
||||||
|
|
||||||
|
def needs(self, record: dict | None = None, **__: dict) -> list[Need]:
|
||||||
|
"""Set of Needs granting permission. Enabling Needs."""
|
||||||
|
if record is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 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, 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.
|
||||||
|
|
||||||
|
.. 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"]["single_ip"]
|
||||||
|
and not self.check_permission()
|
||||||
|
):
|
||||||
|
return [any_user]
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
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", **{"custom_fields.single_ip": True})
|
||||||
|
|
||||||
|
# Lists all records
|
||||||
|
return dsl.Q("match_all")
|
||||||
|
|
||||||
|
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
|
||||||
|
<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, **__: dict) -> list[Need]:
|
||||||
|
"""Generate needs to be checked against current user identity."""
|
||||||
|
return [tugraz_authenticated_user]
|
||||||
253
invenio_config_tugraz/permissions/policies.py
Normal file
253
invenio_config_tugraz/permissions/policies.py
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# -*- 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
|
||||||
|
<https://invenio-access.readthedocs.io/en/latest/api.html
|
||||||
|
#invenio_access.permissions.Permission>`_ in
|
||||||
|
`invenio-access <https://invenio-access.readthedocs.io>`_ .
|
||||||
|
Generators are used to provide the "by whom" part and the implied category of
|
||||||
|
object.
|
||||||
|
|
||||||
|
Actions are class variables of the form: ``can_<action>`` 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 AllowedFromIPNetwork, RecordSingleIP, 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"),
|
||||||
|
AllowedFromIPNetwork(),
|
||||||
|
RecordSingleIP(),
|
||||||
|
]
|
||||||
|
|
||||||
|
can_tugraz_authenticated = [TUGrazAuthenticatedUser(), SystemProcess()]
|
||||||
|
can_authenticated = can_tugraz_authenticated
|
||||||
|
can_all = [
|
||||||
|
AnyUser(),
|
||||||
|
SystemProcess(),
|
||||||
|
AllowedFromIPNetwork(),
|
||||||
|
RecordSingleIP(),
|
||||||
|
]
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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()]
|
||||||
21
invenio_config_tugraz/permissions/roles.py
Normal file
21
invenio_config_tugraz/permissions/roles.py
Normal file
@@ -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 --description "..."`
|
||||||
|
then add roles to users via:
|
||||||
|
`$ invenio roles add user@email.com tugraz_authenticated`
|
||||||
|
"""
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -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."""
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Copyright (C) 2022 Graz University of Technology.
|
# Copyright (C) 2022-2024 Graz University of Technology.
|
||||||
#
|
#
|
||||||
# invenio-config-tugraz is free software; you can redistribute it and/or
|
# 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
|
# modify it under the terms of the MIT License; see LICENSE file for more
|
||||||
@@ -8,21 +8,27 @@
|
|||||||
|
|
||||||
"""Utils file."""
|
"""Utils file."""
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
from flask_principal import Identity
|
from flask_principal import Identity
|
||||||
from invenio_access import any_user
|
from invenio_access import any_user
|
||||||
from invenio_access.utils import get_identity
|
from invenio_access.utils import get_identity
|
||||||
from invenio_accounts import current_accounts
|
from invenio_accounts import current_accounts
|
||||||
|
|
||||||
|
|
||||||
def get_identity_from_user_by_email(email: str = None) -> Identity:
|
def get_identity_from_user_by_email(email: str | None = None) -> Identity:
|
||||||
"""Get the user specified via email or ID."""
|
"""Get the user specified via email or ID."""
|
||||||
|
warnings.warn("deprecated", DeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
if email is None:
|
if email is None:
|
||||||
raise ValueError("the email has to be set to get a identity")
|
msg = "the email has to be set to get a identity"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
user = current_accounts.datastore.get_user(email)
|
user = current_accounts.datastore.get_user(email)
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
raise LookupError(f"user with {email} not found")
|
msg = f"user with {email} not found"
|
||||||
|
raise LookupError(msg)
|
||||||
|
|
||||||
identity = get_identity(user)
|
identity = get_identity(user)
|
||||||
|
|
||||||
@@ -30,3 +36,38 @@ def get_identity_from_user_by_email(email: str = None) -> Identity:
|
|||||||
identity.provides.add(any_user)
|
identity.provides.add(any_user)
|
||||||
|
|
||||||
return identity
|
return identity
|
||||||
|
|
||||||
|
|
||||||
|
def tugraz_account_setup_extension(user, account_info) -> None: # noqa: ANN001, ARG001
|
||||||
|
"""Add tugraz_authenticated role to user after SAML-login was acknowledged.
|
||||||
|
|
||||||
|
To use, have `acs_handler_factory` call invenio_saml's `default_account_setup` first,
|
||||||
|
then this function second.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# invenio.cfg
|
||||||
|
from invenio_saml.handlers import default_account_setup, acs_handler_factory
|
||||||
|
|
||||||
|
def tugraz_account_setup(user, account_info):
|
||||||
|
# links external `account_info` with our database's `user` for future logins
|
||||||
|
default_account_setup(user, account_info)
|
||||||
|
tugraz_account_setup_extension(user, account_info)
|
||||||
|
|
||||||
|
SSO_SAML_IDPS = {
|
||||||
|
"my-tugraz-idp": {
|
||||||
|
...
|
||||||
|
"acs_handler": acs_handler_factory(
|
||||||
|
"my-tugraz-idp", account_setup=tugraz_account_setup
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
For this to work, the role tugraz_authenticated must have been created
|
||||||
|
(e.g. via `invenio roles create tugraz_authenticated`).
|
||||||
|
"""
|
||||||
|
user_email = account_info["user"]["email"]
|
||||||
|
|
||||||
|
# NOTE: `datastore.commit`ing will be done by acs_handler that calls this func
|
||||||
|
# NOTE: this is a No-Op when user_email already has role tugraz_authenticated
|
||||||
|
current_accounts.datastore.add_role_to_user(user_email, "tugraz_authenticated")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
# 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
|
# modify it under the terms of the MIT License; see LICENSE file for more
|
||||||
@@ -8,11 +8,12 @@
|
|||||||
|
|
||||||
"""invenio module for TUGRAZ config."""
|
"""invenio module for TUGRAZ config."""
|
||||||
|
|
||||||
from flask import Blueprint, current_app, redirect, url_for
|
from flask import Blueprint, Flask, redirect, url_for
|
||||||
from invenio_i18n import get_locale
|
from invenio_i18n import get_locale
|
||||||
|
from werkzeug.wrappers import Response as BaseResponse
|
||||||
|
|
||||||
|
|
||||||
def ui_blueprint(app):
|
def ui_blueprint(app: Flask) -> Blueprint:
|
||||||
"""Blueprint for the routes and resources provided by invenio-config-tugraz."""
|
"""Blueprint for the routes and resources provided by invenio-config-tugraz."""
|
||||||
routes = app.config.get("CONFIG_TUGRAZ_ROUTES")
|
routes = app.config.get("CONFIG_TUGRAZ_ROUTES")
|
||||||
|
|
||||||
@@ -27,25 +28,10 @@ def ui_blueprint(app):
|
|||||||
blueprint.add_url_rule(routes["terms"], view_func=terms)
|
blueprint.add_url_rule(routes["terms"], view_func=terms)
|
||||||
blueprint.add_url_rule(routes["gdpr"], view_func=gdpr)
|
blueprint.add_url_rule(routes["gdpr"], view_func=gdpr)
|
||||||
|
|
||||||
@blueprint.before_app_first_request
|
|
||||||
def rank_higher():
|
|
||||||
"""Rank this modules blueprint higher than blueprint of security module.
|
|
||||||
|
|
||||||
Needed in order to overwrite email templates.
|
|
||||||
|
|
||||||
Since the blueprints are in a dict and the order of insertion is
|
|
||||||
retained, popping and reinserting all items (except ours), ensures
|
|
||||||
our blueprint will be in front.
|
|
||||||
"""
|
|
||||||
bps = current_app.blueprints
|
|
||||||
for blueprint_name in list(bps.keys()):
|
|
||||||
if blueprint_name != "invenio_config_tugraz":
|
|
||||||
bps.update({blueprint_name: bps.pop(blueprint_name)})
|
|
||||||
|
|
||||||
return blueprint
|
return blueprint
|
||||||
|
|
||||||
|
|
||||||
def guide():
|
def guide() -> BaseResponse:
|
||||||
"""TUGraz_Repository_Guide."""
|
"""TUGraz_Repository_Guide."""
|
||||||
locale = get_locale()
|
locale = get_locale()
|
||||||
return redirect(
|
return redirect(
|
||||||
@@ -53,11 +39,11 @@ def guide():
|
|||||||
"static",
|
"static",
|
||||||
filename=f"documents/TUGraz_Repository_Guide_02.1_{locale}.pdf",
|
filename=f"documents/TUGraz_Repository_Guide_02.1_{locale}.pdf",
|
||||||
_external=True,
|
_external=True,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def terms():
|
def terms() -> BaseResponse:
|
||||||
"""Terms_And_Conditions."""
|
"""Terms_And_Conditions."""
|
||||||
locale = get_locale()
|
locale = get_locale()
|
||||||
return redirect(
|
return redirect(
|
||||||
@@ -65,11 +51,11 @@ def terms():
|
|||||||
"static",
|
"static",
|
||||||
filename=f"documents/TUGraz_Repository_Terms_And_Conditions_{locale}.pdf",
|
filename=f"documents/TUGraz_Repository_Terms_And_Conditions_{locale}.pdf",
|
||||||
_external=True,
|
_external=True,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def gdpr():
|
def gdpr() -> BaseResponse:
|
||||||
"""General_Data_Protection_Rights."""
|
"""General_Data_Protection_Rights."""
|
||||||
locale = get_locale()
|
locale = get_locale()
|
||||||
return redirect(
|
return redirect(
|
||||||
@@ -77,5 +63,5 @@ def gdpr():
|
|||||||
"static",
|
"static",
|
||||||
filename=f"documents/TUGraz_Repository_General_Data_Protection_Rights_{locale}.pdf",
|
filename=f"documents/TUGraz_Repository_General_Data_Protection_Rights_{locale}.pdf",
|
||||||
_external=True,
|
_external=True,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools", "wheel", "babel>2.8"]
|
requires = ["setuptools", "wheel", "babel>2.8"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
exclude = ["docs"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["ALL"]
|
||||||
|
ignore = [
|
||||||
|
"ANN101", "ANN102",
|
||||||
|
"D203", "D211", "D212", "D213",
|
||||||
|
"E501",
|
||||||
|
"ERA001",
|
||||||
|
"FA102",
|
||||||
|
"FIX002",
|
||||||
|
"INP001",
|
||||||
|
"RUF005", "RUF012",
|
||||||
|
"S101",
|
||||||
|
"TD002", "TD003",
|
||||||
|
"UP009",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,13 +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.
|
|
||||||
#
|
|
||||||
# TODO: Add development versions of some important dependencies here to get a
|
|
||||||
# warning when there are breaking upstream changes, e.g.:
|
|
||||||
#
|
|
||||||
# -e git+git://github.com/mitsuhiko/werkzeug.git#egg=Werkzeug
|
|
||||||
# -e git+git://github.com/mitsuhiko/jinja2.git#egg=Jinja2
|
|
||||||
13
run-tests.sh
13
run-tests.sh
@@ -3,7 +3,7 @@
|
|||||||
#
|
#
|
||||||
# Copyright (C) 2019-2020 CERN.
|
# Copyright (C) 2019-2020 CERN.
|
||||||
# Copyright (C) 2019-2020 Northwestern University.
|
# Copyright (C) 2019-2020 Northwestern University.
|
||||||
# Copyright (C) 2020 Graz University of Technology.
|
# Copyright (C) 2020-2024 Graz University of Technology.
|
||||||
#
|
#
|
||||||
# invenio-config-tugraz is free software; you can redistribute it and/or
|
# 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
|
# modify it under the terms of the MIT License; see LICENSE file for more
|
||||||
@@ -16,17 +16,8 @@ set -o errexit
|
|||||||
# Quit on unbound symbols
|
# Quit on unbound symbols
|
||||||
set -o nounset
|
set -o nounset
|
||||||
|
|
||||||
# Always bring down docker services
|
ruff check .
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
eval "$(docker-services-cli down --env)"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
python -m check_manifest
|
python -m check_manifest
|
||||||
python -m sphinx.cmd.build -qnN docs docs/_build/html
|
python -m sphinx.cmd.build -qnN docs docs/_build/html
|
||||||
eval "$(docker-services-cli up --search ${SEARCH:-elasticsearch} --env)"
|
|
||||||
python -m pytest
|
python -m pytest
|
||||||
tests_exit_code=$?
|
|
||||||
python -m sphinx.cmd.build -qnN -b doctest docs docs/_build/doctest
|
|
||||||
exit "$tests_exit_code"
|
|
||||||
|
|||||||
28
setup.cfg
28
setup.cfg
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
# 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
|
# modify it under the terms of the MIT License; see LICENSE file for more
|
||||||
@@ -25,26 +25,26 @@ classifiers =
|
|||||||
Programming Language :: Python
|
Programming Language :: Python
|
||||||
Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
Topic :: Software Development :: Libraries :: Python Modules
|
Topic :: Software Development :: Libraries :: Python Modules
|
||||||
Programming Language :: Python :: 3.9
|
Programming Language :: Python :: 3.12
|
||||||
Programming Language :: Python :: 3.10
|
|
||||||
Development Status :: 3 - Alpha
|
Development Status :: 3 - Alpha
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
include_package_data = True
|
include_package_data = True
|
||||||
packages = find:
|
packages = find:
|
||||||
python_requires = >=3.9
|
python_requires = >=3.12
|
||||||
zip_safe = False
|
zip_safe = False
|
||||||
install_requires =
|
install_requires =
|
||||||
# keep this dependencies identical to invenio-app-rdm
|
invenio-cache>=1.1.1
|
||||||
invenio-cache>=1.1.1,<2.0.0
|
invenio-i18n>=2.0.0
|
||||||
invenio-i18n>=2.0.0,<3.0.0
|
|
||||||
invenio-rdm-records>=4.0.0
|
invenio-rdm-records>=4.0.0
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
tests =
|
tests =
|
||||||
pytest-black>=0.3.0
|
invenio-app>=1.5.0
|
||||||
pytest-invenio>=2.1.0,<3.0.0
|
|
||||||
invenio-search[opensearch2]>=2.1.0,<3.0.0
|
invenio-search[opensearch2]>=2.1.0,<3.0.0
|
||||||
|
pytest-black-ng>=0.4.0
|
||||||
|
pytest-invenio>=2.1.0,<3.0.0
|
||||||
|
ruff>=0.5.3
|
||||||
Sphinx>=4.5.0
|
Sphinx>=4.5.0
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
@@ -56,6 +56,8 @@ invenio_i18n.translations =
|
|||||||
messages = invenio_config_tugraz
|
messages = invenio_config_tugraz
|
||||||
invenio_config.module =
|
invenio_config.module =
|
||||||
invenio_config_tugraz = invenio_config_tugraz.config
|
invenio_config_tugraz = invenio_config_tugraz.config
|
||||||
|
invenio_base.finalize_app =
|
||||||
|
invenio_config_tugraz = invenio_config_tugraz.ext:finalize_app
|
||||||
|
|
||||||
[aliases]
|
[aliases]
|
||||||
test = pytest
|
test = pytest
|
||||||
@@ -68,9 +70,6 @@ all_files = 1
|
|||||||
[bdist_wheel]
|
[bdist_wheel]
|
||||||
universal = 1
|
universal = 1
|
||||||
|
|
||||||
[pydocstyle]
|
|
||||||
add_ignore = D401
|
|
||||||
|
|
||||||
[compile_catalog]
|
[compile_catalog]
|
||||||
directory = invenio_config_tugraz/translations/
|
directory = invenio_config_tugraz/translations/
|
||||||
|
|
||||||
@@ -93,10 +92,9 @@ output-dir = invenio_config_tugraz/translations/
|
|||||||
profile=black
|
profile=black
|
||||||
|
|
||||||
[check-manifest]
|
[check-manifest]
|
||||||
ignore =
|
ignore = *-requirements.txt
|
||||||
*-requirements.txt
|
|
||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
addopts = --black --isort --pydocstyle --doctest-glob="*.rst" --doctest-modules --cov=invenio_config_tugraz --cov-report=term-missing tests invenio_config_tugraz
|
addopts = --black --cov=invenio_config_tugraz --cov-report=term-missing
|
||||||
testpaths = tests invenio_config_tugraz
|
testpaths = tests invenio_config_tugraz
|
||||||
live_server_scope = module
|
live_server_scope = module
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Copyright (C) 2020 Mojib Wali.
|
# Copyright (C) 2020 Mojib Wali.
|
||||||
|
# Copyright (C) 2020-2024 Graz University of Technology.
|
||||||
#
|
#
|
||||||
# invenio-config-tugraz is free software; you can redistribute it and/or
|
# 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
|
# modify it under the terms of the MIT License; see LICENSE file for more
|
||||||
@@ -12,152 +13,21 @@ See https://pytest-invenio.readthedocs.io/ for documentation on which test
|
|||||||
fixtures are available.
|
fixtures are available.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from invenio_db import InvenioDB, db
|
|
||||||
from invenio_i18n import InvenioI18N
|
|
||||||
from sqlalchemy_utils.functions import create_database, database_exists, drop_database
|
|
||||||
|
|
||||||
from invenio_config_tugraz import InvenioConfigTugraz
|
from invenio_config_tugraz import InvenioConfigTugraz
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def celery_config():
|
def create_app(instance_path: str) -> Flask:
|
||||||
"""Override pytest-invenio fixture.
|
"""Application factory fixture."""
|
||||||
|
|
||||||
TODO: Remove this fixture if you add Celery support.
|
def factory(**config: str) -> Flask:
|
||||||
"""
|
app = Flask("testapp", instance_path=instance_path)
|
||||||
return {}
|
app.config.update(**config)
|
||||||
|
InvenioConfigTugraz(app)
|
||||||
|
return app
|
||||||
|
|
||||||
|
return factory
|
||||||
@pytest.fixture()
|
|
||||||
def create_app(request):
|
|
||||||
"""Basic Flask application."""
|
|
||||||
instance_path = tempfile.mkdtemp()
|
|
||||||
app = Flask("testapp")
|
|
||||||
DB = os.getenv("SQLALCHEMY_DATABASE_URI", "sqlite://")
|
|
||||||
app.config.update(
|
|
||||||
INVENIO_CONFIG_TUGRAZ_SINGLE_IP=["127.0.0.1", "127.0.0.2"],
|
|
||||||
INVENIO_CONFIG_TUGRAZ_IP_RANGES=[
|
|
||||||
["127.0.0.2", "127.0.0.99"],
|
|
||||||
["127.0.1.3", "127.0.1.5"],
|
|
||||||
],
|
|
||||||
SQLALCHEMY_DATABASE_URI=DB,
|
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS=False,
|
|
||||||
)
|
|
||||||
InvenioI18N(app)
|
|
||||||
InvenioConfigTugraz(app)
|
|
||||||
InvenioDB(app)
|
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
db_url = str(db.engine.url)
|
|
||||||
if db_url != "sqlite://" and not database_exists(db_url):
|
|
||||||
create_database(db_url)
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
def teardown():
|
|
||||||
with app.app_context():
|
|
||||||
db_url = str(db.engine.url)
|
|
||||||
db.session.close()
|
|
||||||
if db_url != "sqlite://":
|
|
||||||
drop_database(db_url)
|
|
||||||
shutil.rmtree(instance_path)
|
|
||||||
|
|
||||||
request.addfinalizer(teardown)
|
|
||||||
app.test_request_context().push()
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def open_record():
|
|
||||||
"""Open record data as dict coming from the external world."""
|
|
||||||
return {
|
|
||||||
"access": {
|
|
||||||
"metadata": False,
|
|
||||||
"files": False,
|
|
||||||
"owned_by": [1],
|
|
||||||
"access_right": "open",
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"publication_date": "2020-06-01",
|
|
||||||
"resource_type": {"type": "image", "subtype": "image-photo"},
|
|
||||||
# Technically not required
|
|
||||||
"creators": [
|
|
||||||
{"name": "Troy Brown", "type": "personal"},
|
|
||||||
{
|
|
||||||
"name": "Phillip Lester",
|
|
||||||
"type": "personal",
|
|
||||||
"identifiers": {"orcid": "0000-0002-1825-0097"},
|
|
||||||
"affiliations": [
|
|
||||||
{"name": "Carter-Morris", "identifiers": {"ror": "03yrm5c26"}}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Steven Williamson",
|
|
||||||
"type": "personal",
|
|
||||||
"identifiers": {"orcid": "0000-0002-1825-0097"},
|
|
||||||
"affiliations": [
|
|
||||||
{
|
|
||||||
"name": "Ritter and Sons",
|
|
||||||
"identifiers": {"ror": "03yrm5c26"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Montgomery, Bush and Madden",
|
|
||||||
"identifiers": {"ror": "03yrm5c26"},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"title": "A Romans story",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def singleip_record():
|
|
||||||
"""Single Ip record data as dict coming from the external world."""
|
|
||||||
return {
|
|
||||||
"access": {
|
|
||||||
"metadata": False,
|
|
||||||
"files": False,
|
|
||||||
"owned_by": [1],
|
|
||||||
"access_right": "singleip",
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"publication_date": "2020-06-01",
|
|
||||||
"resource_type": {"type": "image", "subtype": "image-photo"},
|
|
||||||
# Technically not required
|
|
||||||
"creators": [
|
|
||||||
{"name": "Troy Brown", "type": "personal"},
|
|
||||||
{
|
|
||||||
"name": "Phillip Lester",
|
|
||||||
"type": "personal",
|
|
||||||
"identifiers": {"orcid": "0000-0002-1825-0097"},
|
|
||||||
"affiliations": [
|
|
||||||
{"name": "Carter-Morris", "identifiers": {"ror": "03yrm5c26"}}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Steven Williamson",
|
|
||||||
"type": "personal",
|
|
||||||
"identifiers": {"orcid": "0000-0002-1825-0097"},
|
|
||||||
"affiliations": [
|
|
||||||
{
|
|
||||||
"name": "Ritter and Sons",
|
|
||||||
"identifiers": {"ror": "03yrm5c26"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Montgomery, Bush and Madden",
|
|
||||||
"identifiers": {"ror": "03yrm5c26"},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"title": "A Romans story",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,31 +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.
|
|
||||||
|
|
||||||
"""Test Generators."""
|
|
||||||
|
|
||||||
from invenio_access.permissions import any_user
|
|
||||||
|
|
||||||
from invenio_config_tugraz.generators import RecordIp
|
|
||||||
|
|
||||||
|
|
||||||
def test_recordip(create_app, open_record, singleip_record):
|
|
||||||
"""Test Generator RecordIp."""
|
|
||||||
generator = RecordIp()
|
|
||||||
open_record = open_record
|
|
||||||
singleiprec = singleip_record
|
|
||||||
|
|
||||||
assert generator.needs(record=None) == []
|
|
||||||
assert generator.needs(record=open_record) == [any_user]
|
|
||||||
assert generator.needs(record=singleiprec) == []
|
|
||||||
|
|
||||||
assert generator.excludes(record=open_record) == []
|
|
||||||
assert generator.excludes(record=open_record) == []
|
|
||||||
|
|
||||||
assert generator.query_filter().to_dict() == {
|
|
||||||
"bool": {"must_not": [{"match": {"access.access_right": "singleip"}}]}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
# 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
|
# modify it under the terms of the MIT License; see LICENSE file for more
|
||||||
@@ -13,14 +13,14 @@ from flask import Flask
|
|||||||
from invenio_config_tugraz import InvenioConfigTugraz
|
from invenio_config_tugraz import InvenioConfigTugraz
|
||||||
|
|
||||||
|
|
||||||
def test_version():
|
def test_version() -> None:
|
||||||
"""Test version import."""
|
"""Test version import."""
|
||||||
from invenio_config_tugraz import __version__
|
from invenio_config_tugraz import __version__
|
||||||
|
|
||||||
assert __version__
|
assert __version__
|
||||||
|
|
||||||
|
|
||||||
def test_init():
|
def test_init() -> None:
|
||||||
"""Test extension initialization."""
|
"""Test extension initialization."""
|
||||||
app = Flask("testapp")
|
app = Flask("testapp")
|
||||||
ext = InvenioConfigTugraz(app)
|
ext = InvenioConfigTugraz(app)
|
||||||
|
|||||||
88
tests/test_policies.py
Normal file
88
tests/test_policies.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# -*- 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",
|
||||||
|
"can_view",
|
||||||
|
"can_all",
|
||||||
|
"can_search_drafts",
|
||||||
|
"can_tugraz_authenticated",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_policies_synced() -> None:
|
||||||
|
"""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_<action>`s`
|
||||||
|
if extras := set(tugraz_cans) - set(rdm_cans) - ALLOWED_DIFFERENCES:
|
||||||
|
msg = f"""
|
||||||
|
TU Graz's permission-policy has additional fields over invenio-rdm's:{extras}
|
||||||
|
if this is intentional, add to ALLOWED_DIFFERENCES in test-file
|
||||||
|
otherwise remove extraneous fields from TUGrazRDMRecordPermissionPolicy
|
||||||
|
"""
|
||||||
|
raise KeyError(msg)
|
||||||
|
|
||||||
|
if missing := set(rdm_cans) - set(tugraz_cans):
|
||||||
|
msg = f"""
|
||||||
|
invenio-rdm's permission-policy has fields unhandled by TU Graz's: {missing}
|
||||||
|
if this is intentional, add to ALLOWED_DIFFERENCES
|
||||||
|
otherwise set the corresponding fields in TUGrazRDMRecordPermissionPolicy
|
||||||
|
"""
|
||||||
|
raise KeyError(msg)
|
||||||
|
|
||||||
|
# check whether same permission-generators used for same `can_<action>`
|
||||||
|
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}:
|
||||||
|
msg = f"""
|
||||||
|
permission-policy for `{can_name}` differs between TU-Graz and invenio-rdm
|
||||||
|
if this is intentional, add to ALLOWED_DIFFERENCES in test-file
|
||||||
|
otherwise fix TUGrazRDMRecordPermissionPolicy
|
||||||
|
"""
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
msg = f"""
|
||||||
|
invenio-rdm's NEED_LABEL_TO_ACTION differs from TU Graz's in {label}
|
||||||
|
if this is intentional, add to ALLOWED_DIFFERENCES in test-file
|
||||||
|
otherwise fix TUGrazRDMRecordPermissionPolicy.NEED_LABEL_TO_ACTION
|
||||||
|
"""
|
||||||
|
raise ValueError(msg)
|
||||||
Reference in New Issue
Block a user