From c8d153bb6d6d56c973353364f75068d8ccf9ee89 Mon Sep 17 00:00:00 2001 From: Ronald Krist Date: Tue, 24 Oct 2023 14:15:23 +0200 Subject: [PATCH] community records pt2 --- format.sh | 6 +- oarepo_communities/cache.py | 16 +++ oarepo_communities/cf/loader.py | 11 +- oarepo_communities/cf/permissions.py | 17 ++- oarepo_communities/cf/user_map.py | 4 +- oarepo_communities/components/actions.py | 22 ++-- oarepo_communities/ext.py | 30 +++++ oarepo_communities/ext_config.py | 5 + oarepo_communities/permissions/__init__.py | 0 oarepo_communities/permissions/presets.py | 42 +++++++ oarepo_communities/permissions/record.py | 65 +++++++++++ oarepo_communities/proxies.py | 6 + .../resources/community_records/config.py | 9 +- .../resources/community_records/resource.py | 11 +- .../resources/record_communities/config.py | 7 +- .../resources/record_communities/resource.py | 22 +--- .../services/community_records/config.py | 36 +++--- .../services/community_records/schema.py | 8 +- .../services/community_records/service.py | 20 +++- .../services/record_communities/config.py | 12 +- .../services/record_communities/errors.py | 6 +- .../services/record_communities/schema.py | 6 +- .../services/record_communities/service.py | 70 +++++------ oarepo_communities/utils/utils.py | 4 - setup.cfg | 5 + tests/test_permissions/conftest.py | 109 ++++++++++-------- .../test_permissions/test_permissions_api.py | 109 ++++++++++++------ tests/thesis.yaml | 2 + 28 files changed, 450 insertions(+), 210 deletions(-) create mode 100644 oarepo_communities/cache.py create mode 100644 oarepo_communities/ext.py create mode 100644 oarepo_communities/ext_config.py create mode 100644 oarepo_communities/permissions/__init__.py create mode 100644 oarepo_communities/permissions/presets.py create mode 100644 oarepo_communities/permissions/record.py create mode 100644 oarepo_communities/proxies.py diff --git a/format.sh b/format.sh index e09be13..ddbbaf4 100755 --- a/format.sh +++ b/format.sh @@ -1,3 +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 +black oarepo_communities tests --target-version py310 +autoflake --in-place --remove-all-unused-imports --recursive oarepo_communities tests +isort oarepo_communities tests --profile black diff --git a/oarepo_communities/cache.py b/oarepo_communities/cache.py new file mode 100644 index 0000000..22ca4ed --- /dev/null +++ b/oarepo_communities/cache.py @@ -0,0 +1,16 @@ +from cachetools import TTLCache, cached +from invenio_access.permissions import system_identity +from invenio_communities import current_communities + + +@cached(cache=TTLCache(maxsize=1028, ttl=600)) +def permissions_cache(community_id): + return current_communities.service.read(system_identity, community_id).data[ + "custom_fields" + ]["permissions"] + + +def usermap(community_id): + return current_communities.service.read(system_identity, community_id).data[ + "custom_fields" + ]["usermap"] diff --git a/oarepo_communities/cf/loader.py b/oarepo_communities/cf/loader.py index 61c84c4..c0acad3 100644 --- a/oarepo_communities/cf/loader.py +++ b/oarepo_communities/cf/loader.py @@ -1,6 +1,11 @@ from flask import current_app + def get_field(record_class): - if str(record_class).find("invenio_communities.communities.records.api.Community") > 0: - return "custom_fields", current_app.config["COMMUNITIES_CUSTOM_FIELDS"] - return None \ No newline at end of file + if ( + str(record_class).find("invenio_communities.communities.records.api.Community") + > 0 + and "COMMUNITIES_CUSTOM_FIELDS" in current_app.config + ): + return current_app.config["COMMUNITIES_CUSTOM_FIELDS"] + return None diff --git a/oarepo_communities/cf/permissions.py b/oarepo_communities/cf/permissions.py index 6e14d89..7bb7657 100644 --- a/oarepo_communities/cf/permissions.py +++ b/oarepo_communities/cf/permissions.py @@ -1,15 +1,28 @@ 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} + return { + "custom_fields": { + "properties": {"permissions": {"type": "object", "dynamic": True}} + } + } + + @property + def mapping_settings(self): + return {} @property def field(self): - return ma_fields.Dict(keys=ma_fields.String, values=ma_fields.Dict(keys=ma_fields.String, values=ma_fields.Boolean)) + 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 }, diff --git a/oarepo_communities/cf/user_map.py b/oarepo_communities/cf/user_map.py index 604e158..ababe1b 100644 --- a/oarepo_communities/cf/user_map.py +++ b/oarepo_communities/cf/user_map.py @@ -1,8 +1,10 @@ from invenio_records_resources.services.custom_fields import BaseCF from marshmallow import fields as ma_fields + class UserMapCF(BaseCF): """""" + @property def mapping(self): return {"type": "object", "dynamic": True} @@ -15,4 +17,4 @@ def field(self): # "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/actions.py b/oarepo_communities/components/actions.py index b899c2f..a653ca5 100644 --- a/oarepo_communities/components/actions.py +++ b/oarepo_communities/components/actions.py @@ -1,7 +1,7 @@ from collections import defaultdict from invenio_records_resources.services.records.components import ServiceComponent -from oarepo_runtime.communities.proxies import current_communities_permissions +from ..proxies import current_communities_permissions class AllowedActionsComponent(ServiceComponent): @@ -13,21 +13,25 @@ def _get_available_actions(self, identity, record, dict_to_save_result, **kwargs 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) - + 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} + 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 + self._get_available_actions(identity, record, "extra_context", **kwargs) diff --git a/oarepo_communities/ext.py b/oarepo_communities/ext.py new file mode 100644 index 0000000..5444ec7 --- /dev/null +++ b/oarepo_communities/ext.py @@ -0,0 +1,30 @@ +from .cache import permissions_cache + + +class OARepoCommunities(object): + """OARepo extension of Invenio-Vocabularies.""" + + 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): + """Initialize configuration.""" + from . import ext_config + + if "OAREPO_PERMISSIONS_PRESETS" not in app.config: + app.config["OAREPO_PERMISSIONS_PRESETS"] = {} + + for k in ext_config.OAREPO_PERMISSIONS_PRESETS: + if k not in app.config["OAREPO_PERMISSIONS_PRESETS"]: + app.config["OAREPO_PERMISSIONS_PRESETS"][ + k + ] = ext_config.OAREPO_PERMISSIONS_PRESETS[k] + + self.permissions_cache = permissions_cache diff --git a/oarepo_communities/ext_config.py b/oarepo_communities/ext_config.py new file mode 100644 index 0000000..9c30263 --- /dev/null +++ b/oarepo_communities/ext_config.py @@ -0,0 +1,5 @@ +from oarepo_communities.permissions.presets import CommunityPermissionPolicy + +OAREPO_PERMISSIONS_PRESETS = { + "community": CommunityPermissionPolicy, +} diff --git a/oarepo_communities/permissions/__init__.py b/oarepo_communities/permissions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oarepo_communities/permissions/presets.py b/oarepo_communities/permissions/presets.py new file mode 100644 index 0000000..a05c32f --- /dev/null +++ b/oarepo_communities/permissions/presets.py @@ -0,0 +1,42 @@ +from invenio_records_permissions import RecordPermissionPolicy +from invenio_records_permissions.generators import ( + AnyUser, + AuthenticatedUser, + SystemProcess, +) + +from .record import RecordCommunitiesGenerator + + +class CommunityPermissionPolicy(RecordPermissionPolicy): + can_search = [SystemProcess(), AnyUser()] + can_read = [SystemProcess(), RecordCommunitiesGenerator("can_read")] + can_create = [SystemProcess(), AuthenticatedUser()] + can_update = [SystemProcess(), RecordCommunitiesGenerator("can_update")] + can_delete = [SystemProcess(), RecordCommunitiesGenerator("can_delete")] + can_manage = [SystemProcess()] + + can_create_files = [SystemProcess()] + can_set_content_files = [SystemProcess()] + can_get_content_files = [AnyUser(), SystemProcess()] + can_commit_files = [SystemProcess()] + can_read_files = [AnyUser(), SystemProcess()] + can_update_files = [SystemProcess()] + can_delete_files = [SystemProcess()] + + can_edit = [SystemProcess()] + can_new_version = [SystemProcess()] + can_search_drafts = [SystemProcess()] + can_read_draft = [SystemProcess()] + can_update_draft = [SystemProcess()] + can_delete_draft = [SystemProcess()] + can_publish = [SystemProcess(), RecordCommunitiesGenerator("can_publish")] + can_draft_create_files = [SystemProcess()] + can_draft_set_content_files = [SystemProcess()] + can_draft_get_content_files = [SystemProcess()] + can_draft_commit_files = [SystemProcess()] + can_draft_read_files = [SystemProcess()] + can_draft_update_files = [SystemProcess()] + + can_add_community = [SystemProcess(), AuthenticatedUser()] + can_remove_community = [SystemProcess(), AuthenticatedUser()] diff --git a/oarepo_communities/permissions/record.py b/oarepo_communities/permissions/record.py new file mode 100644 index 0000000..9df3007 --- /dev/null +++ b/oarepo_communities/permissions/record.py @@ -0,0 +1,65 @@ +from collections import defaultdict + +from cachetools import TTLCache, cached +from invenio_communities.generators import CommunityRoleNeed +from invenio_records_permissions.generators import Generator + +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 community_id, roles in community2role_list.items(): + for role in roles: + _needs.add(CommunityRoleNeed(community_id, role)) + return _needs + return [] + + """ + def communities(self, identity): + + roles = self.roles() + community_ids = set() + for n in identity.provides: + if n.method == "community" and n.role in roles: + community_ids.add(n.value) + return list(community_ids) + + def query_filter(self, identity=None, **kwargs): + + return dsl.Q("terms", **{"parent.communities.ids": self.communities(identity)}) + + """ + + +@cached(cache=TTLCache(maxsize=1028, ttl=600)) +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 community_id, 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][community_id].append(role) + return by_actions diff --git a/oarepo_communities/proxies.py b/oarepo_communities/proxies.py new file mode 100644 index 0000000..9a3cf84 --- /dev/null +++ b/oarepo_communities/proxies.py @@ -0,0 +1,6 @@ +from flask import current_app +from werkzeug.local import LocalProxy + +current_communities_permissions = LocalProxy( + lambda: current_app.extensions["oarepo-communities"].permissions_cache +) diff --git a/oarepo_communities/resources/community_records/config.py b/oarepo_communities/resources/community_records/config.py index c190d4d..2305cf1 100644 --- a/oarepo_communities/resources/community_records/config.py +++ b/oarepo_communities/resources/community_records/config.py @@ -1,11 +1,10 @@ from invenio_drafts_resources.resources import RecordResourceConfig - -from invenio_records_resources.services.base.config import ConfiguratorMixin, FromConfig +from invenio_records_resources.services.base.config import ConfiguratorMixin class CommunityRecordsResourceConfig(RecordResourceConfig, ConfiguratorMixin): """Community's records resource config.""" - #blueprint_name = "community-records" - #url_prefix = "/communities" - routes = {"list": "//records"} \ No newline at end of file + # blueprint_name = "community-records" + # url_prefix = "/communities" + routes = {"list": "//records"} diff --git a/oarepo_communities/resources/community_records/resource.py b/oarepo_communities/resources/community_records/resource.py index e75fc6c..c9641c9 100644 --- a/oarepo_communities/resources/community_records/resource.py +++ b/oarepo_communities/resources/community_records/resource.py @@ -1,11 +1,5 @@ - from flask import g - -from flask_resources import ( - resource_requestctx, - response_handler, - route, -) +from flask_resources import resource_requestctx, response_handler, route from invenio_drafts_resources.resources import RecordResource from invenio_records_resources.resources.records.resource import ( request_data, @@ -14,6 +8,7 @@ ) from invenio_records_resources.resources.records.utils import search_preference + class CommunityRecordsResource(RecordResource): """RDM community's records resource.""" @@ -61,4 +56,4 @@ def delete(self): response = {} if errors: response["errors"] = errors - return response, 200 \ No newline at end of file + return response, 200 diff --git a/oarepo_communities/resources/record_communities/config.py b/oarepo_communities/resources/record_communities/config.py index cf6be0c..6277764 100644 --- a/oarepo_communities/resources/record_communities/config.py +++ b/oarepo_communities/resources/record_communities/config.py @@ -1,15 +1,14 @@ import marshmallow as ma from invenio_communities.communities.resources import CommunityResourceConfig from invenio_communities.communities.resources.config import community_error_handlers - from invenio_records_resources.services.base.config import ConfiguratorMixin class RecordCommunitiesResourceConfig(CommunityResourceConfig, ConfiguratorMixin): """Record communities resource config.""" - #blueprint_name = "records-community" - #url_prefix = "/nr-documents/" + # blueprint_name = "records-community" + # url_prefix = "/nr-documents/" routes = { "list": "//communities", "suggestions": "//communities-suggestions", @@ -21,4 +20,4 @@ class RecordCommunitiesResourceConfig(CommunityResourceConfig, ConfiguratorMixin "membership": ma.fields.Boolean(), } - error_handlers = community_error_handlers \ No newline at end of file + error_handlers = community_error_handlers diff --git a/oarepo_communities/resources/record_communities/resource.py b/oarepo_communities/resources/record_communities/resource.py index 45553f8..4c038e1 100644 --- a/oarepo_communities/resources/record_communities/resource.py +++ b/oarepo_communities/resources/record_communities/resource.py @@ -1,24 +1,8 @@ - -from flask import abort, current_app, g, send_file -from flask_cors import cross_origin -from flask_resources import ( - HTTPJSONException, - Resource, - ResponseHandler, - from_conf, - request_parser, - resource_requestctx, - response_handler, - route, - with_content_negotiation, -) - +from flask import g +from flask_resources import Resource, resource_requestctx, response_handler, route from invenio_records_resources.resources.errors import ErrorHandlersMixin from invenio_records_resources.resources.records.resource import ( request_data, - request_extra_args, - request_headers, - request_read_args, request_search_args, request_view_args, ) @@ -113,4 +97,4 @@ def remove(self): if errors: response["errors"] = errors - return response, 200 if len(processed) > 0 else 400 \ No newline at end of file + return response, 200 if len(processed) > 0 else 400 diff --git a/oarepo_communities/services/community_records/config.py b/oarepo_communities/services/community_records/config.py index c4d2fd6..12e8725 100644 --- a/oarepo_communities/services/community_records/config.py +++ b/oarepo_communities/services/community_records/config.py @@ -1,24 +1,22 @@ from invenio_communities.communities.records.api import Community - -from invenio_records_resources.services.base.config import ( - ConfiguratorMixin, -) +from invenio_records_resources.services.base.config import ConfiguratorMixin from invenio_records_resources.services.records.config import RecordServiceConfig - -from invenio_records_resources.services.records.links import ( - pagination_links, -) +from invenio_records_resources.services.records.links import pagination_links +from oarepo_runtime.config.service import PermissionsPresetsConfigMixin from .schema import CommunityRecordsSchema -#from .schemas import RDMParentSchema, RDMRecordSchema -#from .schemas.community_records import CommunityRecordsSchema +# from .schemas import RDMParentSchema, RDMRecordSchema +# from .schemas.community_records import CommunityRecordsSchema -from oarepo_runtime.config.service import PermissionsPresetsConfigMixin -class CommunityRecordsServiceConfig(PermissionsPresetsConfigMixin, RecordServiceConfig, ConfiguratorMixin): + +class CommunityRecordsServiceConfig( + PermissionsPresetsConfigMixin, RecordServiceConfig, ConfiguratorMixin +): """Community records service config.""" + # define at builder level: record_cls # schema = RDMRecordSchema # record service @@ -26,23 +24,23 @@ class CommunityRecordsServiceConfig(PermissionsPresetsConfigMixin, RecordService service_id = "community-records" community_cls = Community - #permission_policy_cls = FromConfig( + # permission_policy_cls = FromConfig( # "RDM_PERMISSION_POLICY", default=RDMRecordPermissionPolicy, import_string=True - #) #how to use these things? + # ) #how to use these things? # Search configuration - #search = FromConfigSearchOptions( + # search = FromConfigSearchOptions( # "RDM_SEARCH", # "RDM_SORT_OPTIONS", # "RDM_FACETS", # search_option_cls=RDMSearchOptions, - #) - #search_versions = FromConfigSearchOptions( + # ) + # search_versions = FromConfigSearchOptions( # "RDM_SEARCH_VERSIONING", # "RDM_SORT_OPTIONS", # "RDM_FACETS", # search_option_cls=RDMSearchVersionsOptions, - #) + # ) # Service schemas community_record_schema = CommunityRecordsSchema @@ -52,4 +50,4 @@ class CommunityRecordsServiceConfig(PermissionsPresetsConfigMixin, RecordService links_search_community_records = pagination_links( "{+api}/communities/{id}/records{?args*}" - ) \ No newline at end of file + ) diff --git a/oarepo_communities/services/community_records/schema.py b/oarepo_communities/services/community_records/schema.py index 866ec5e..1d00b19 100644 --- a/oarepo_communities/services/community_records/schema.py +++ b/oarepo_communities/services/community_records/schema.py @@ -1,4 +1,6 @@ from marshmallow import Schema, ValidationError, fields, validate, validates + + class RecordSchema(Schema): """Schema to define a community id.""" @@ -18,8 +20,8 @@ def validate_records(self, value): max_number = self.context["max_number"] if max_number < len(value): raise ValidationError( - "Too many records passed, {max_number} max allowed.".format( - max_number=max_number + "Too many records passed, {max_number} max allowed.".format( + max_number=max_number ) ) @@ -35,4 +37,4 @@ def validate_records(self, value): if duplicated: raise ValidationError( "Duplicated records {rec_ids}.".format(rec_ids=duplicated) - ) \ No newline at end of file + ) diff --git a/oarepo_communities/services/community_records/service.py b/oarepo_communities/services/community_records/service.py index fb42f5f..bb39b4b 100644 --- a/oarepo_communities/services/community_records/service.py +++ b/oarepo_communities/services/community_records/service.py @@ -17,7 +17,7 @@ from invenio_records_resources.services.uow import unit_of_work from invenio_search.engine import dsl -#from invenio_rdm_records.proxies import current_record_communities_service +# from invenio_rdm_records.proxies import current_record_communities_service class CommunityRecordsService(RecordService): @@ -25,6 +25,7 @@ class CommunityRecordsService(RecordService): The record communities service is in charge of managing the records of a given community. """ + def __init__(self, config, record_communities_service=None): super().__init__(config) self.record_communities_service = record_communities_service @@ -46,7 +47,7 @@ def search( params=None, search_preference=None, extra_filter=None, - **kwargs + **kwargs, ): """Search for records published in the given community.""" self.require_permission(identity, "search") @@ -62,7 +63,7 @@ def search( ) if extra_filter is not None: community_filter = community_filter & extra_filter - + # todo drafts? search = self._search( "search", identity, @@ -74,6 +75,19 @@ def search( ) search_result = search.execute() + """ + return self.result_list( + self, + identity, + search_result, + params, + links_tpl=LinksTemplate(self.config.links_search, context={"args": params}), + links_item_tpl=self.links_item_tpl, + expandable_fields=self.expandable_fields, + expand=expand, + ) + """ + return self.result_list( self, identity, diff --git a/oarepo_communities/services/record_communities/config.py b/oarepo_communities/services/record_communities/config.py index a7ff81c..f37f3ac 100644 --- a/oarepo_communities/services/record_communities/config.py +++ b/oarepo_communities/services/record_communities/config.py @@ -1,15 +1,16 @@ - from invenio_indexer.api import RecordIndexer from invenio_records_resources.services.base.config import ( ConfiguratorMixin, ServiceConfig, ) +from oarepo_runtime.config.service import PermissionsPresetsConfigMixin +from .schema import RecordCommunitiesSchema -from .schema import RecordCommunitiesSchema -from oarepo_runtime.config.service import PermissionsPresetsConfigMixin -class RecordCommunitiesServiceConfig(PermissionsPresetsConfigMixin, ServiceConfig, ConfiguratorMixin): +class RecordCommunitiesServiceConfig( + PermissionsPresetsConfigMixin, ServiceConfig, ConfiguratorMixin +): """Record communities service config.""" service_id = "" @@ -19,7 +20,8 @@ class RecordCommunitiesServiceConfig(PermissionsPresetsConfigMixin, ServiceConfi indexer_queue_name = service_id index_dumper = None + # todo do we need this? # Max n. communities that can be added at once max_number_of_additions = 10 # Max n. communities that can be removed at once - max_number_of_removals = 10 \ No newline at end of file + max_number_of_removals = 10 diff --git a/oarepo_communities/services/record_communities/errors.py b/oarepo_communities/services/record_communities/errors.py index 4cb5e29..5f299ac 100644 --- a/oarepo_communities/services/record_communities/errors.py +++ b/oarepo_communities/services/record_communities/errors.py @@ -3,6 +3,7 @@ class CommunityAlreadyExists(Exception): description = "The record is already included in this community." + class RecordCommunityMissing(Exception): """Record does not belong to the community.""" @@ -15,6 +16,5 @@ def __init__(self, record_id, community_id): def description(self): """Exception description.""" return "The record {record_id} in not included in the community {community_id}.".format( - record_id=self.record_id, community_id=self.community_id - ) - + record_id=self.record_id, community_id=self.community_id + ) diff --git a/oarepo_communities/services/record_communities/schema.py b/oarepo_communities/services/record_communities/schema.py index c8755ba..4b1cd1a 100644 --- a/oarepo_communities/services/record_communities/schema.py +++ b/oarepo_communities/services/record_communities/schema.py @@ -32,9 +32,9 @@ def validate_communities(self, value): max_number = self.context["max_number"] if max_number < len(value): raise ValidationError( - "Too many communities passed, {max_number} max allowed.".format( - max_number=max_number - ) + "Too many communities passed, {max_number} max allowed.".format( + max_number=max_number + ) ) # check unique ids diff --git a/oarepo_communities/services/record_communities/service.py b/oarepo_communities/services/record_communities/service.py index b825ff4..9ea6415 100644 --- a/oarepo_communities/services/record_communities/service.py +++ b/oarepo_communities/services/record_communities/service.py @@ -1,12 +1,10 @@ from invenio_communities.proxies import current_communities -#from invenio_i18n import lazy_gettext as _ -#from invenio_notifications.services.uow import NotificationOp -from invenio_pidstore.errors import PIDDoesNotExistError -from invenio_records_resources.services import ( - RecordIndexerMixin, - ServiceSchemaWrapper, -) from invenio_drafts_resources.services import RecordService + +# from invenio_i18n import lazy_gettext as _ +# from invenio_notifications.services.uow import NotificationOp +from invenio_pidstore.errors import PIDDoesNotExistError +from invenio_records_resources.services import RecordIndexerMixin, ServiceSchemaWrapper from invenio_records_resources.services.errors import PermissionDeniedError from invenio_records_resources.services.uow import ( IndexRefreshOp, @@ -14,12 +12,13 @@ RecordIndexOp, unit_of_work, ) -from invenio_requests import current_request_type_registry, current_requests_service -from invenio_requests.resolvers.registry import ResolverRegistry from invenio_search.engine import dsl from sqlalchemy.orm.exc import NoResultFound -from oarepo_communities.services.record_communities.errors import CommunityAlreadyExists, RecordCommunityMissing +from oarepo_communities.services.record_communities.errors import ( + CommunityAlreadyExists, + RecordCommunityMissing, +) class RecordCommunitiesService(RecordService, RecordIndexerMixin): @@ -28,6 +27,10 @@ class RecordCommunitiesService(RecordService, RecordIndexerMixin): The communities service is in charge of managing communities of a given record. """ + def __init__(self, config, record_service=None): + super().__init__(config) + self.record_service = record_service + @property def schema(self): """Returns the data schema instance.""" @@ -37,6 +40,7 @@ def schema(self): def record_cls(self): """Factory for creating a record class.""" return self.config.record_cls + """ def _exists(self, identity, community_id, record): @@ -56,7 +60,6 @@ def _exists(self, identity, community_id, record): """ def include_directly(self, record, community, uow): - # integrity check, it should never happen on a published record # assert not record.parent.review @@ -64,7 +67,7 @@ def include_directly(self, record, community, uow): default = not record.parent.communities record.parent.communities.add(community, default=default) uow.register(RecordCommitOp(record.parent)) - uow.register(RecordIndexOp(record, indexer=self.config.record_service.indexer)) + uow.register(RecordIndexOp(record, indexer=self.record_service.indexer)) def _include(self, identity, community_id, comment, require_review, record, uow): """Create request to add the community to the record.""" @@ -77,39 +80,39 @@ def _include(self, identity, community_id, comment, require_review, record, uow) raise CommunityAlreadyExists() # check if there is already an open request, to avoid duplications - #existing_request_id = self._exists(identity, com_id, record) - #if existing_request_id: + # existing_request_id = self._exists(identity, com_id, record) + # if existing_request_id: # raise OpenRequestAlreadyExists(existing_request_id) - #type_ = current_request_type_registry.lookup(CommunityInclusion.type_id) - #receiver = ResolverRegistry.resolve_entity_proxy( + # type_ = current_request_type_registry.lookup(CommunityInclusion.type_id) + # receiver = ResolverRegistry.resolve_entity_proxy( # {"community": com_id} - #).resolve() + # ).resolve() - #request_item = current_requests_service.create( + # request_item = current_requests_service.create( # identity, # {}, # type_, # receiver, # topic=record, # uow=uow, - #) + # ) # create review request - #request_item = current_rdm_records.community_inclusion_service.submit( + # request_item = current_rdm_records.community_inclusion_service.submit( # identity, record, community, request_item._request, comment, uow - #) + # ) # include directly when allowed - #if not require_review: + # if not require_review: # request_item = current_rdm_records.community_inclusion_service.include( # identity, community, request_item._request, uow # ) - #result = current_rdm_records.community_inclusion_service.include( + # result = current_rdm_records.community_inclusion_service.include( # identity, community, request_item._request, uow - #) + # ) - #return result + # return result self.include_directly(record, community, uow) return self.result_item() @@ -152,23 +155,23 @@ def _add(self, identity, id_, data, is_draft, uow): request_item = self._include( identity, community_id, comment, require_review, record, uow ) - #result["request_id"] = str(request_item.data["id"]) - #result["request"] = request_item.to_dict() + # result["request_id"] = str(request_item.data["id"]) + # result["request"] = request_item.to_dict() processed.append(result) - #uow.register( + # uow.register( # NotificationOp( # CommunityInclusionSubmittedNotificationBuilder.build( # request_item._request # ) # ) - #) + # ) except (NoResultFound, PIDDoesNotExistError): result["message"] = "Community not found." errors.append(result) except ( CommunityAlreadyExists, - #OpenRequestAlreadyExists, - #InvalidAccessRestrictions, + # OpenRequestAlreadyExists, + # InvalidAccessRestrictions, PermissionDeniedError, ) as ex: result["message"] = ex.description @@ -235,7 +238,7 @@ def search( search_preference=None, expand=False, extra_filter=None, - **kwargs + **kwargs, ): """Search for record's communities.""" record = self.record_cls.pid.resolve(id_) @@ -252,8 +255,9 @@ def search( search_preference=search_preference, expand=expand, extra_filter=communities_filter, - **kwargs + **kwargs, ) + """ @staticmethod def _get_excluded_communities_filter(record, identity, id_): diff --git a/oarepo_communities/utils/utils.py b/oarepo_communities/utils/utils.py index ab96ca3..6041347 100644 --- a/oarepo_communities/utils/utils.py +++ b/oarepo_communities/utils/utils.py @@ -1,5 +1,3 @@ - - """ def get_allowed_actions(record, identity=None): record_communities = set(record["parent"]["communities"]["ids"]) @@ -23,5 +21,3 @@ def get_allowed_actions(record, identity=None): allowed_actions_for_record_and_user |= {permission for permission, allowed in permissions.items() if allowed} """ - - diff --git a/setup.cfg b/setup.cfg index e1bf365..baa5e60 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,6 +12,7 @@ long_description_content_type = text/markdown python = >=3.9 install_requires = oarepo>=11,<12 + cachetools #packages = find: [options.extras_require] @@ -25,4 +26,8 @@ tests = [options.entry_points] oarepo.custom_fields = record_field = oarepo_communities.cf.loader:get_field +invenio_base.apps = + oarepo_communities = oarepo_communities.ext:OARepoCommunities +invenio_base.api_apps = + oarepo_communities = oarepo_communities.ext:OARepoCommunities diff --git a/tests/test_permissions/conftest.py b/tests/test_permissions/conftest.py index 8525f6c..1ac1dfa 100644 --- a/tests/test_permissions/conftest.py +++ b/tests/test_permissions/conftest.py @@ -1,22 +1,17 @@ import os -from pathlib import Path import pytest import yaml -from flask_security import login_user -from flask_security.utils import hash_password -from invenio_access import ActionUsers, current_access from invenio_access.permissions import system_identity -from invenio_accounts.proxies import current_datastore -from invenio_accounts.testutils import login_user_via_session from invenio_app.factory import create_api -from invenio_records_resources.services.uow import RecordCommitOp, UnitOfWork -from thesis.proxies import current_service -from thesis.records.api import ThesisDraft, ThesisRecord from invenio_communities import current_communities from invenio_communities.communities.records.api import Community -from invenio_pidstore.errors import PIDDoesNotExistError from invenio_communities.generators import CommunityRoleNeed +from invenio_pidstore.errors import PIDDoesNotExistError +from invenio_records_resources.services.uow import RecordCommitOp, UnitOfWork +from thesis.proxies import current_service +from thesis.records.api import ThesisDraft, ThesisRecord + @pytest.fixture(scope="function") def sample_metadata_list(): @@ -40,12 +35,12 @@ def create_app(instance_path, entry_points): def app_config(app_config): """Mimic an instance's configuration.""" app_config["JSONSCHEMAS_HOST"] = "localhost" - app_config["RECORDS_REFRESOLVER_CLS"] = ( - "invenio_records.resolver.InvenioRefResolver" - ) - app_config["RECORDS_REFRESOLVER_STORE"] = ( - "invenio_jsonschemas.proxies.current_refresolver_store" - ) + app_config[ + "RECORDS_REFRESOLVER_CLS" + ] = "invenio_records.resolver.InvenioRefResolver" + app_config[ + "RECORDS_REFRESOLVER_STORE" + ] = "invenio_jsonschemas.proxies.current_refresolver_store" app_config["RATELIMIT_AUTHENTICATED_USER"] = "200 per second" app_config["SEARCH_HOSTS"] = [ { @@ -58,6 +53,7 @@ def app_config(app_config): app_config["CACHE_DEFAULT_TIMEOUT"] = 300 from oarepo_communities.cf.permissions import PermissionsCF + app_config["COMMUNITIES_CUSTOM_FIELDS"] = [PermissionsCF("permissions")] app_config["FILES_REST_STORAGE_CLASS_LIST"] = { @@ -69,6 +65,7 @@ def app_config(app_config): return app_config + @pytest.fixture(scope="function") def sample_draft(app, db, input_data): with UnitOfWork(db.session) as uow: @@ -84,6 +81,7 @@ def vocab_cf(app, db, cache): prepare_cf_indices() + @pytest.fixture def example_record(app, db, input_data): # record = current_service.create(system_identity, sample_data[0]) @@ -105,6 +103,7 @@ def community_owner_helper(UserFixture, app, db): u.create(app, db) return u + @pytest.fixture() def community_curator_helper(UserFixture, app, db): """Community owner.""" @@ -115,6 +114,7 @@ def community_curator_helper(UserFixture, app, db): u.create(app, db) return u + @pytest.fixture() def community_manager_helper(UserFixture, app, db): """Community owner.""" @@ -125,6 +125,7 @@ def community_manager_helper(UserFixture, app, db): u.create(app, db) return u + @pytest.fixture() def community_reader_helper(UserFixture, app, db): """Community owner.""" @@ -135,6 +136,7 @@ def community_reader_helper(UserFixture, app, db): u.create(app, db) return u + @pytest.fixture() def rando_user(UserFixture, app, db): """Community owner.""" @@ -146,7 +148,6 @@ def rando_user(UserFixture, app, db): return u - @pytest.fixture(scope="module") def minimal_community(): """Minimal community metadata.""" @@ -160,44 +161,47 @@ def minimal_community(): "title": "My Community", }, } + + @pytest.fixture() def community_permissions_cf(): - - return { - "custom_fields":{ - "permissions": { - "owner": { - "can_publish": True, - "can_read": True, - "can_update": True, - "can_delete": True - }, - "manager": { - "can_publish": True, - "can_read": False, - "can_update": False, - "can_delete": False - }, - "curator": { - "can_publish": True, - "can_read": True, - "can_update": True, - "can_delete": False - }, - "reader": { - "can_publish": True, - "can_read": True, - "can_update": False, - "can_delete": False + return { + "custom_fields": { + "permissions": { + "owner": { + "can_publish": True, + "can_read": True, + "can_update": True, + "can_delete": True, + }, + "manager": { + "can_publish": True, + "can_read": False, + "can_update": False, + "can_delete": False, + }, + "curator": { + "can_publish": True, + "can_read": True, + "can_update": True, + "can_delete": False, + }, + "reader": { + "can_publish": True, + "can_read": True, + "can_update": False, + "can_delete": False, + }, } } } - } + @pytest.fixture(scope="module", autouse=True) def location(location): return location + @pytest.fixture() def inviter(): """Add/invite a user to a community with a specific role.""" @@ -220,6 +224,7 @@ def invite(user_id, community_id, role): return invite + def _community_get_or_create(community_dict, identity): """Util to get or create community, to avoid duplicate error.""" slug = community_dict["slug"] @@ -232,16 +237,23 @@ def _community_get_or_create(community_dict, identity): ) Community.index.refresh() return c + + @pytest.fixture() def community(app, community_owner_helper, minimal_community): """Get the current RDM records service.""" return _community_get_or_create(minimal_community, community_owner_helper.identity) + @pytest.fixture() def community_owner(UserFixture, community_owner_helper, community, inviter, app, db): - #inviter(community_owner_helper.id, community.id, "owner") - community_owner_helper.identity.provides.add(CommunityRoleNeed(community.data["id"], "owner")) + # inviter(community_owner_helper.id, community.id, "owner") + community_owner_helper.identity.provides.add( + CommunityRoleNeed(community.data["id"], "owner") + ) return community_owner_helper + + """ @pytest.fixture() def community_manager(UserFixture, community_owner_helper, community, inviter, app, db): @@ -255,8 +267,9 @@ def community_curator(UserFixture, community_owner_helper, community, inviter, a community_owner_helper.identity.provides.add(CommunityRoleNeed(community.data["id"], "curator")) return community_owner_helper """ + + @pytest.fixture() def community_reader(UserFixture, community_reader_helper, community, inviter, app, db): inviter(community_reader_helper.id, community.data["id"], "reader") return community_reader_helper - diff --git a/tests/test_permissions/test_permissions_api.py b/tests/test_permissions/test_permissions_api.py index 2340a1f..07cf4bc 100644 --- a/tests/test_permissions/test_permissions_api.py +++ b/tests/test_permissions/test_permissions_api.py @@ -10,9 +10,7 @@ def _create_and_publish(client, input_data, community, publish_authorized): """Create a draft and publish it.""" # Create the draft - response = client.post( - RECORD_COMMUNITIES_BASE_URL, json=input_data - ) + response = client.post(RECORD_COMMUNITIES_BASE_URL, json=input_data) assert response.status_code == 201 @@ -37,7 +35,6 @@ def _create_and_publish(client, input_data, community, publish_authorized): return recid - def _resp_to_input(resp): return { "slug": resp["slug"], @@ -61,30 +58,35 @@ def _recid_with_community( community, community_owner, community_permissions_cf, - publish_authorized=True + publish_authorized=True, ): - comm = _community_with_permissions_cf(community, community_owner.identity, community_permissions_cf) + comm = _community_with_permissions_cf( + community, community_owner.identity, community_permissions_cf + ) recid = _create_and_publish(owner_client, input_data, comm, publish_authorized) return recid - - -def test_owner(client, community_owner, rando_user, community, community_permissions_cf, input_data, vocab_cf, search_clear): +def test_owner( + client, + community_owner, + rando_user, + community, + community_permissions_cf, + input_data, + vocab_cf, + search_clear, +): owner_client = community_owner.login(client) - recid = _recid_with_community(owner_client, input_data, community, community_owner, community_permissions_cf) - - response_read = owner_client.get( - f"{RECORD_COMMUNITIES_BASE_URL}{recid}" + recid = _recid_with_community( + owner_client, input_data, community, community_owner, community_permissions_cf ) + + response_read = owner_client.get(f"{RECORD_COMMUNITIES_BASE_URL}{recid}") assert response_read.status_code == 200 - response_delete = owner_client.delete( - f"{RECORD_COMMUNITIES_BASE_URL}{recid}" - ) + response_delete = owner_client.delete(f"{RECORD_COMMUNITIES_BASE_URL}{recid}") assert response_delete.status_code == 204 - response_read = owner_client.get( - f"{RECORD_COMMUNITIES_BASE_URL}{recid}" - ) + response_read = owner_client.get(f"{RECORD_COMMUNITIES_BASE_URL}{recid}") assert response_read.status_code == 410 """ jsn = response_read.json["metadata"] @@ -96,28 +98,65 @@ def test_owner(client, community_owner, rando_user, community, community_permiss print() """ -def test_cf(client, community_owner, community, community_permissions_cf, input_data, vocab_cf, search_clear): + +def test_cf( + client, + community_owner, + community, + community_permissions_cf, + input_data, + vocab_cf, + search_clear, +): community_owner.login(client) - recid = _recid_with_community(client, input_data, community, community_owner, community_permissions_cf) - #sleep(5) - response = client.get( - f"{RECORD_COMMUNITIES_BASE_URL}{recid}/communities" + recid = _recid_with_community( + client, input_data, community, community_owner, community_permissions_cf + ) + # sleep(5) + response = client.get(f"{RECORD_COMMUNITIES_BASE_URL}{recid}/communities") + assert ( + response.json["hits"]["hits"][0]["custom_fields"] + == community_permissions_cf["custom_fields"] ) - assert response.json['hits']['hits'][0]["custom_fields"] == community_permissions_cf["custom_fields"] -def test_reader(client, community_owner, community_reader, community, community_permissions_cf, input_data, vocab_cf, search_clear): - reader_client = community_reader.login(client) - recid = _recid_with_community(reader_client, input_data, community, community_owner, community_permissions_cf) - response_read = reader_client.get( - f"{RECORD_COMMUNITIES_BASE_URL}{recid}" +def test_reader( + client, + community_owner, + community_reader, + community, + community_permissions_cf, + input_data, + vocab_cf, + search_clear, +): + reader_client = community_reader.login(client) + recid = _recid_with_community( + reader_client, input_data, community, community_owner, community_permissions_cf ) + + response_read = reader_client.get(f"{RECORD_COMMUNITIES_BASE_URL}{recid}") assert response_read.status_code == 200 - response_delete = reader_client.delete( - f"{RECORD_COMMUNITIES_BASE_URL}{recid}" - ) + response_delete = reader_client.delete(f"{RECORD_COMMUNITIES_BASE_URL}{recid}") assert response_delete.status_code == 403 -def test_rando(client, community_owner, rando_user, community, community_permissions_cf, input_data, vocab_cf, search_clear): + +def test_rando( + client, + community_owner, + rando_user, + community, + community_permissions_cf, + input_data, + vocab_cf, + search_clear, +): rando_client = rando_user.login(client) - _recid_with_community(rando_client, input_data, community, community_owner, community_permissions_cf, publish_authorized=False) + _recid_with_community( + rando_client, + input_data, + community, + community_owner, + community_permissions_cf, + publish_authorized=False, + ) diff --git a/tests/thesis.yaml b/tests/thesis.yaml index 4dbbd44..8ceef0b 100644 --- a/tests/thesis.yaml +++ b/tests/thesis.yaml @@ -15,6 +15,7 @@ record: draft: {} record-communities: {} + community-records: {} @@ -22,6 +23,7 @@ profiles: - record - draft - record_communities + - community_records settings: