Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions modules/invenio-files-rest/invenio_files_rest/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,42 @@ def has_update_version_role(user):
if lst.name in roles_user:
return True
return False


def get_guest_activity_bucket_ids(token):

@ivis-miyachi ivis-miyachi Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix no.460, 25, 474, 475, 476

"""Get the ids of the buckets used by the guest activity of a token.

The buckets are those of the item registered in the activity and of the
root version of that item.

:param token: The guest activity token.
:return: A set of bucket ids as strings.
"""
from invenio_pidstore.models import PersistentIdentifier
from invenio_records_files.models import RecordsBuckets
from weko_workflow.api import WorkActivity
from weko_workflow.models import GuestActivity

if not token:
return set()
guest_activity = GuestActivity.query.filter_by(token=token).first()
if not guest_activity:
return set()
activity = WorkActivity.get_activity_by_id(guest_activity.activity_id)
if not activity or not activity.item_id:
return set()

record_ids = {activity.item_id}
recid = PersistentIdentifier.query.filter_by(
pid_type='recid', object_type='rec',
object_uuid=activity.item_id).first()
if recid:
root = PersistentIdentifier.query.filter_by(
pid_type='recid',
pid_value=recid.pid_value.split('.')[0]).first()
if root and root.object_uuid:
record_ids.add(root.object_uuid)

records_buckets = RecordsBuckets.query.filter(
RecordsBuckets.record_id.in_(list(record_ids))).all()
return {str(rb.bucket_id) for rb in records_buckets}
10 changes: 9 additions & 1 deletion modules/invenio-files-rest/invenio_files_rest/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
InvalidTagError, MissingQueryParameter, MultipartInvalidChunkSize
from .models import Bucket, Location, MultipartObject, ObjectVersion, \
ObjectVersionTag, Part
from .permissions import get_guest_activity_bucket_ids
from .proxies import current_files_rest, current_permission_factory
from .serializer import json_serializer
from .signals import file_downloaded, file_previewed
Expand Down Expand Up @@ -375,6 +376,8 @@ def decorate(*args, **kwargs):
def is_guest_login_can_access_file(permission):
"""Check guest login upload file.

Only the buckets used by the guest activity of the token are allowed.

Args:
permission: The permission to check.

Expand All @@ -387,10 +390,15 @@ def is_guest_login_can_access_file(permission):
"files-rest-object-read", "files-rest-bucket-update",
"files-rest-object-delete", "files-rest-object-delete-version",
]
bucket_ids = None
for need in permission.needs:
if need.method == 'action' and \
need.value in guest_access_file_actions:
return True
if bucket_ids is None:

@ivis-miyachi ivis-miyachi Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix no.460, 25, 474, 475, 476

bucket_ids = get_guest_activity_bucket_ids(
session.get('guest_token'))
if getattr(need, 'argument', None) in bucket_ids:
return True
return False


Expand Down
82 changes: 82 additions & 0 deletions modules/invenio-files-rest/tests/test_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015-2019 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Test permission helpers."""

import uuid
from unittest.mock import MagicMock, patch

from invenio_files_rest.permissions import get_guest_activity_bucket_ids


def _first(value):
query = MagicMock()
query.first.return_value = value
return query


# def get_guest_activity_bucket_ids(token):
# .tox/c1/bin/pytest --cov=invenio_files_rest tests/test_permissions.py::test_get_guest_activity_bucket_ids -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio-files-rest/.tox/c1/tmp
def test_get_guest_activity_bucket_ids(app):
from invenio_pidstore.models import PersistentIdentifier
from invenio_records_files.models import RecordsBuckets
from weko_workflow.models import GuestActivity

item_id = uuid.uuid4()
root_id = uuid.uuid4()
bucket_ids = [uuid.uuid4(), uuid.uuid4()]

guest_query = MagicMock()
pid_query = MagicMock()
rb_query = MagicMock()
get_activity = 'weko_workflow.api.WorkActivity.get_activity_by_id'

with patch.object(GuestActivity, 'query', guest_query), \
patch.object(PersistentIdentifier, 'query', pid_query), \
patch.object(RecordsBuckets, 'query', rb_query), \
patch(get_activity) as mock_activity:
# No token
assert get_guest_activity_bucket_ids(None) == set()
guest_query.filter_by.assert_not_called()

# Unknown token
guest_query.filter_by.return_value = _first(None)
assert get_guest_activity_bucket_ids('token') == set()
guest_query.filter_by.assert_called_with(token='token')

# Activity without item
guest_query.filter_by.return_value = _first(
MagicMock(activity_id='A-00000000-00001'))
mock_activity.return_value = MagicMock(item_id=None)
assert get_guest_activity_bucket_ids('token') == set()
mock_activity.assert_called_with('A-00000000-00001')

mock_activity.return_value = None
assert get_guest_activity_bucket_ids('token') == set()

# Activity with item
mock_activity.return_value = MagicMock(item_id=item_id)
pid_query.filter_by.side_effect = [
_first(MagicMock(pid_value='1.1')),
_first(MagicMock(object_uuid=root_id)),
]
rb_query.filter.return_value.all.return_value = [
MagicMock(bucket_id=bucket_ids[0]),
MagicMock(bucket_id=bucket_ids[1]),
]
result = get_guest_activity_bucket_ids('token')
assert result == {str(b) for b in bucket_ids}
assert pid_query.filter_by.call_args_list[1][1] == dict(
pid_type='recid', pid_value='1')

# Item without PID
pid_query.filter_by.side_effect = [_first(None)]
rb_query.filter.return_value.all.return_value = [
MagicMock(bucket_id=bucket_ids[0])]
result = get_guest_activity_bucket_ids('token')
assert result == {str(bucket_ids[0])}
64 changes: 64 additions & 0 deletions modules/invenio-files-rest/tests/test_views_objectversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,3 +641,67 @@ def test_put_header_invalid_tags(app, client, bucket, permissions, get_md5,
headers={header_name: 'a=1&a=2'},
)
assert resp.status_code == 400


# def is_guest_login_can_access_file(permission):
# .tox/c1/bin/pytest --cov=invenio_files_rest tests/test_views_objectversion.py::test_is_guest_login_can_access_file -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio-files-rest/.tox/c1/tmp
def test_is_guest_login_can_access_file(app, db, bucket, objects):
"""Test guest access limited to the buckets of the guest activity."""
from flask import session
from invenio_files_rest.models import Bucket
from invenio_files_rest.permissions import permission_factory
from invenio_files_rest.views import is_guest_login_can_access_file

other_bucket = Bucket.create()
db.session.commit()
target = 'invenio_files_rest.views.get_guest_activity_bucket_ids'

with app.test_request_context():
# No guest token
with patch(target, return_value={str(bucket.id)}) as mock_ids:
assert not is_guest_login_can_access_file(
permission_factory(objects[0], 'object-read'))
mock_ids.assert_not_called()

session['guest_token'] = 'guest_token_value'
with patch(target, return_value={str(bucket.id)}) as mock_ids:
for action in ['object-read', 'bucket-update', 'object-delete',
'object-delete-version']:
target_obj = bucket if action == 'bucket-update' \
else objects[0]
assert is_guest_login_can_access_file(
permission_factory(target_obj, action))
mock_ids.assert_called_with('guest_token_value')

# Bucket not used by the guest activity
assert not is_guest_login_can_access_file(
permission_factory(other_bucket, 'bucket-update'))
# Action not allowed to guest
assert not is_guest_login_can_access_file(
permission_factory(bucket, 'bucket-read'))

with patch(target, return_value=set()):
assert not is_guest_login_can_access_file(
permission_factory(objects[0], 'object-read'))


# .tox/c1/bin/pytest --cov=invenio_files_rest tests/test_views_objectversion.py::test_put_guest -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio-files-rest/.tox/c1/tmp
@pytest.mark.parametrize('is_guest_bucket, expected', [
(True, 200),
(False, 404),
])
def test_put_guest(client, bucket, is_guest_bucket, expected):
"""Test upload of an object with a guest token."""
object_url = url_for(
'invenio_files_rest.object_api', bucket_id=bucket.id, key='test.txt')
bucket_ids = {str(bucket.id)} if is_guest_bucket else {'other_bucket'}
with client.session_transaction() as sess:
sess['guest_token'] = 'guest_token_value'
with patch('invenio_files_rest.views.get_guest_activity_bucket_ids',
return_value=bucket_ids):
resp = client.put(
object_url,
input_stream=BytesIO(b'guest_content'),
)
assert resp.status_code == expected

2 changes: 2 additions & 0 deletions modules/invenio-iiif/invenio_iiif/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"recid": {
"pid_type": "recid",
"route": "/records/<pid_value>",
"permission_factory_imp":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix no.34, 925, 35

"weko_records_ui.permissions:page_permission_factory",
},

}
Expand Down
14 changes: 8 additions & 6 deletions modules/invenio-iiif/invenio_iiif/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
import tempfile

import pkg_resources
from flask import g
from invenio_files_rest.views import ObjectResource
from flask import abort, g
from invenio_files_rest.models import ObjectVersion

from .permissions import iiif_object_permission_factory

try:
pkg_resources.get_distribution('wand')
from wand.image import Image
Expand All @@ -31,12 +32,13 @@ def protect_api(uuid=None, **kwargs):
"""Retrieve object and check permissions.

Retrieve ObjectVersion of image being requested and check permission
using the Invenio-Files-REST permission factory.
of the record and the file which the object belongs to.
"""
bucket, version_id, key = uuid.split(':', 2)
# skip Invenio-Files-REST permission factory
g.obj = ObjectVersion.get(bucket, key, version_id=version_id)
#g.obj = ObjectResource.get_object(bucket, key, version_id)
obj = ObjectVersion.get(bucket, key, version_id=version_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix no.34, 925, 35

if not obj or not iiif_object_permission_factory(obj).can():
abort(404)
g.obj = obj
return g.obj


Expand Down
2 changes: 2 additions & 0 deletions modules/invenio-iiif/invenio_iiif/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .previewer import can_preview
from .utils import iiif_image_key
from .handlers import image_opener
from .permissions import iiif_object_permission_factory


class IIIFMetadata(dict):
Expand Down Expand Up @@ -103,6 +104,7 @@ def dumps(self):
obj
for obj in ObjectVersion.get_by_bucket(bucket).all()
if can_preview(PreviewFile(None, None, obj))
and iiif_object_permission_factory(obj, record=self.record).can()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix no.35

]

if not images:
Expand Down
65 changes: 65 additions & 0 deletions modules/invenio-iiif/invenio_iiif/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix no.34, 925, 35

#
# This file is part of Invenio.
# Copyright (C) 2018 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Permissions for the IIIF API."""


def _get_record_of_bucket(bucket_id):
"""Get the metadata of the record linked to a bucket."""
from invenio_records.models import RecordMetadata
from invenio_records_files.models import RecordsBuckets

rb = RecordsBuckets.query.filter_by(bucket_id=bucket_id).first()
if not rb:
return None
rm = RecordMetadata.query.filter_by(id=rb.record_id).first()
return rm.json if rm else None


def _get_file_metadata(record, version_id):
"""Get the file metadata of a record by the object version id."""
version_id = str(version_id)
for value in record.values():
if not isinstance(value, dict) or \
value.get('attribute_type') != 'file':
continue
for item in value.get('attribute_value_mlt') or []:
if isinstance(item, dict) and \
item.get('version_id') == version_id:
return item
return None


def iiif_object_permission_factory(obj, record=None):
"""Permission factory for reading an object through the IIIF API.

When the object is a file of a record, the permissions of the record and
of the file are checked. Otherwise the Invenio-Files-REST permission
factory is used.

:param obj: A :class:`invenio_files_rest.models.ObjectVersion` instance.
:param record: The metadata of the record owning the object. It is looked
up from the bucket of the object when not given.
"""
def can(self):
from invenio_files_rest.proxies import current_permission_factory
from weko_records_ui.permissions import \
check_file_download_permission, page_permission_factory

record_json = record
if record_json is None:
record_json = _get_record_of_bucket(obj.bucket_id)
if record_json:
fjson = _get_file_metadata(record_json, obj.version_id)
if fjson is not None:
return bool(
page_permission_factory(record_json).can()
and check_file_download_permission(record_json, fjson))
return bool(current_permission_factory(obj, 'object-read').can())

return type('IIIFObjectPermissionChecker', (), {'can': can})()
6 changes: 6 additions & 0 deletions modules/invenio-iiif/invenio_iiif/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@
from __future__ import absolute_import, print_function

from celery import shared_task
from flask import g
from flask_iiif.restful import IIIFImageAPI
from invenio_files_rest.models import ObjectVersion


@shared_task(ignore_result=True)
def create_thumbnail(uuid, thumbnail_width):
"""Create the thumbnail for an image."""
# 利用者のリクエストではない内部処理なので、利用者の権限は確かめずに

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix no.34, 925, 35

# 対象を解決しておく(image_opener は g.obj があればそれを使う)。
bucket, version_id, key = uuid.split(':', 2)
g.obj = ObjectVersion.get(bucket, key, version_id=version_id)
# size = '!' + thumbnail_width + ','
size = thumbnail_width + ',' # flask_iiif doesn't support ! at the moment
region = "full"
Expand Down
6 changes: 5 additions & 1 deletion modules/invenio-iiif/invenio_iiif/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from functools import partial

from flask import Blueprint, abort, current_app, redirect, url_for
from flask_login import current_user
from invenio_pidstore.errors import (
PIDDeletedError,
PIDDoesNotExistError,
Expand Down Expand Up @@ -157,7 +158,10 @@ def manifest_view(
)
abort(500)

# TODO Check permissions
if permission_factory and not permission_factory(record).can():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix no.35

if current_user.is_authenticated:
abort(403)
abort(401)

manifest = manifest_class(record)
data = manifest.dumps()
Expand Down
Loading
Loading