From 6df4aa78ee4818e757ae1d1a06fa5347771794b3 Mon Sep 17 00:00:00 2001 From: Ronald Krist Date: Tue, 26 Sep 2023 12:54:31 +0200 Subject: [PATCH] initial commit --- .gitignore | 91 +++++++++++++++++++++++ LICENSE | 21 ++++++ README.md | 1 + format.sh | 3 + oarepo_communities/__init__.py | 0 oarepo_communities/cf/__init__.py | 0 oarepo_communities/cf/loader.py | 8 ++ oarepo_communities/cf/permissions.py | 18 +++++ oarepo_communities/cf/roles.py | 18 +++++ oarepo_communities/components/__init__.py | 0 oarepo_communities/components/actions.py | 35 +++++++++ oarepo_communities/ext.py | 20 +++++ oarepo_communities/generators/__init__.py | 0 oarepo_communities/generators/record.py | 48 ++++++++++++ oarepo_communities/permissions.py | 9 +++ oarepo_communities/proxies.py | 4 + oarepo_communities/utils/__init__.py | 0 oarepo_communities/utils/utils.py | 23 ++++++ pyproject.toml | 3 + setup.cfg | 26 +++++++ setup.py | 3 + 21 files changed, 331 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 format.sh create mode 100644 oarepo_communities/__init__.py create mode 100644 oarepo_communities/cf/__init__.py create mode 100644 oarepo_communities/cf/loader.py create mode 100644 oarepo_communities/cf/permissions.py create mode 100644 oarepo_communities/cf/roles.py create mode 100644 oarepo_communities/components/__init__.py create mode 100644 oarepo_communities/components/actions.py create mode 100644 oarepo_communities/ext.py create mode 100644 oarepo_communities/generators/__init__.py create mode 100644 oarepo_communities/generators/record.py create mode 100644 oarepo_communities/permissions.py create mode 100644 oarepo_communities/proxies.py create mode 100644 oarepo_communities/utils/__init__.py create mode 100644 oarepo_communities/utils/utils.py create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad71df3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,91 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# Idea software family +.idea/ + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +venv/ +.venv/ +build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Vim swapfiles +.*.sw? + +tests/test.db + +.venv +.direnv + +docs/migration/data + +.env +.envrc +/.python-version +/poetry.lock +example/data +.DS_Store + +test-model + +# Testing +sample/ + +tests/test-sample-app +tests/test-sample-site + +example_document/ +dist/ + +.model_venv/ +.vscode diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1e43fbc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2021 CESNET. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd51903 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# OARepo requests \ No newline at end of file diff --git a/format.sh b/format.sh new file mode 100755 index 0000000..e09be13 --- /dev/null +++ b/format.sh @@ -0,0 +1,3 @@ +black oarepo_requests tests --target-version py310 +autoflake --in-place --remove-all-unused-imports --recursive oarepo_requests tests +isort oarepo_requests tests --profile black diff --git a/oarepo_communities/__init__.py b/oarepo_communities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oarepo_communities/cf/__init__.py b/oarepo_communities/cf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oarepo_communities/cf/loader.py b/oarepo_communities/cf/loader.py new file mode 100644 index 0000000..0643b7b --- /dev/null +++ b/oarepo_communities/cf/loader.py @@ -0,0 +1,8 @@ +from invenio_communities.communities.records.api import Community as InvenioCommunityRecord +from flask import current_app + +def get_field(record_class): + if str(record_class).find("invenio_communities.communities.records.api.Community") > 0: + custom_field = getattr(record_class, "custom_fields") + return None, current_app.config[custom_field.config_key] + return None \ No newline at end of file diff --git a/oarepo_communities/cf/permissions.py b/oarepo_communities/cf/permissions.py new file mode 100644 index 0000000..6e14d89 --- /dev/null +++ b/oarepo_communities/cf/permissions.py @@ -0,0 +1,18 @@ +from invenio_records_resources.services.custom_fields import BaseCF +from marshmallow import fields as ma_fields + +class PermissionsCF(BaseCF): + """""" + @property + def mapping(self): + return {"type": "object", "dynamic": True} + + @property + def field(self): + return ma_fields.Dict(keys=ma_fields.String, values=ma_fields.Dict(keys=ma_fields.String, values=ma_fields.Boolean)) + # example { + # "owner": {"can_create": true , "can_read": true, "can_update": true , "can_delete":true }, + # "manager": {"can_create": true , "can_read": true, "can_update": true , "can_delete":true }, + # "curator": {"can_create": true , "can_read": true, "can_update": true , "can_delete":false}, + # "reader": {"can_create": false, "can_read": true, "can_update": false, "can_delete":false}, + # } diff --git a/oarepo_communities/cf/roles.py b/oarepo_communities/cf/roles.py new file mode 100644 index 0000000..2e1fcf8 --- /dev/null +++ b/oarepo_communities/cf/roles.py @@ -0,0 +1,18 @@ +from invenio_records_resources.services.custom_fields import BaseCF +from marshmallow import fields as ma_fields + +class CommunityRolesToGroupsCF(BaseCF): + """""" + @property + def mapping(self): + return {"type": "object", "dynamic": True} + + @property + def field(self): + return ma_fields.Dict(keys=ma_fields.String, values=ma_fields.Dict(keys=ma_fields.String, values=ma_fields.String)) + # example { + # "owner": {"can_create": true , "can_read": true, "can_update": true , "can_delete":true }, + # "manager": {"can_create": true , "can_read": true, "can_update": true , "can_delete":true }, + # "curator": {"can_create": true , "can_read": true, "can_update": true , "can_delete":false}, + # "reader": {"can_create": false, "can_read": true, "can_update": false, "can_delete":false}, + # } \ No newline at end of file diff --git a/oarepo_communities/components/__init__.py b/oarepo_communities/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oarepo_communities/components/actions.py b/oarepo_communities/components/actions.py new file mode 100644 index 0000000..25fdd0a --- /dev/null +++ b/oarepo_communities/components/actions.py @@ -0,0 +1,35 @@ +from collections import defaultdict + +from invenio_records_resources.services.records.components import ServiceComponent +from invenio_communities.proxies import current_communities + +from oarepo_communities.proxies import current_communities_permissions + + +class AllowedActionsComponent(ServiceComponent): + def _get_available_actions(self, identity, record, dict_to_save_result, **kwargs): + record_communities = set(record["parent"]["communities"]["ids"]) + + usercommunities2roles = defaultdict(list) + communities_permissions = {} + for need in identity.provides: + if need.method == "community" and need.value in record_communities: + usercommunities2roles[need.value].append(need.role) + communities_permissions[need.value] = \ + current_communities_permissions(need.value) + + + allowed_actions_for_record_and_user = set() + for user_community, user_roles_community in usercommunities2roles.items(): + if user_community in record_communities: + for user_role_community in user_roles_community: + permissions = communities_permissions[user_community][user_role_community] + allowed_actions_for_record_and_user |= {permission for permission, allowed in permissions.items() if + allowed} + + dict_to_save_result = kwargs[dict_to_save_result] + dict_to_save_result["allowed_actions"] = allowed_actions_for_record_and_user + + + def before_ui_detail(self, identity, data=None, record=None, errors=None, **kwargs): + self._get_available_actions(identity, record, "extra_context", **kwargs) \ No newline at end of file diff --git a/oarepo_communities/ext.py b/oarepo_communities/ext.py new file mode 100644 index 0000000..0a2a9a5 --- /dev/null +++ b/oarepo_communities/ext.py @@ -0,0 +1,20 @@ +from .permissions import permissions_cache + + +class OArepoCommunities(object): + """Invenio extension.""" + + def __init__(self, app=None): + """Extension initialization.""" + if app: + self.init_app(app) + + def init_app(self, app): + """Flask application initialization.""" + self.init_config(app) + app.extensions["oarepo-communities"] = self + + + def init_config(self, app): + self.permissions_cache = permissions_cache + diff --git a/oarepo_communities/generators/__init__.py b/oarepo_communities/generators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oarepo_communities/generators/record.py b/oarepo_communities/generators/record.py new file mode 100644 index 0000000..62a566e --- /dev/null +++ b/oarepo_communities/generators/record.py @@ -0,0 +1,48 @@ +from collections import defaultdict + +from cachetools import TTLCache, cached +from invenio_records_permissions.generators import ( + Generator, +) +from invenio_communities.generators import CommunityRoleNeed +from ..proxies import current_communities_permissions + + +class RecordCommunitiesGenerator(Generator): + """Allows system_process role.""" + + def __init__(self, action): + self.action = action + + def needs(self, **kwargs): + _needs = set() + if "record" in kwargs and hasattr(kwargs["record"], "parent"): + record = kwargs["record"] + try: + community_ids = record.parent["communities"]["ids"] + except KeyError: + return [] + by_actions = record_community_permissions(frozenset(community_ids)) + if self.action in by_actions: + community2role_list = by_actions[self.action] + for c, roles in community2role_list.items(): + for role in roles: + _needs.add(CommunityRoleNeed(c, role)) + return _needs + return [] + + +@cached(cache=TTLCache(maxsize=1028, ttl=360)) +def record_community_permissions(record_communities): + communities_permissions = {} + + for record_community_id in record_communities: + communities_permissions[record_community_id] = current_communities_permissions(record_community_id) + + by_actions = defaultdict(lambda: defaultdict(list)) + for c, role_permissions_dct in communities_permissions.items(): + for role, role_permissions in role_permissions_dct.items(): + for action, val in role_permissions.items(): + if val: + by_actions[action][c].append(role) + return by_actions diff --git a/oarepo_communities/permissions.py b/oarepo_communities/permissions.py new file mode 100644 index 0000000..78f3931 --- /dev/null +++ b/oarepo_communities/permissions.py @@ -0,0 +1,9 @@ +from functools import lru_cache + +from invenio_access.permissions import system_identity +from invenio_communities import current_communities +import cachetools + +@cachetools.cached(cache=cachetools.TTLCache(maxsize=1028, ttl=360)) +def permissions_cache(community_id): + return current_communities.service.read(system_identity, community_id).data["custom_fields"]["permissions"] \ No newline at end of file diff --git a/oarepo_communities/proxies.py b/oarepo_communities/proxies.py new file mode 100644 index 0000000..add251e --- /dev/null +++ b/oarepo_communities/proxies.py @@ -0,0 +1,4 @@ +from flask import current_app +from werkzeug.local import LocalProxy + +current_communities_permissions = LocalProxy(lambda: current_app.extensions["oarepo-communities"].permissions_cache) \ No newline at end of file diff --git a/oarepo_communities/utils/__init__.py b/oarepo_communities/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oarepo_communities/utils/utils.py b/oarepo_communities/utils/utils.py new file mode 100644 index 0000000..9c09d7c --- /dev/null +++ b/oarepo_communities/utils/utils.py @@ -0,0 +1,23 @@ +""" +def get_allowed_actions(record, identity=None): + record_communities = set(record["parent"]["communities"]["ids"]) + + user_community_roles = defaultdict(list) + communities_permissions = {} + + for record_community_id in record_communities: + if need.method == "community" and need.value in record_communities: + for need in identity.provides: + + user_community_roles[need.value].append(need.role) + communities_permissions[need.value] = \ + current_communities_permissions(need.value) + + allowed_actions_for_record_and_user = set() + for user_community, user_roles_community in user_community_roles.items(): + if user_community in record_communities: + for user_role_community in user_roles_community: + permissions = communities_permissions[user_community][user_role_community] + allowed_actions_for_record_and_user |= {permission for permission, allowed in permissions.items() if + allowed} + """ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8289f07 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build_system] +requires = ["setuptools", "wheel", "babel>2.8"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..44c0e13 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,26 @@ +[metadata] +name = oarepo-communities +version = 1.0.0 +description = +authors = Ronald Krist +readme = README.md +long_description = file:README.md +long_description_content_type = text/markdown + + +[options] +python = >=3.9 +install_requires = + oarepo>=11,<12 +#packages = find: + +[options.package_data] +* = *.json, *.rst, *.md, *.json5, *.jinja2 + +[options.entry_points] +oarepo.custom_fields = record_field = oarepo_communities.cf.loader:get_field +invenio_base.apps = + oarepo_vocabularies = oarepo_communities.ext:OArepoCommunities +invenio_base.api_apps = + oarepo_vocabularies = oarepo_communities.ext:OArepoCommunities + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup()