Skip to content

Commit

Permalink
grant access: implement groups grant record access
Browse files Browse the repository at this point in the history
* add new endpoint for groups grant record access
* rename "role" to "group" to be consistent with groups in community memberships
* manage groups enabled feature flag by the permission
* closes #1672
  • Loading branch information
anikachurilova authored and ntarocco committed May 7, 2024
1 parent df7fe6b commit 8513b7b
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 102 deletions.
6 changes: 6 additions & 0 deletions invenio_rdm_records/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
RDMCommunityRecordsResource,
RDMCommunityRecordsResourceConfig,
RDMDraftFilesResourceConfig,
RDMGrantGroupAccessResourceConfig,
RDMGrantsAccessResource,
RDMGrantUserAccessResourceConfig,
RDMParentGrantsResource,
Expand Down Expand Up @@ -232,6 +233,11 @@ def init_resource(self, app):
config=RDMGrantUserAccessResourceConfig.build(app),
)

self.grant_group_access_resource = RDMGrantsAccessResource(
service=self.records_service,
config=RDMGrantGroupAccessResourceConfig.build(app),
)

# Record's communities
self.record_communities_resource = RDMRecordCommunitiesResource(
service=self.record_communities_service,
Expand Down
2 changes: 2 additions & 0 deletions invenio_rdm_records/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .config import (
RDMCommunityRecordsResourceConfig,
RDMDraftFilesResourceConfig,
RDMGrantGroupAccessResourceConfig,
RDMGrantUserAccessResourceConfig,
RDMParentGrantsResourceConfig,
RDMParentRecordLinksResourceConfig,
Expand Down Expand Up @@ -39,6 +40,7 @@
"RDMGrantsAccessResource",
"RDMParentGrantsResourceConfig",
"RDMGrantUserAccessResourceConfig",
"RDMGrantGroupAccessResourceConfig",
"RDMParentRecordLinksResource",
"RDMParentRecordLinksResourceConfig",
"RDMRecordCommunitiesResourceConfig",
Expand Down
44 changes: 41 additions & 3 deletions invenio_rdm_records/resources/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,15 +346,15 @@ class RDMDraftMediaFilesResourceConfig(FileResourceConfig, ConfiguratorMixin):
LookupError: create_error_handler(
HTTPJSONException(
code=404,
description="No secret link found with the given ID.",
description=_("No secret link found with the given ID."),
)
),
}

grants_error_handlers = {
**deepcopy(RecordResourceConfig.error_handlers),
LookupError: create_error_handler(
HTTPJSONException(code=404, description="No grant found with the given ID.")
HTTPJSONException(code=404, description=_("No grant found with the given ID."))
),
GrantExistsError: create_error_handler(
lambda e: HTTPJSONException(
Expand All @@ -367,7 +367,14 @@ class RDMDraftMediaFilesResourceConfig(FileResourceConfig, ConfiguratorMixin):
user_access_error_handlers = {
**deepcopy(RecordResourceConfig.error_handlers),
LookupError: create_error_handler(
HTTPJSONException(code=404, description="No grant found by given user id.")
HTTPJSONException(code=404, description=_("No grant found by given user id."))
),
}

group_access_error_handlers = {
**deepcopy(RecordResourceConfig.error_handlers),
LookupError: create_error_handler(
HTTPJSONException(code=404, description=_("No grant found by given group id."))
),
}

Expand Down Expand Up @@ -462,6 +469,37 @@ class RDMGrantUserAccessResourceConfig(RecordResourceConfig, ConfiguratorMixin):
error_handlers = user_access_error_handlers


class RDMGrantGroupAccessResourceConfig(RecordResourceConfig, ConfiguratorMixin):
"""Record grants group access resource configuration."""

blueprint_name = "record_group_access"

url_prefix = "/records/<pid_value>/access"

routes = {
"item": "/groups/<subject_id>",
"list": "/groups",
}

links_config = {}

request_view_args = {
"pid_value": ma.fields.Str(),
"subject_id": ma.fields.Str(), # group id
}

grant_subject_type = "role"

response_handlers = {
"application/vnd.inveniordm.v1+json": RecordResourceConfig.response_handlers[
"application/json"
],
**deepcopy(RecordResourceConfig.response_handlers),
}

error_handlers = group_access_error_handlers


#
# Community's records
#
Expand Down
134 changes: 48 additions & 86 deletions invenio_rdm_records/services/access/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@

from ...requests.access import AccessRequestToken, GuestAccessRequest, UserAccessRequest
from ...secret_links.errors import InvalidPermissionLevelError
from ..decorators import groups_enabled
from ..errors import AccessRequestExistsError, GrantExistsError
from ..results import GrantSubjectExpandableField


class RecordAccessService(RecordService):
"""RDM Secret Link service."""

group_subject_type = "role"

def link_result_item(self, *args, **kwargs):
"""Create a new instance of the resource unit."""
return self.config.link_result_item_cls(*args, **kwargs)
Expand Down Expand Up @@ -340,7 +343,7 @@ def _validate_grant_subject(self, identity, grant):
current_user_resources.users_service.read(
identity=identity, id_=grant.subject_id
)
elif grant.subject_type == "role":
elif grant.subject_type == RecordAccessService.group_subject_type:
current_user_resources.groups_service.read(
identity=identity, id_=grant.subject_id
)
Expand Down Expand Up @@ -385,6 +388,13 @@ def bulk_create_grants(self, identity, id_, data, expand=False, uow=None):
raise GrantExistsError()

for grant in grants:
# checks if groups are enabled in the instance
if (
not current_app.config.get("USERS_RESOURCES_GROUPS_ENABLED", False)
and grant["subject"]["type"] == RecordAccessService.group_subject_type
):
raise PermissionDeniedError()

# Creation
new_grant = parent.access.grants.create(
subject_type=grant["subject"]["type"],
Expand Down Expand Up @@ -433,6 +443,13 @@ def read_grant(self, identity, id_, grant_id, expand=False):

grant = parent.access.grants[grant_id]

# checks if groups are enabled in the instance
if (
not current_app.config.get("USERS_RESOURCES_GROUPS_ENABLED", False)
and grant.subject_type == RecordAccessService.group_subject_type
):
raise PermissionDeniedError()

return self.grant_result_item(
self,
identity,
Expand All @@ -459,6 +476,14 @@ def update_grant(

# Fetching (required for parts of the validation)
old_grant = parent.access.grants[grant_id]

# checks if groups are enabled in the instance
if (
not current_app.config.get("USERS_RESOURCES_GROUPS_ENABLED", False)
and old_grant.subject_type == RecordAccessService.group_subject_type
):
raise PermissionDeniedError()

if partial:
data = {
"permission": data.get("permission", old_grant.permission),
Expand Down Expand Up @@ -506,97 +531,22 @@ def read_all_grants(self, identity, id_, expand=False):
# Permissions
self.require_permission(identity, "manage", record=record)

# Fetching
return self.grants_result_list(
service=self,
identity=identity,
results=parent.access.grants,
expand=expand,
)
existing_grants = parent.access.grants

def read_all_grants_by_subject(self, identity, id_, subject_type, expand=False):
"""Read access grants of a record (resp. its parent) by subject type."""
record, parent = self.get_parent_and_record_or_draft(id_)

# Permissions
self.require_permission(identity, "manage", record=record)

user_grants = []
for grant in parent.access.grants:
if grant.subject_type == subject_type:
user_grants.append(grant)
for grant in existing_grants:
# removes group grants if groups are not enabled in the instance
if (
not current_app.config.get("USERS_RESOURCES_GROUPS_ENABLED", False)
and grant.subject_type == RecordAccessService.group_subject_type
):
# don't fail with 403, instead return only user grants, even if group grants are present
existing_grants.remove(grant)

# Fetching
return self.grants_result_list(
service=self,
identity=identity,
results=user_grants,
expand=expand,
)

@unit_of_work()
def update_grant_by_subject(
self,
identity,
id_,
subject_id,
subject_type,
data,
expand=False,
uow=None,
):
"""Update access grant for a record (resp. its parent) by subject."""
record, parent = self.get_parent_and_record_or_draft(id_)

# Permissions
self.require_permission(identity, "manage", record=record)

# Fetching (required for parts of the validation)
grant_id = None
for grant in parent.access.grants:
if grant.subject_id == subject_id and grant.subject_type == subject_type:
grant_id = parent.access.grants.index(grant)

if grant_id is None:
raise LookupError(subject_id)

old_grant = parent.access.grants[grant_id]
data = {
"permission": data.get("permission", old_grant.permission),
"subject": {
"type": data.get("subject", {}).get("type", old_grant.subject_type),
"id": data.get("subject", {}).get("id", old_grant.subject_id),
},
"origin": data.get("origin", old_grant.origin),
}

# Validation
data, __ = self.schema_grant.load(
data, context={"identity": identity}, raise_errors=True
)

# Update
try:
new_grant = parent.access.grants.grant_cls.create(
origin=data["origin"],
permission=data["permission"],
subject_type=data["subject"]["type"],
subject_id=data["subject"]["id"],
resolve_subject=True,
)
except LookupError:
raise ValidationError(
_("Could not find the specified subject."), field_name="subject.id"
)

parent.access.grants[grant_id] = new_grant

uow.register(ParentRecordCommitOp(parent, indexer_context=dict(service=self)))

return self.grant_result_item(
self,
identity,
new_grant,
results=existing_grants,
expand=expand,
)

Expand All @@ -612,6 +562,14 @@ def delete_grant(self, identity, id_, grant_id, uow=None):
if not 0 <= grant_id < len(parent.access.grants):
raise LookupError(str(grant_id))

# checks if groups are enabled in the instance
if (
not current_app.config.get("USERS_RESOURCES_GROUPS_ENABLED", False)
and parent.access.grants[grant_id].subject_type
== RecordAccessService.group_subject_type
):
raise PermissionDeniedError()

# Deletion
parent.access.grants.pop(grant_id)

Expand Down Expand Up @@ -878,6 +836,7 @@ def update_access_settings(

# TODO: rework the whole service and move these to a separate one:
# https://github.com/inveniosoftware/invenio-rdm-records/issues/1685
@groups_enabled(group_subject_type)
def read_grant_by_subject(
self, identity, id_, subject_id, subject_type, expand=False
):
Expand All @@ -902,6 +861,7 @@ def read_grant_by_subject(
expand=expand,
)

@groups_enabled(group_subject_type)
def read_all_grants_by_subject(self, identity, id_, subject_type, expand=False):
"""Read access grants of a record (resp. its parent) by subject type."""
record, parent = self.get_parent_and_record_or_draft(id_)
Expand All @@ -922,6 +882,7 @@ def read_all_grants_by_subject(self, identity, id_, subject_type, expand=False):
expand=expand,
)

@groups_enabled(group_subject_type)
@unit_of_work()
def update_grant_by_subject(
self,
Expand Down Expand Up @@ -988,6 +949,7 @@ def update_grant_by_subject(
expand=expand,
)

@groups_enabled(group_subject_type)
@unit_of_work()
def delete_grant_by_subject(
self, identity, id_, subject_id, subject_type, uow=None
Expand Down
8 changes: 8 additions & 0 deletions invenio_rdm_records/services/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ def archive_download_enabled(record, ctx):
return current_app.config["RDM_ARCHIVE_DOWNLOAD_ENABLED"]


def _groups_enabled(record, ctx):
"""Return if groups are enabled."""
return current_app.config.get("USERS_RESOURCES_GROUPS_ENABLED", False)


def is_datacite_test(record, ctx):
"""Return if the datacite test mode is being used."""
return current_app.config["DATACITE_TEST_MODE"]
Expand Down Expand Up @@ -482,6 +487,9 @@ class RDMRecordServiceConfig(RecordServiceConfig, ConfiguratorMixin):
"access_links": RecordLink("{+api}/records/{id}/access/links"),
"access_grants": RecordLink("{+api}/records/{id}/access/grants"),
"access_users": RecordLink("{+api}/records/{id}/access/users"),
"access_groups": RecordLink(
"{+api}/records/{id}/access/groups", when=_groups_enabled
),
"access_request": RecordLink("{+api}/records/{id}/access/request"),
"access": RecordLink("{+api}/records/{id}/access"),
# TODO: only include link when DOI support is enabled.
Expand Down
33 changes: 33 additions & 0 deletions invenio_rdm_records/services/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2024 CERN.
#
# Invenio-RDM-Records is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.

"""RDM services decorators."""

from functools import wraps

from flask import current_app
from invenio_records_resources.services.errors import PermissionDeniedError


def groups_enabled(group_subject_type, **kwargs):
"""Decorator to check if users are trying to access disabled feature."""

def decorator(f):
@wraps(f)
def inner(self, *args, **kwargs):
subject_type = kwargs["subject_type"]
if (
not current_app.config.get("USERS_RESOURCES_GROUPS_ENABLED", False)
and subject_type == group_subject_type
):
raise PermissionDeniedError()

return f(self, *args, **kwargs)

return inner

return decorator
Loading

0 comments on commit 8513b7b

Please sign in to comment.