-
Notifications
You must be signed in to change notification settings - Fork 95
fix: ファイルの取得・プレビュー・IIIF でファイル単位の権限判定を行う #1925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bc50425
6aa5c50
67bb31e
3768aef
e90be8e
97e5e4c
0a463b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
|
|
||
|
|
||
| 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])} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,8 @@ | |
| "recid": { | ||
| "pid_type": "recid", | ||
| "route": "/records/<pid_value>", | ||
| "permission_factory_imp": | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fix no.34, 925, 35 |
||
| "weko_records_ui.permissions:page_permission_factory", | ||
| }, | ||
|
|
||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fix no.35 |
||
| ] | ||
|
|
||
| if not images: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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})() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.""" | ||
| # 利用者のリクエストではない内部処理なので、利用者の権限は確かめずに | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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