Skip to content

Commit

Permalink
files: add check for deleted record
Browse files Browse the repository at this point in the history
* closes #1479
  • Loading branch information
jrcastro2 authored and kpsherva committed Oct 4, 2023
1 parent f69ae38 commit 1e669d9
Show file tree
Hide file tree
Showing 8 changed files with 404 additions and 5 deletions.
10 changes: 5 additions & 5 deletions invenio_rdm_records/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from flask_iiif import IIIF
from flask_principal import identity_loaded
from invenio_records_resources.resources.files import FileResource
from invenio_records_resources.services import FileService

from . import config
from .oaiserver.resources.config import OAIPMHServerResourceConfig
Expand Down Expand Up @@ -62,6 +61,7 @@
RDMMediaFileRecordServiceConfig,
RDMRecordMediaFilesServiceConfig,
)
from .services.files import RDMFileService
from .services.pids import PIDManager, PIDsService
from .services.review.service import ReviewService
from .utils import verify_token
Expand Down Expand Up @@ -147,17 +147,17 @@ def init_services(self, app):
# Services
self.records_service = RDMRecordService(
service_configs.record,
files_service=FileService(service_configs.file),
draft_files_service=FileService(service_configs.file_draft),
files_service=RDMFileService(service_configs.file),
draft_files_service=RDMFileService(service_configs.file_draft),
access_service=RecordAccessService(service_configs.record),
pids_service=PIDsService(service_configs.record, PIDManager),
review_service=ReviewService(service_configs.record),
)

self.records_media_files_service = RDMRecordService(
service_configs.record_with_media_files,
files_service=FileService(service_configs.media_file),
draft_files_service=FileService(service_configs.media_file_draft),
files_service=RDMFileService(service_configs.media_file),
draft_files_service=RDMFileService(service_configs.media_file_draft),
pids_service=PIDsService(service_configs.record, PIDManager),
)

Expand Down
31 changes: 31 additions & 0 deletions invenio_rdm_records/resources/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from invenio_drafts_resources.resources import RecordResourceConfig
from invenio_i18n import lazy_gettext as _
from invenio_records.systemfields.relations import InvalidRelationValue
from invenio_records_resources.resources.errors import ErrorHandlersMixin
from invenio_records_resources.resources.files import FileResourceConfig
from invenio_records_resources.resources.records.headers import etag_headers
from invenio_records_resources.services.base.config import ConfiguratorMixin, FromConfig
Expand Down Expand Up @@ -233,6 +234,21 @@ class RDMRecordFilesResourceConfig(FileResourceConfig, ConfiguratorMixin):
blueprint_name = "record_files"
url_prefix = "/records/<pid_value>"

error_handlers = {
**ErrorHandlersMixin.error_handlers,
RecordDeletedException: create_error_handler(
lambda e: (
HTTPJSONException(code=404, description=_("Record not found"))
if not e.record.tombstone.is_visible
else HTTPJSONException(
code=410,
description=_("Record deleted"),
tombstone=e.record.tombstone.dump(),
)
)
),
}


#
# Draft files
Expand Down Expand Up @@ -260,6 +276,21 @@ class RDMRecordMediaFilesResourceConfig(FileResourceConfig, ConfiguratorMixin):
"list-archive": "/media-files-archive",
}

error_handlers = {
**ErrorHandlersMixin.error_handlers,
RecordDeletedException: create_error_handler(
lambda e: (
HTTPJSONException(code=404, description=_("Record not found"))
if not e.record.tombstone.is_visible
else HTTPJSONException(
code=410,
description=_("Record deleted"),
tombstone=e.record.tombstone.dump(),
)
)
),
}


#
# Draft files
Expand Down
11 changes: 11 additions & 0 deletions invenio_rdm_records/services/files/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2023 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.

"""File Service API."""
from invenio_rdm_records.services.files.service import RDMFileService

__all__ = ("RDMFileService",)
34 changes: 34 additions & 0 deletions invenio_rdm_records/services/files/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2023 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.

"""File Service API."""
from invenio_records_resources.services import FileService
from invenio_records_resources.services.errors import FileKeyNotFoundError

from invenio_rdm_records.services.errors import RecordDeletedException


class RDMFileService(FileService):
"""A service for adding files support to records."""

def _check_record_deleted_permissions(self, record, identity):
"""Ensure that the record exists (not deleted) or raise."""
if record.is_draft:
return
if record.deletion_status.is_deleted:
can_read_deleted = self.check_permission(
identity, "read_deleted_files", record=record
)
if not can_read_deleted:
raise RecordDeletedException(record)

def _get_record(self, id_, identity, action, file_key=None):
"""Get the associated record."""
record = super()._get_record(id_, identity, action, file_key)
self._check_record_deleted_permissions(record, identity)

return record
2 changes: 2 additions & 0 deletions invenio_rdm_records/services/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ class RDMRecordPermissionPolicy(RecordPermissionPolicy):
else_=can_read,
)
]
can_read_deleted_files = can_read_deleted
can_media_read_deleted_files = can_read_deleted_files
# Allow reading the files of a record
can_read_files = [
IfRestricted("files", then_=can_view, else_=can_all),
Expand Down
106 changes: 106 additions & 0 deletions tests/resources/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ def create_draft(client, record, headers):
return response.json


def delete_record(client, recid, headers):
"""Soft delete a record."""
response = client.delete(f"/records/{recid}/delete", json={}, headers=headers)
assert response.status_code == 204
response.close()


def attach_file(client, recid, key, headers):
"""Attach a file to a record."""

Expand Down Expand Up @@ -108,3 +115,102 @@ def test_published_record_files_deny_edit(
# the upload of a file is the one that is affected when we lock the bucket
assert response.status_code == 403
logout_user(client)


def test_files_api_flow_for_deleted_record(
client, headers, running_app, minimal_record, users, location, superuser
):
login_user(client, users[0])

draft = create_draft(client, minimal_record, headers)
recid = draft["id"]

attach_file(client, recid, "test.pdf", headers)

# publish the draft
response = client.post(link(draft["links"]["publish"]), headers=headers)
assert response.status_code == 202
assert response.data

logout_user(client)

url = f"/records/{recid}/files"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/files/test.pdf"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/files/test.pdf/content"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/files-archive"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

login_user(client, superuser.user)

# We delete the record
delete_record(client, recid, headers)

# Superuser has access to record
url = f"/records/{recid}/files"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/files/test.pdf"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/files/test.pdf/content"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/files-archive"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

logout_user(client)

# Non superuser users have no access to the record
url = f"/records/{recid}/files"

response = client.get(url, headers=headers)
assert 410 == response.status_code
assert response.data

url = f"/records/{recid}/files/test.pdf"

response = client.get(url, headers=headers)
assert 410 == response.status_code
assert response.data

url = f"/records/{recid}/files/test.pdf/content"

response = client.get(url, headers=headers)
assert 410 == response.status_code
assert response.data

url = f"/records/{recid}/files-archive"

response = client.get(url, headers=headers)
assert 410 == response.status_code
assert response.data
110 changes: 110 additions & 0 deletions tests/resources/test_media_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ def create_draft(client, record, headers):
return response.json["id"]


def publish_draft(client, recid, headers):
"""Publish a draft and returns the id."""
response = client.post(f"/records/{recid}/draft/actions/publish", headers=headers)
assert response.status_code == 202
assert response.data
return recid


def delete_record(client, recid, headers):
"""Soft delete a record."""
response = client.delete(f"/records/{recid}/delete", json={}, headers=headers)
assert response.status_code == 204
response.close()


def init_file(client, recid, headers):
"""Init a file for draft with given recid."""
return client.post(
Expand Down Expand Up @@ -160,3 +175,98 @@ def test_only_owners_can_list_draft_w_public_files(
login_user(client, users[0])
response = client.get(url, headers=headers)
assert 200 == response.status_code


def test_list_media_files_for_deleted_record(
client, headers, running_app, minimal_record, users, location, superuser
):
login_user(client, users[0])

recid = create_draft(client, minimal_record, headers)
init_file(client, recid, headers)
upload_file(client, recid)
# upload file and commit
response = commit_file(client, recid, headers)
assert response.status_code == 200
assert response.data
publish_draft(client, recid, headers)
logout_user(client)

url = f"/records/{recid}/media-files"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/media-files/test.pdf"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/media-files/test.pdf/content"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/media-files-archive"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

login_user(client, superuser.user)

# We delete the record
delete_record(client, recid, headers)

url = f"/records/{recid}/media-files"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/media-files/test.pdf?include_deleted=1"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/media-files/test.pdf/content"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

url = f"/records/{recid}/media-files-archive?include_deleted=1"

response = client.get(url, headers=headers)
assert 200 == response.status_code
assert response.data

logout_user(client)

url = f"/records/{recid}/media-files"

response = client.get(url, headers=headers)
assert 410 == response.status_code
assert response.data

url = f"/records/{recid}/media-files/test.pdf"

response = client.get(url, headers=headers)
assert 410 == response.status_code
assert response.data

url = f"/records/{recid}/media-files/test.pdf/content"

response = client.get(url, headers=headers)
assert 410 == response.status_code
assert response.data

url = f"/records/{recid}/media-files-archive"

response = client.get(url, headers=headers)
assert 410 == response.status_code
assert response.data
Loading

0 comments on commit 1e669d9

Please sign in to comment.