Skip to content

Commit

Permalink
api: create new endpoint for access/users
Browse files Browse the repository at this point in the history
* add resource and service layer
* add error handerls
* add links
* cover with tests
* closes #1671
  • Loading branch information
anikachurilova committed Mar 15, 2024
1 parent ab5797a commit d0b85b6
Show file tree
Hide file tree
Showing 15 changed files with 1,003 additions and 5 deletions.
7 changes: 7 additions & 0 deletions invenio_rdm_records/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
RDMCommunityRecordsResource,
RDMCommunityRecordsResourceConfig,
RDMDraftFilesResourceConfig,
RDMGrantsAccessResource,
RDMGrantUserAccessResourceConfig,
RDMParentGrantsResource,
RDMParentGrantsResourceConfig,
RDMParentRecordLinksResource,
Expand Down Expand Up @@ -223,6 +225,11 @@ def init_resource(self, app):
config=RDMParentGrantsResourceConfig.build(app),
)

self.grant_user_access_resource = RDMGrantsAccessResource(
service=self.records_service,
config=RDMGrantUserAccessResourceConfig.build(app),
)

# Record's communities
self.record_communities_resource = RDMRecordCommunitiesResource(
service=self.record_communities_service,
Expand Down
4 changes: 4 additions & 0 deletions invenio_rdm_records/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
IIIFResourceConfig,
RDMCommunityRecordsResourceConfig,
RDMDraftFilesResourceConfig,
RDMGrantUserAccessResourceConfig,
RDMParentGrantsResourceConfig,
RDMParentRecordLinksResourceConfig,
RDMRecordCommunitiesResourceConfig,
Expand All @@ -22,6 +23,7 @@
from .resources import (
IIIFResource,
RDMCommunityRecordsResource,
RDMGrantsAccessResource,
RDMParentGrantsResource,
RDMParentRecordLinksResource,
RDMRecordRequestsResource,
Expand All @@ -35,7 +37,9 @@
"RDMCommunityRecordsResourceConfig",
"RDMDraftFilesResourceConfig",
"RDMParentGrantsResource",
"RDMGrantsAccessResource",
"RDMParentGrantsResourceConfig",
"RDMGrantUserAccessResourceConfig",
"RDMParentRecordLinksResource",
"RDMParentRecordLinksResourceConfig",
"RDMRecordCommunitiesResourceConfig",
Expand Down
54 changes: 51 additions & 3 deletions invenio_rdm_records/resources/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

from ..services.errors import (
AccessRequestExistsError,
GrantExistsError,
InvalidAccessRestrictions,
RecordDeletedException,
ReviewExistsError,
Expand Down Expand Up @@ -357,7 +358,23 @@ class RDMDraftMediaFilesResourceConfig(FileResourceConfig, ConfiguratorMixin):
{
LookupError: create_error_handler(
HTTPJSONException(code=404, description="No grant found with the given ID.")
)
),
GrantExistsError: create_error_handler(
lambda e: HTTPJSONException(
code=400,
description=e.description,
)
),
}
)

user_access_error_handlers = RecordResourceConfig.error_handlers.copy()

user_access_error_handlers.update(
{
LookupError: create_error_handler(
HTTPJSONException(code=404, description="No grant found by given user id.")
),
}
)

Expand Down Expand Up @@ -399,8 +416,8 @@ class RDMParentGrantsResourceConfig(RecordResourceConfig, ConfiguratorMixin):
url_prefix = "/records/<pid_value>/access"

routes = {
"list": "/users",
"item": "/users/<grant_id>",
"list": "/grants",
"item": "/grants/<grant_id>",
}

links_config = {}
Expand All @@ -421,6 +438,37 @@ class RDMParentGrantsResourceConfig(RecordResourceConfig, ConfiguratorMixin):
error_handlers = grants_error_handlers


class RDMGrantUserAccessResourceConfig(RecordResourceConfig, ConfiguratorMixin):
"""Record grants user access resource configuration."""

blueprint_name = "record_user_access"

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

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

links_config = {}

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

grant_subject_type = "user"

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

error_handlers = user_access_error_handlers


#
# Community's records
#
Expand Down
74 changes: 74 additions & 0 deletions invenio_rdm_records/resources/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,80 @@ def search(self):
return items.to_dict(), 200


class RDMGrantsAccessResource(RecordResource):
"""Users and groups grant access resource."""

def create_url_rules(self):
"""Create the URL rules for the record resource."""

def p(route_name):
"""Prefix a route with the URL prefix."""
return f"{self.config.url_prefix}{self.config.routes[route_name]}"

return [
route("GET", p("item"), self.read),
route("DELETE", p("item"), self.delete),
route("GET", p("list"), self.search),
route("PATCH", p("item"), self.partial_update),
]

@request_extra_args
@request_view_args
@response_handler()
def read(self):
"""Read an access grant for a record by subject."""
item = self.service.access.read_grant_by_subject(
identity=g.identity,
id_=resource_requestctx.view_args["pid_value"],
subject_id=resource_requestctx.view_args["subject_id"],
subject_type=self.config.grant_subject_type,
expand=resource_requestctx.args.get("expand", False),
)

return item.to_dict(), 200

@request_view_args
def delete(self):
"""Delete an access grant for a record by subject."""
self.service.access.delete_grant_by_subject(
identity=g.identity,
id_=resource_requestctx.view_args["pid_value"],
subject_id=resource_requestctx.view_args["subject_id"],
subject_type=self.config.grant_subject_type,
)
return "", 204

@request_extra_args
@request_search_args
@request_view_args
@response_handler(many=True)
def search(self):
"""List access grants for a record by subject type."""
items = self.service.access.read_all_grants_by_subject(
identity=g.identity,
id_=resource_requestctx.view_args["pid_value"],
subject_type=self.config.grant_subject_type,
expand=resource_requestctx.args.get("expand", False),
)
return items.to_dict(), 200

@request_extra_args
@request_view_args
@request_data
@response_handler()
def partial_update(self):
"""Patch access grant for a record by subject."""
item = self.service.access.update_grant_by_subject(
identity=g.identity,
id_=resource_requestctx.view_args["pid_value"],
subject_id=resource_requestctx.view_args["subject_id"],
subject_type=self.config.grant_subject_type,
data=resource_requestctx.data,
expand=resource_requestctx.args.get("expand", False),
)
return item.to_dict(), 200


#
# Community's records
#
Expand Down
145 changes: 144 additions & 1 deletion invenio_rdm_records/services/access/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

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


Expand Down Expand Up @@ -365,6 +365,13 @@ def create_grant(self, identity, id_, data, expand=False, uow=None):
data, context={"identity": identity}, raise_errors=True
)

for grant in parent.access.grants:
if (
grant.subject_id == data["subject"]["id"]
and grant.subject_type == data["subject"]["type"]
):
raise GrantExistsError()

# Creation
grant = parent.access.grants.create(
subject_type=data["subject"]["type"],
Expand Down Expand Up @@ -756,3 +763,139 @@ def update_access_settings(
record,
links_tpl=self.links_item_tpl,
)

# TODO: rework the whole service and move these to a separate one:
# https://github.com/inveniosoftware/invenio-rdm-records/issues/1685
def read_grant_by_subject(
self, identity, id_, subject_id, subject_type, expand=False
):
"""Read a specific access grant of a record by subject."""
record, parent = self.get_parent_and_record_or_draft(id_)

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

result = None
for grant in parent.access.grants:
if grant.subject_id == subject_id and grant.subject_type == subject_type:
result = grant

if not result:
raise LookupError(subject_id)

return self.grant_result_item(
self,
identity,
result,
expand=expand,
)

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)

# Fetching
return self.grant_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_index = None
for grant in parent.access.grants:
if grant.subject_id == subject_id and grant.subject_type == subject_type:
grant_index = parent.access.grants.index(grant)

if grant_index is None:
raise LookupError(subject_id)

old_grant = parent.access.grants[grant_index]
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_index] = new_grant

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

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

@unit_of_work()
def delete_grant_by_subject(
self, identity, id_, subject_id, subject_type, uow=None
):
"""Delete an access grant for a record by subject."""
record, parent = self.get_parent_and_record_or_draft(id_)

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

# Deletion
result = None
for grant in parent.access.grants:
if grant.subject_id == subject_id and grant.subject_type == subject_type:
result = grant
parent.access.grants.remove(grant)

if not result:
raise LookupError(subject_id)

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

return True
1 change: 1 addition & 0 deletions invenio_rdm_records/services/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ class RDMRecordServiceConfig(RecordServiceConfig, ConfiguratorMixin):
),
"versions": RecordLink("{+api}/records/{id}/versions"),
"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_request": RecordLink("{+api}/records/{id}/access/request"),
"access": RecordLink("{+api}/records/{id}/access"),
Expand Down
6 changes: 6 additions & 0 deletions invenio_rdm_records/services/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ class RDMRecordsException(Exception):
"""Base exception for RDMRecords errors."""


class GrantExistsError(RDMRecordsException):
"""Exception raised when trying to create a grant that already exists for user/role."""

description = _("Grant for this user/role already exists within this record.")


class RecordDeletedException(RDMRecordsException):
"""Exception denoting that the record was deleted."""

Expand Down
Loading

0 comments on commit d0b85b6

Please sign in to comment.