From bc50425c41b2b98d031205a79540792b589e6c3a Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Sun, 27 Sep 2026 21:36:29 +0000 Subject: [PATCH 1/7] =?UTF-8?q?fix(weko-records-ui):=20=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E6=A8=A9=E9=99=90=E3=81=AE=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E3=81=A7=E3=82=B3=E3=83=9F=E3=83=A5=E3=83=8B=E3=83=86=E3=82=A3?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E8=80=85=E3=81=AE=E7=AE=A1=E8=BD=84=E3=82=92?= =?UTF-8?q?=E7=A2=BA=E8=AA=8D=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ファイルのダウンロード可否と所有者・管理者判定で、Community Administrator を check_created_id と同じく has_comadmin_permission により自コミュニティ 配下のアイテムに限定する。System / Repository Administrator は従来どおり。 判定を is_superuser_or_record_comadmin にまとめ、 check_file_download_permission と is_owners_or_superusers から使う。 Co-Authored-By: Claude Opus 5.5 --- .../weko-records-ui/tests/test_permissions.py | 52 ++++++++++++++++++- .../weko_records_ui/permissions.py | 43 +++++++++------ 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/modules/weko-records-ui/tests/test_permissions.py b/modules/weko-records-ui/tests/test_permissions.py index 26938e1e5c..e236a6850b 100644 --- a/modules/weko-records-ui/tests/test_permissions.py +++ b/modules/weko-records-ui/tests/test_permissions.py @@ -324,6 +324,50 @@ def test_check_file_download_permission(app, records, users, db_file_permission, with patch("flask_login.utils._get_user", return_value=users[4]["obj"]): assert check_file_download_permission(record, fjson, False) == False +# .tox/c1/bin/pytest --cov=weko_records_ui tests/test_permissions.py::test_check_file_download_permission_comadmin -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-records-ui/.tox/c1/tmp +@pytest.mark.parametrize( + "accessrole, is_display_file_info", + [ + ("open_no", False), + ("open_no", True), + ("open_restricted", False), + ], +) +def test_check_file_download_permission_comadmin( + app, records, users, db_file_permission, accessrole, is_display_file_info): + indexer, results = records + record = results[0]["record"] + record['_deposit']['created_by'] = 1 + record['owner'] = '1' + record['weko_shared_ids'] = [] + fjson = {'url': {'url': 'https://weko3.example.org/record/11/files/001.jpg'}, + 'date': [{'dateType': 'Available', 'dateValue': '2022-09-27'}], 'format': 'image/jpeg', + 'filename': 'helloworld.pdf', 'filesize': [{'value': '2.7 MB'}], 'accessrole': accessrole, + 'version_id': 'd73bd9cb-aa9e-4cd0-bf07-c5976d40bdde', 'displaytype': 'preview', + 'is_thumbnail': False, 'future_date_message': '', 'download_preview_message': '', 'size': 2700000.0, + 'mimetype': 'image/jpeg', 'file_order': 0} + + with patch("weko_records_ui.permissions.check_site_license_permission", return_value=False), \ + patch("weko_records_ui.permissions.check_open_restricted_permission", return_value=False): + # comadmin: record under the user's community + with patch("flask_login.utils._get_user", return_value=users[3]["obj"]): + with patch("weko_records_ui.permissions.has_comadmin_permission", return_value=True) as mock_comadmin: + assert check_file_download_permission(record, fjson, is_display_file_info) == True + mock_comadmin.assert_called_with(record) + + # comadmin: record outside the user's communities + with patch("flask_login.utils._get_user", return_value=users[3]["obj"]): + with patch("weko_records_ui.permissions.has_comadmin_permission", return_value=False): + assert check_file_download_permission(record, fjson, is_display_file_info) == False + + # repoadmin / sysadmin: always allowed, community is not consulted + for user in (users[1], users[2]): + with patch("flask_login.utils._get_user", return_value=user["obj"]): + with patch("weko_records_ui.permissions.has_comadmin_permission", return_value=False) as mock_comadmin: + assert check_file_download_permission(record, fjson, is_display_file_info) == True + mock_comadmin.assert_not_called() + + # def check_open_restricted_permission(record, fjson): # .tox/c1/bin/pytest --cov=weko_records_ui tests/test_permissions.py::test_check_open_restricted_permission -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-records-ui/.tox/c1/tmp def test_check_open_restricted_permission(app, records, users,db_file_permission,mocker): @@ -1270,9 +1314,13 @@ def test_is_owners_or_superusers(app,records,users): # sysadmin with patch("flask_login.utils._get_user", return_value=users[2]["obj"]): assert is_owners_or_superusers(testrec) - # comadmin + # comadmin: only for records under the user's communities with patch("flask_login.utils._get_user", return_value=users[3]["obj"]): - assert is_owners_or_superusers(testrec) + with patch("weko_records_ui.permissions.has_comadmin_permission", return_value=True) as mock_comadmin: + assert is_owners_or_superusers(testrec) + mock_comadmin.assert_called_once_with(testrec) + with patch("weko_records_ui.permissions.has_comadmin_permission", return_value=False): + assert not is_owners_or_superusers(testrec) # def __isint(str): -> bool: diff --git a/modules/weko-records-ui/weko_records_ui/permissions.py b/modules/weko-records-ui/weko_records_ui/permissions.py index d38110827d..0806e06d9d 100644 --- a/modules/weko-records-ui/weko_records_ui/permissions.py +++ b/modules/weko-records-ui/weko_records_ui/permissions.py @@ -135,12 +135,7 @@ def __check_user_permission(user_id_list): is_ok = True # Check super users else: - super_users = current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER'] + \ - current_app.config['WEKO_PERMISSION_ROLE_COMMUNITY'] - for role in list(current_user.roles or []): - if role.name in super_users: - is_ok = True - break + is_ok = is_superuser_or_record_comadmin(record) return is_ok if fjson: @@ -170,11 +165,8 @@ def __check_user_permission(user_id_list): return is_can # Super users - supers = current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER'] + \ - current_app.config['WEKO_PERMISSION_ROLE_COMMUNITY'] - for role in list(current_user.roles or []): - if role.name in supers: - return is_can + if is_superuser_or_record_comadmin(record): + return is_can try: from .utils import is_future @@ -723,12 +715,31 @@ def is_owners_or_superusers(record) -> bool: return True # Super users - supers = current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER'] + \ - current_app.config['WEKO_PERMISSION_ROLE_COMMUNITY'] - for role in list(current_user.roles or []): - if role.name in supers: - return True + return is_superuser_or_record_comadmin(record) + +def is_superuser_or_record_comadmin(record) -> bool: + """Check whether the current user administers the record. + + System and Repository Administrators administer every record. + A Community Administrator administers only the records placed under + the indexes of the communities the user belongs to + (see :func:`has_comadmin_permission`), as in :func:`check_created_id`. + + Args: + record (dict): the record metadata. + + Returns: + bool: True if the current user is a super user, or a Community + Administrator of a community that the record belongs to. + """ + supers = current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER'] + comadmin = current_app.config['WEKO_PERMISSION_ROLE_COMMUNITY'] + role_names = [role.name for role in list(current_user.roles or [])] + if any(name in supers for name in role_names): + return True + if any(name in comadmin for name in role_names): + return has_comadmin_permission(record) return False From 6aa5c50aa17088b556d2ef999b6fb8bb20a6494c Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Sun, 27 Sep 2026 21:38:06 +0000 Subject: [PATCH 2/7] =?UTF-8?q?fix(weko-records-ui):=20=E3=83=97=E3=83=AC?= =?UTF-8?q?=E3=83=93=E3=83=A5=E3=83=BC=E3=81=A7=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E6=A8=A9=E9=99=90=E3=82=92=E7=A2=BA=E8=AA=8D?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ファイル単位の権限を要求するデコレータ file_permission_required を追加し、 preview に付ける。判定は file_ui と同じく file_permission_factory を使う。 権限がない場合、未ログインならログイン画面へ誘導し、ログイン済みなら 403 を返す。 Co-Authored-By: Claude Opus 5.5 --- modules/weko-records-ui/tests/test_preview.py | 47 +++++++++++++++++++ .../weko_records_ui/permissions.py | 28 +++++++++++ .../weko_records_ui/preview.py | 3 ++ 3 files changed, 78 insertions(+) diff --git a/modules/weko-records-ui/tests/test_preview.py b/modules/weko-records-ui/tests/test_preview.py index 5e93cacf72..71feaa7d93 100644 --- a/modules/weko-records-ui/tests/test_preview.py +++ b/modules/weko-records-ui/tests/test_preview.py @@ -86,6 +86,53 @@ def view1(parameter0): assert preview(record.pid,record,template)=="" +# .tox/c1/bin/pytest --cov=weko_records_ui tests/test_preview.py::test_preview_file_permission -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-records-ui/.tox/c1/tmp +def test_preview_file_permission(app, records, users): + @app.route('/record//preview_permission_test/') + def view_preview_permission_test(pid_value, filename): + return '' + + from werkzeug.exceptions import Forbidden + + indexer, results = records + # record 5 has a single file whose accessrole is open_no + record = results[4]['record'] + recid = results[4]['recid'] + filename = 'helloworld.pdf' + template = 'invenio_records_ui/detail.html' + url = '/record/{}/preview_permission_test/{}'.format(recid.pid_value, filename) + + # guest user: redirected to login + with app.test_request_context(url): + with patch('weko_accounts.views._redirect_method', return_value='redirect') as mock_redirect: + assert preview(record.pid, record, template) == 'redirect' + mock_redirect.assert_called_once_with(has_next=True) + + # logged in user without file permission + with app.test_request_context(url): + with patch('flask_login.utils._get_user', return_value=users[4]['obj']): + with pytest.raises(Forbidden): + preview(record.pid, record, template) + + # owner and sysadmin can preview + for user in (users[7], users[2]): + with app.test_request_context(url): + with patch('flask_login.utils._get_user', return_value=user['obj']): + assert "Preview" in preview(record.pid, record, template) + + # file permission is checked with the requested file + with app.test_request_context(url): + checker = MagicMock() + checker.can.return_value = False + with patch('flask_login.utils._get_user', return_value=users[2]['obj']): + with patch('weko_records_ui.permissions.file_permission_factory', return_value=checker) as mock_factory: + with pytest.raises(Forbidden): + preview(record.pid, record, template) + args, kwargs = mock_factory.call_args + assert args[0] == record + assert kwargs['fjson'].get('filename') == filename + + # def children_to_list(node): def test_children_to_list(app): obj1 = MagicMock() diff --git a/modules/weko-records-ui/weko_records_ui/permissions.py b/modules/weko-records-ui/weko_records_ui/permissions.py index 0806e06d9d..c935de8471 100644 --- a/modules/weko-records-ui/weko_records_ui/permissions.py +++ b/modules/weko-records-ui/weko_records_ui/permissions.py @@ -99,6 +99,34 @@ def can(self): return type('FileDownLoadPermissionChecker', (), {'can': can})() +def file_permission_required(f): + """Require the file permission on the file requested to a record view. + + For views registered in ``RECORDS_UI_ENDPOINTS`` whose signature is + ``view(pid, record, **kwargs)`` and whose route has ````. + The file is resolved in the same way as the previewer does, and checked + with :func:`file_permission_factory`, as ``file_ui`` does. + + If the user may not access the file, a guest user is redirected to the + login page and a logged in user gets 403. A missing file is left to the + view (which returns 404). + """ + @wraps(f) + def decorated(pid, record, *args, **kwargs): + from invenio_previewer.proxies import current_previewer + fileobj = current_previewer.record_file_factory( + pid, record, request.view_args.get( + 'filename', request.args.get('filename', type=str)) + ) + if fileobj and not file_permission_factory(record, fjson=fileobj).can(): + if not current_user.is_authenticated: + from weko_accounts.views import _redirect_method + return _redirect_method(has_next=True) + abort(403) + return f(pid, record, *args, **kwargs) + return decorated + + def check_file_download_permission(record, fjson, is_display_file_info=False, item_type=None): """Check file download.""" def site_license_check(item_type): diff --git a/modules/weko-records-ui/weko_records_ui/preview.py b/modules/weko-records-ui/weko_records_ui/preview.py index 942b9483d8..e04b622311 100644 --- a/modules/weko-records-ui/weko_records_ui/preview.py +++ b/modules/weko-records-ui/weko_records_ui/preview.py @@ -27,7 +27,10 @@ from invenio_previewer.extensions.zip import make_tree from invenio_previewer.proxies import current_previewer +from .permissions import file_permission_required + +@file_permission_required def preview(pid, record, template=None, **kwargs): """Preview file for given record. From 67bb31ee7e61766e8f6575e5c4fa26d664970ede Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Sun, 27 Sep 2026 21:45:47 +0000 Subject: [PATCH 3/7] =?UTF-8?q?fix(invenio-files-rest):=20=E3=82=B2?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=E3=81=A7?= =?UTF-8?q?=E3=81=AE=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E3=82=92=E5=BD=93=E8=A9=B2=E3=82=A2=E3=82=AF=E3=83=86=E3=82=A3?= =?UTF-8?q?=E3=83=93=E3=83=86=E3=82=A3=E3=81=AE=E3=83=90=E3=82=B1=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=81=AB=E9=99=90=E5=AE=9A=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ゲストトークンを持つセッションに許可するファイル操作を、トークンに対応する ゲストアクティビティのアイテム(およびそのルートバージョン)に紐づくバケットに 限定する。 Co-Authored-By: Claude Opus 5.5 --- .../invenio_files_rest/permissions.py | 39 +++++++++ .../invenio_files_rest/views.py | 10 ++- .../tests/test_permissions.py | 82 +++++++++++++++++++ .../tests/test_views_objectversion.py | 64 +++++++++++++++ 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 modules/invenio-files-rest/tests/test_permissions.py diff --git a/modules/invenio-files-rest/invenio_files_rest/permissions.py b/modules/invenio-files-rest/invenio_files_rest/permissions.py index 9da908d79b..678bde99c1 100644 --- a/modules/invenio-files-rest/invenio_files_rest/permissions.py +++ b/modules/invenio-files-rest/invenio_files_rest/permissions.py @@ -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): + """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} diff --git a/modules/invenio-files-rest/invenio_files_rest/views.py b/modules/invenio-files-rest/invenio_files_rest/views.py index 854c597a1a..336a906e74 100644 --- a/modules/invenio-files-rest/invenio_files_rest/views.py +++ b/modules/invenio-files-rest/invenio_files_rest/views.py @@ -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 @@ -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. @@ -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: + bucket_ids = get_guest_activity_bucket_ids( + session.get('guest_token')) + if getattr(need, 'argument', None) in bucket_ids: + return True return False diff --git a/modules/invenio-files-rest/tests/test_permissions.py b/modules/invenio-files-rest/tests/test_permissions.py new file mode 100644 index 0000000000..5e035915b2 --- /dev/null +++ b/modules/invenio-files-rest/tests/test_permissions.py @@ -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])} diff --git a/modules/invenio-files-rest/tests/test_views_objectversion.py b/modules/invenio-files-rest/tests/test_views_objectversion.py index e57c90d9ab..cff0cc8a50 100644 --- a/modules/invenio-files-rest/tests/test_views_objectversion.py +++ b/modules/invenio-files-rest/tests/test_views_objectversion.py @@ -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 + From 3768aef924b8163c8c9f7582db7597c9c6c17972 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Sun, 27 Sep 2026 21:46:10 +0000 Subject: [PATCH 4/7] =?UTF-8?q?fix(invenio-iiif):=20=E7=94=BB=E5=83=8F?= =?UTF-8?q?=E9=85=8D=E4=BF=A1=E3=81=A7=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E6=A8=A9=E9=99=90=E5=88=A4=E5=AE=9A=E3=82=92=E8=A1=8C?= =?UTF-8?q?=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IIIF の画像・画像情報の取得時に、対象ファイルが属するレコードの閲覧権限と ファイルのダウンロード権限を weko-records-ui の既存判定で確認する。 レコードのファイルでないオブジェクトは Invenio-Files-REST の権限で判定する。 Co-Authored-By: Claude Opus 5.5 --- modules/invenio-iiif/invenio_iiif/handlers.py | 14 +- .../invenio-iiif/invenio_iiif/permissions.py | 65 +++++++ modules/invenio-iiif/tests/test_handlers.py | 49 +++++- .../invenio-iiif/tests/test_permissions.py | 158 ++++++++++++++++++ modules/invenio-iiif/tests/test_views.py | 37 +++- 5 files changed, 308 insertions(+), 15 deletions(-) create mode 100644 modules/invenio-iiif/invenio_iiif/permissions.py create mode 100644 modules/invenio-iiif/tests/test_permissions.py diff --git a/modules/invenio-iiif/invenio_iiif/handlers.py b/modules/invenio-iiif/invenio_iiif/handlers.py index 2333551c81..6f0e8ee4c2 100644 --- a/modules/invenio-iiif/invenio_iiif/handlers.py +++ b/modules/invenio-iiif/invenio_iiif/handlers.py @@ -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 @@ -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) + if not obj or not iiif_object_permission_factory(obj).can(): + abort(404) + g.obj = obj return g.obj diff --git a/modules/invenio-iiif/invenio_iiif/permissions.py b/modules/invenio-iiif/invenio_iiif/permissions.py new file mode 100644 index 0000000000..162593c1cf --- /dev/null +++ b/modules/invenio-iiif/invenio_iiif/permissions.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +# 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})() diff --git a/modules/invenio-iiif/tests/test_handlers.py b/modules/invenio-iiif/tests/test_handlers.py index f90008cf44..a4ed1c5cdb 100644 --- a/modules/invenio-iiif/tests/test_handlers.py +++ b/modules/invenio-iiif/tests/test_handlers.py @@ -1,25 +1,65 @@ +import pytest +from mock import MagicMock +from werkzeug.exceptions import HTTPException + from invenio_files_rest.models import Bucket, ObjectVersion,FileInstance from invenio_iiif.handlers import protect_api, image_opener + +def _patch_permission(mocker, can): + return mocker.patch( + "invenio_iiif.handlers.iiif_object_permission_factory", + return_value=MagicMock(can=MagicMock(return_value=can))) + + # def protect_api(uuid=None, **kwargs) # .tox/c1/bin/pytest --cov=invenio_iiif tests/test_handlers.py::test_protect_api -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp -def test_protect_api(db,location): +def test_protect_api(db,location,mocker): bucket = Bucket.create() obj = ObjectVersion.create(bucket,"test.txt") db.session.commit() version_id = obj.version_id key = obj.key - + id = "{}:{}:{}".format(bucket.id,version_id,key) + mock_permission = _patch_permission(mocker, True) result = protect_api(id) assert result == obj + mock_permission.assert_called_with(obj) + + +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_handlers.py::test_protect_api_no_permission -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp +def test_protect_api_no_permission(db,location,mocker): + bucket = Bucket.create() + obj = ObjectVersion.create(bucket,"test.txt") + db.session.commit() + + id = "{}:{}:{}".format(bucket.id,obj.version_id,obj.key) + _patch_permission(mocker, False) + with pytest.raises(HTTPException) as httperror: + protect_api(id) + assert httperror.value.code == 404 + + +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_handlers.py::test_protect_api_not_found -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp +def test_protect_api_not_found(db,location,mocker): + bucket = Bucket.create() + obj = ObjectVersion.create(bucket,"test.txt") + db.session.commit() + + id = "{}:{}:{}".format(bucket.id,obj.version_id,"not_exist.txt") + mock_permission = _patch_permission(mocker, True) + with pytest.raises(HTTPException) as httperror: + protect_api(id) + assert httperror.value.code == 404 + mock_permission.assert_not_called() # def image_opener(key): # .tox/c1/bin/pytest --cov=invenio_iiif tests/test_handlers.py::test_image_opener -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp -def test_image_opener(db, location): +def test_image_opener(db, location, mocker): bucket = Bucket.create() obj = ObjectVersion.create(bucket,"test.txt") db.session.commit() @@ -35,5 +75,6 @@ def test_image_opener(db, location): key = obj.key id = "{}:{}:{}".format(bucket.id,version_id,key) + _patch_permission(mocker, True) result = image_opener(id) - assert result.read() == b"" \ No newline at end of file + assert result.read() == b"" diff --git a/modules/invenio-iiif/tests/test_permissions.py b/modules/invenio-iiif/tests/test_permissions.py new file mode 100644 index 0000000000..58aaa9e5df --- /dev/null +++ b/modules/invenio-iiif/tests/test_permissions.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# +# 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. + +"""Test of IIIF permissions.""" + +from mock import MagicMock +from invenio_files_rest.models import Bucket, ObjectVersion +from invenio_records.api import Record +from invenio_records_files.models import RecordsBuckets + +from invenio_iiif.permissions import ( + _get_file_metadata, + _get_record_of_bucket, + iiif_object_permission_factory, +) + +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_permissions.py -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp + + +def _record_json(version_id): + return { + "recid": "1", + "item_1": { + "attribute_name": "Title", + "attribute_value_mlt": [{"subitem_title": "title"}], + }, + "item_2": { + "attribute_name": "File", + "attribute_type": "file", + "attribute_value_mlt": [ + {"filename": "other.png", "version_id": "other"}, + { + "filename": "image.png", + "version_id": str(version_id), + "accessrole": "open_access", + }, + ], + }, + } + + +def _patch_record_permissions(mocker, page=True, file=True): + page_factory = mocker.patch( + "weko_records_ui.permissions.page_permission_factory", + return_value=MagicMock(can=MagicMock(return_value=page)), + ) + file_check = mocker.patch( + "weko_records_ui.permissions.check_file_download_permission", + return_value=file, + ) + return page_factory, file_check + + +def _patch_files_rest_permission(mocker, can): + return mocker.patch( + "invenio_files_rest.proxies.current_permission_factory", + MagicMock(return_value=MagicMock(can=MagicMock(return_value=can))), + ) + + +def _create_object(db): + bucket = Bucket.create() + obj = ObjectVersion.create(bucket, "image.png") + db.session.commit() + return bucket, obj + + +# def _get_file_metadata(record, version_id): +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_permissions.py::test_get_file_metadata -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp +def test_get_file_metadata(): + record = _record_json("v1") + assert _get_file_metadata(record, "v1")["filename"] == "image.png" + assert _get_file_metadata(record, "v2") is None + assert _get_file_metadata({"item_1": "value"}, "v1") is None + + +# def _get_record_of_bucket(bucket_id): +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_permissions.py::test_get_record_of_bucket -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp +def test_get_record_of_bucket(app, db, location): + bucket, obj = _create_object(db) + assert _get_record_of_bucket(bucket.id) is None + + record = Record.create(_record_json(obj.version_id)) + RecordsBuckets.create(record=record.model, bucket=bucket) + db.session.commit() + result = _get_record_of_bucket(bucket.id) + assert result["recid"] == "1" + + +# def iiif_object_permission_factory(obj, record=None): +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_permissions.py::test_iiif_object_permission_factory_record_file -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp +def test_iiif_object_permission_factory_record_file(app, db, location, mocker): + bucket, obj = _create_object(db) + record = Record.create(_record_json(obj.version_id)) + RecordsBuckets.create(record=record.model, bucket=bucket) + db.session.commit() + # The files-rest permission is not used for a file of a record. + _patch_files_rest_permission(mocker, True) + + page_factory, file_check = _patch_record_permissions(mocker, True, True) + with app.test_request_context(): + assert iiif_object_permission_factory(obj).can() is True + assert page_factory.call_args[0][0]["recid"] == "1" + assert file_check.call_args[0][1]["filename"] == "image.png" + + _patch_record_permissions(mocker, page=False, file=True) + with app.test_request_context(): + assert iiif_object_permission_factory(obj).can() is False + + _patch_record_permissions(mocker, page=True, file=False) + with app.test_request_context(): + assert iiif_object_permission_factory(obj).can() is False + + +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_permissions.py::test_iiif_object_permission_factory_given_record -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp +def test_iiif_object_permission_factory_given_record(app, db, location, mocker): + bucket, obj = _create_object(db) + record = _record_json(obj.version_id) + _patch_files_rest_permission(mocker, True) + + _patch_record_permissions(mocker, True, True) + with app.test_request_context(): + assert iiif_object_permission_factory(obj, record=record).can() is True + + _patch_record_permissions(mocker, True, False) + with app.test_request_context(): + assert iiif_object_permission_factory(obj, record=record).can() is False + + +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_permissions.py::test_iiif_object_permission_factory_not_record_file -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp +def test_iiif_object_permission_factory_not_record_file(app, db, location, mocker): + bucket, obj = _create_object(db) + page_factory, file_check = _patch_record_permissions(mocker, True, True) + + # No record is linked to the bucket. + files_rest = _patch_files_rest_permission(mocker, False) + with app.test_request_context(): + assert iiif_object_permission_factory(obj).can() is False + files_rest.assert_called_with(obj, "object-read") + + files_rest = _patch_files_rest_permission(mocker, True) + with app.test_request_context(): + assert iiif_object_permission_factory(obj).can() is True + + # The object is not a file of the record. + record = Record.create(_record_json("other-version")) + RecordsBuckets.create(record=record.model, bucket=bucket) + db.session.commit() + files_rest = _patch_files_rest_permission(mocker, False) + with app.test_request_context(): + assert iiif_object_permission_factory(obj).can() is False + page_factory.assert_not_called() + file_check.assert_not_called() diff --git a/modules/invenio-iiif/tests/test_views.py b/modules/invenio-iiif/tests/test_views.py index 2ab39fb22d..3320cf0cca 100644 --- a/modules/invenio-iiif/tests/test_views.py +++ b/modules/invenio-iiif/tests/test_views.py @@ -11,7 +11,7 @@ from __future__ import absolute_import, print_function import pytest -from mock import patch +from mock import MagicMock, patch from werkzeug.exceptions import HTTPException from flask import url_for,make_response from flask_iiif.utils import iiif_image_url @@ -27,22 +27,51 @@ # .tox/c1/bin/pytest --cov=invenio_iiif tests/test_views.py -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp -def test_get_image(client, image_object, image_uuid): +def _patch_object_permission(mocker, can): + return mocker.patch( + "invenio_iiif.handlers.iiif_object_permission_factory", + return_value=MagicMock(can=MagicMock(return_value=can))) + + +def test_get_image(client, image_object, image_uuid, mocker): """Test retrieval of image.""" #with pytest.raises(AttributeError): + _patch_object_permission(mocker, True) res = client.get(iiif_image_url(uuid=image_uuid, size='200,200')) assert res.status_code == 200 assert res.content_type == 'image/png' -def test_image_info(client, image_object, image_uuid): +def test_get_image_no_permission(client, image_object, image_uuid, mocker): + """Test retrieval of image without permission.""" + _patch_object_permission(mocker, False) + res = client.get(iiif_image_url(uuid=image_uuid, size='200,200')) + assert res.status_code == 404 + + +def test_get_image_default_permission(client, image_object, image_uuid): + """Test retrieval of image not linked to a record by anonymous user.""" + res = client.get(iiif_image_url(uuid=image_uuid, size='200,200')) + assert res.status_code == 404 + + +def test_image_info(client, image_object, image_uuid, mocker): """Test retrieval of image info.""" + _patch_object_permission(mocker, True) res = client.get( url_for('iiifimageinfo', version='v2', uuid=image_uuid)) assert res.status_code == 200 assert res.content_type == 'application/json' +def test_image_info_no_permission(client, image_object, image_uuid, mocker): + """Test retrieval of image info without permission.""" + _patch_object_permission(mocker, False) + res = client.get( + url_for('iiifimageinfo', version='v2', uuid=image_uuid)) + assert res.status_code == 404 + + def test_get_restricted_image(client, image_object, image_uuid): """Test retrieval of image.""" image_url = iiif_image_url(uuid=image_uuid, size='200,200') @@ -160,5 +189,3 @@ def test_manifest_view(app,records,mocker): result = manifest_view(pid_value,resolver,permission_factory,manifest_class) assert result.status_code == 204 - - \ No newline at end of file From e90be8ec0afdd3a1cc07a6310207b6aa4eaab505 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Sun, 27 Sep 2026 21:46:17 +0000 Subject: [PATCH 5/7] =?UTF-8?q?fix(invenio-iiif):=20=E3=83=9E=E3=83=8B?= =?UTF-8?q?=E3=83=95=E3=82=A7=E3=82=B9=E3=83=88=E3=81=A7=E3=83=AC=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=A8=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E6=A8=A9=E9=99=90=E5=88=A4=E5=AE=9A=E3=82=92=E8=A1=8C?= =?UTF-8?q?=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit マニフェストのエンドポイントに weko-records-ui のレコード閲覧権限を permission factory として設定し、マニフェストに含める画像も ファイルの権限があるものに限定する。 Co-Authored-By: Claude Opus 5.5 --- modules/invenio-iiif/invenio_iiif/config.py | 2 + modules/invenio-iiif/invenio_iiif/manifest.py | 2 + modules/invenio-iiif/invenio_iiif/views.py | 6 ++- modules/invenio-iiif/tests/test_manifest.py | 42 ++++++++++++++++++- modules/invenio-iiif/tests/test_views.py | 40 ++++++++++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/modules/invenio-iiif/invenio_iiif/config.py b/modules/invenio-iiif/invenio_iiif/config.py index f23f02daea..6f0524570e 100644 --- a/modules/invenio-iiif/invenio_iiif/config.py +++ b/modules/invenio-iiif/invenio_iiif/config.py @@ -30,6 +30,8 @@ "recid": { "pid_type": "recid", "route": "/records/", + "permission_factory_imp": + "weko_records_ui.permissions:page_permission_factory", }, } diff --git a/modules/invenio-iiif/invenio_iiif/manifest.py b/modules/invenio-iiif/invenio_iiif/manifest.py index e4f992c168..92028a8549 100644 --- a/modules/invenio-iiif/invenio_iiif/manifest.py +++ b/modules/invenio-iiif/invenio_iiif/manifest.py @@ -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): @@ -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() ] if not images: diff --git a/modules/invenio-iiif/invenio_iiif/views.py b/modules/invenio-iiif/invenio_iiif/views.py index 0ee8f1e64e..bd6036b0d8 100644 --- a/modules/invenio-iiif/invenio_iiif/views.py +++ b/modules/invenio-iiif/invenio_iiif/views.py @@ -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, @@ -157,7 +158,10 @@ def manifest_view( ) abort(500) - # TODO Check permissions + if permission_factory and not permission_factory(record).can(): + if current_user.is_authenticated: + abort(403) + abort(401) manifest = manifest_class(record) data = manifest.dumps() diff --git a/modules/invenio-iiif/tests/test_manifest.py b/modules/invenio-iiif/tests/test_manifest.py index 5cd183c7d8..f929c939be 100644 --- a/modules/invenio-iiif/tests/test_manifest.py +++ b/modules/invenio-iiif/tests/test_manifest.py @@ -1,7 +1,9 @@ # .tox/c1/bin/pytest --cov=invenio_iiif tests/test_manifest.py -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp -from invenio_iiif.manifest import IIIFMetadata,IIIFManifest +from mock import MagicMock + +from invenio_iiif.manifest import IIIFMetadata,IIIFManifest,Image # class IIIFMetadata(dict): # .tox/c1/bin/pytest --cov=invenio_iiif tests/test_manifest.py::TestIIIFMetadata -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp @@ -43,6 +45,44 @@ def test_dumps(self,app,records): result = obj.dumps() assert result == {} +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_manifest.py::TestIIIFManifest::test_dumps_permission -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp + def test_dumps_permission(self,app,records,mocker): + record = records[0][2] + allowed = MagicMock(key="allowed.png") + denied = MagicMock(key="denied.png") + mock_query = MagicMock() + mock_query.all.return_value = [allowed, denied] + mocker.patch("invenio_iiif.manifest.ObjectVersion.get_by_bucket", + return_value=mock_query) + mocker.patch("invenio_iiif.manifest.can_preview", return_value=True) + mocker.patch("invenio_iiif.manifest.PreviewFile") + mock_permission = mocker.patch( + "invenio_iiif.manifest.iiif_object_permission_factory", + side_effect=lambda obj, record=None: MagicMock( + can=MagicMock(return_value=obj is allowed))) + mock_key = mocker.patch("invenio_iiif.manifest.iiif_image_key", + side_effect=lambda obj: "bucket:version:" + obj.key) + + def set_hw(image): + image.height = 10 + image.width = 10 + mocker.patch.object(Image, "set_hw_from_iiif", autospec=True, + side_effect=set_hw) + with app.test_request_context("/test"): + obj = IIIFManifest(record) + obj.manifest.toJSON = MagicMock(return_value={"test": "value"}) + result = obj.dumps() + assert result == {"test": "value"} + mock_key.assert_called_once_with(allowed) + assert mock_permission.call_args[1]["record"] == record + + # no permitted image + mocker.patch( + "invenio_iiif.manifest.iiif_object_permission_factory", + return_value=MagicMock(can=MagicMock(return_value=False))) + obj = IIIFManifest(record) + assert obj.dumps() == {} + # class ManifestFactory(PrezyManifestFactory): # def image(self, ident, label="", iiif=False, region='full', size='full'): # class Image(PreziImage): diff --git a/modules/invenio-iiif/tests/test_views.py b/modules/invenio-iiif/tests/test_views.py index 3320cf0cca..e78a0516cd 100644 --- a/modules/invenio-iiif/tests/test_views.py +++ b/modules/invenio-iiif/tests/test_views.py @@ -189,3 +189,43 @@ def test_manifest_view(app,records,mocker): result = manifest_view(pid_value,resolver,permission_factory,manifest_class) assert result.status_code == 204 + + +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_views.py::test_manifest_view_permission -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp +def test_manifest_view_permission(app,records,mocker): + from invenio_pidstore.resolver import Resolver + from invenio_iiif.manifest import IIIFManifest + from invenio_records.api import Record + pid_value=records[0][0].pid_value + resolver=Resolver(pid_type="recid",object_type="rec",getter=Record.get_record) + manifest_class=IIIFManifest + + def factory(can): + return MagicMock(return_value=MagicMock(can=MagicMock(return_value=can))) + + with app.test_request_context("/test"): + # allowed + permission_factory = factory(True) + result = manifest_view(pid_value,resolver,permission_factory,manifest_class) + assert result.status_code == 204 + assert permission_factory.call_args[0][0]["recid"] == records[0][2]["recid"] + + # denied for anonymous user + mock_user = mocker.patch("invenio_iiif.views.current_user") + mock_user.is_authenticated = False + with pytest.raises(HTTPException) as httperror: + manifest_view(pid_value,resolver,factory(False),manifest_class) + assert httperror.value.code == 401 + + # denied for authenticated user + mock_user.is_authenticated = True + with pytest.raises(HTTPException) as httperror: + manifest_view(pid_value,resolver,factory(False),manifest_class) + assert httperror.value.code == 403 + + +# .tox/c1/bin/pytest --cov=invenio_iiif tests/test_views.py::test_manifest_endpoint_permission_config -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio_iiif/.tox/c1/tmp +def test_manifest_endpoint_permission_config(): + from invenio_iiif.config import IIIF_MANIFEST_ENDPOINTS + assert IIIF_MANIFEST_ENDPOINTS["recid"]["permission_factory_imp"] == \ + "weko_records_ui.permissions:page_permission_factory" From 97e5e4cf33303ff37e56daec5d25ff2c2f5e65fb Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Mon, 28 Sep 2026 17:22:18 +0000 Subject: [PATCH 6/7] =?UTF-8?q?test(weko-records-ui):=20=E3=83=97=E3=83=AC?= =?UTF-8?q?=E3=83=93=E3=83=A5=E3=83=BC=E3=81=AE=E6=A8=A9=E9=99=90=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=A7=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E3=83=A1=E3=82=BF=E3=83=87=E3=83=BC=E3=82=BF=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E4=BD=93=E3=81=AB=E6=9B=B8=E3=81=8D=E8=BE=BC=E3=82=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 権限判定はファイル実体の JSON にある accessrole を見る。アイテム登録時は メタデータがそこへ書き込まれるが、テストのフィクスチャは書き込んでいない ため、判定が常に許可になりテストが成立していなかった。登録時と同じく メタデータを書き込んでから判定させる。 Co-Authored-By: Claude Opus 5.5 --- modules/weko-records-ui/tests/test_preview.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/weko-records-ui/tests/test_preview.py b/modules/weko-records-ui/tests/test_preview.py index 71feaa7d93..a0ab881fd1 100644 --- a/modules/weko-records-ui/tests/test_preview.py +++ b/modules/weko-records-ui/tests/test_preview.py @@ -87,7 +87,7 @@ def view1(parameter0): # .tox/c1/bin/pytest --cov=weko_records_ui tests/test_preview.py::test_preview_file_permission -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-records-ui/.tox/c1/tmp -def test_preview_file_permission(app, records, users): +def test_preview_file_permission(app, db, records, users): @app.route('/record//preview_permission_test/') def view_preview_permission_test(pid_value, filename): return '' @@ -99,6 +99,13 @@ def view_preview_permission_test(pid_value, filename): record = results[4]['record'] recid = results[4]['recid'] filename = 'helloworld.pdf' + # アイテム登録時と同じく、ファイルのメタデータ(accessrole など)を + # ファイル実体の JSON に書き込んでおく。権限判定はこの値を見る + file_md = [f for f in record.get_file_data() if f.get('filename') == filename][0] + for f in record.files: + if f.obj.key == filename: + f.obj.file.update_json(file_md) + db.session.commit() template = 'invenio_records_ui/detail.html' url = '/record/{}/preview_permission_test/{}'.format(recid.pid_value, filename) From 0a463b8943e8a663edd37f9ecd7c3643fad31e90 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Mon, 28 Sep 2026 21:27:58 +0000 Subject: [PATCH 7/7] =?UTF-8?q?fix(invenio-iiif):=20=E3=82=B5=E3=83=A0?= =?UTF-8?q?=E3=83=8D=E3=82=A4=E3=83=AB=E4=BD=9C=E6=88=90=E3=82=BF=E3=82=B9?= =?UTF-8?q?=E3=82=AF=E3=81=A7=E3=81=AF=E5=88=A9=E7=94=A8=E8=80=85=E3=81=AE?= =?UTF-8?q?=E6=A8=A9=E9=99=90=E3=82=92=E7=A2=BA=E3=81=8B=E3=82=81=E3=81=9A?= =?UTF-8?q?=E3=81=AB=E5=AF=BE=E8=B1=A1=E3=82=92=E8=A7=A3=E6=B1=BA=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 画像を開く処理に権限判定を入れたため、リクエストの外で動くサムネイル作成 タスクが、利用者の情報が無く失敗するようになっていた。内部処理なので対象の オブジェクトを直接解決し、画像を開く処理にはそれを使わせる。利用者の リクエストで通る経路の判定は変えない。 Co-Authored-By: Claude Opus 5.5 --- modules/invenio-iiif/invenio_iiif/tasks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/invenio-iiif/invenio_iiif/tasks.py b/modules/invenio-iiif/invenio_iiif/tasks.py index 409672a0e0..e92d872986 100644 --- a/modules/invenio-iiif/invenio_iiif/tasks.py +++ b/modules/invenio-iiif/invenio_iiif/tasks.py @@ -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.""" + # 利用者のリクエストではない内部処理なので、利用者の権限は確かめずに + # 対象を解決しておく(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"