-
Notifications
You must be signed in to change notification settings - Fork 292
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* NEW Uses Flask-IIIF extension providing various image manipulation capabilities. * NEW Adds possibility to refer to documents and legacy BibDocFiles via special path such as `/api/multimedia/image/recid:{recid}` or `/api/multimedia/image/recid:{recid}-{filename}` or `/api/multimedia/image/uuid` with proper permission checking. * (closes #3080) Reviewed-by: Jiri Kuncar <[email protected]> Signed-off-by: Harris Tzovanakis <[email protected]>
- Loading branch information
Showing
7 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# This file is part of Invenio. | ||
# Copyright (C) 2015 CERN. | ||
# | ||
# Invenio is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License as | ||
# published by the Free Software Foundation; either version 2 of the | ||
# License, or (at your option) any later version. | ||
# | ||
# Invenio is distributed in the hope that it will be useful, but | ||
# WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
# General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with Invenio; if not, write to the Free Software Foundation, Inc., | ||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. | ||
|
||
"""Flask-IIIF extension. | ||
.. py:data:: IIIF_IMAGE_OPENER | ||
There are two ways to initialize an IIIF image object, by ``fullpath`` and | ||
``bytestream``. Can be an ``import string`` or a ``callback function``. | ||
By default ``identifier_to_path`` return the ``fullpath`` of | ||
:class:`~invenio.modules.documents.api:Document`. | ||
default: :func:`~invenio.modules.documents.utis.identifier_to_path` | ||
""" | ||
|
||
from flask_iiif import IIIF | ||
from flask_iiif.errors import MultimediaError | ||
|
||
from six import string_types | ||
|
||
from werkzeug.utils import import_string | ||
|
||
from .utils import api_file_permission_check | ||
|
||
__all__ = ('setup_app', ) | ||
|
||
iiif = IIIF() | ||
|
||
|
||
def setup_app(app): | ||
"""Setup Flask-IIIF extension.""" | ||
if 'invenio.modules.documents' in app.config.get('PACKAGES_EXCLUDE'): | ||
raise MultimediaError( | ||
"Could not initialize the Flask-IIIF extension because " | ||
":class:`~invenio.modules.documents.api:Document` is missing" | ||
) | ||
|
||
iiif.init_app(app) | ||
iiif.init_restful(app.extensions['restful']) | ||
app.config.setdefault( | ||
'IIIF_IMAGE_OPENER', | ||
'invenio.modules.documents.utils:identifier_to_path' | ||
) | ||
|
||
uuid_to_source_handler = app.config['IIIF_IMAGE_OPENER'] | ||
|
||
uuid_to_source = ( | ||
import_string(uuid_to_source_handler) if | ||
isinstance(uuid_to_source_handler, string_types) else | ||
uuid_to_source_handler | ||
) | ||
iiif.uuid_to_image_opener_handler(uuid_to_source) | ||
app.config['IIIF_CACHE_HANDLER'] = 'invenio.ext.cache:cache' | ||
|
||
# protect the api | ||
iiif.api_decorator_handler(api_file_permission_check) | ||
return app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# This file is part of Invenio. | ||
# Copyright (C) 2015 CERN. | ||
# | ||
# Invenio is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License as | ||
# published by the Free Software Foundation; either version 2 of the | ||
# License, or (at your option) any later version. | ||
# | ||
# Invenio is distributed in the hope that it will be useful, but | ||
# WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
# General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with Invenio; if not, write to the Free Software Foundation, Inc., | ||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. | ||
|
||
"""Flask-IIIF extension utils.""" | ||
|
||
from invenio.modules.documents.utils import ( | ||
identifier_to_path_and_permissions | ||
) | ||
|
||
__all__ = ('api_file_permission_check', ) | ||
|
||
|
||
def api_file_permission_check(*args, **kwargs): | ||
"""IIIF API file permission check.""" | ||
identifier_to_path_and_permissions(kwargs.get('uuid', '')) |
105 changes: 105 additions & 0 deletions
105
invenio/modules/documents/testsuite/test_file_permissions_and_fullpath.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# This file is part of Invenio. | ||
# Copyright (C) 2015 CERN. | ||
# | ||
# Invenio is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License as | ||
# published by the Free Software Foundation; either version 2 of the | ||
# License, or (at your option) any later version. | ||
# | ||
# Invenio is distributed in the hope that it will be useful, but | ||
# WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
# General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with Invenio; if not, write to the Free Software Foundation, Inc., | ||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. | ||
|
||
"""Test for document and legacy bibdocs access restrictions.""" | ||
|
||
import os | ||
import shutil | ||
import tempfile | ||
|
||
from invenio.testsuite import InvenioTestCase, make_test_suite, run_test_suite | ||
|
||
|
||
class DocumentAndLegacyRestrictionsTest(InvenioTestCase): | ||
|
||
"""Test document access restrictions.""" | ||
|
||
def setUp(self): | ||
"""Run before the test.""" | ||
from invenio.modules.documents.api import Document | ||
self.document = Document.create({'title': 'J.A.R.V.I.S'}) | ||
self.path = tempfile.mkdtemp() | ||
|
||
def tearDown(self): | ||
"""Run after the tests.""" | ||
self.document = None | ||
shutil.rmtree(self.path) | ||
|
||
def test_legacy_syntax(self): | ||
"""Test legacy syntax.""" | ||
from invenio.modules.documents.utils import _parse_legacy_syntax | ||
uuid_1 = 'recid:22' | ||
uuid_2 = 'recid:22-filename.jpg' | ||
|
||
check_uuid_1 = _parse_legacy_syntax(uuid_1) | ||
check_uuid_2 = _parse_legacy_syntax(uuid_2) | ||
answer_uuid_1 = '22', None | ||
answer_uuid_2 = '22', 'filename.jpg' | ||
|
||
self.assertEqual( | ||
check_uuid_1, | ||
answer_uuid_1 | ||
) | ||
self.assertEqual( | ||
check_uuid_2, | ||
answer_uuid_2 | ||
) | ||
|
||
def test_not_found_error(self): | ||
"""Test when the file doesn't exists.""" | ||
from werkzeug.exceptions import NotFound | ||
from invenio.modules.documents.utils import identifier_to_path | ||
self.assertRaises( | ||
NotFound, | ||
identifier_to_path, | ||
'this_is_not_a_uuid' | ||
) | ||
self.assertRaises( | ||
NotFound, | ||
identifier_to_path, | ||
self.document.get('uuid') | ||
) | ||
|
||
def test_forbidden_error(self): | ||
"""Test when the file is restricted.""" | ||
from werkzeug.exceptions import Forbidden | ||
from invenio.modules.documents.utils import ( | ||
identifier_to_path_and_permissions | ||
) | ||
content = 'S.H.I.E.L.D.' | ||
source, sourcepath = tempfile.mkstemp() | ||
|
||
with open(sourcepath, 'w+') as f: | ||
f.write(content) | ||
|
||
uri = os.path.join(self.path, 'classified.txt') | ||
self.document.setcontents(sourcepath, uri) | ||
self.document['restriction']['email'] = '[email protected]' | ||
test_document = self.document.update() | ||
self.assertRaises( | ||
Forbidden, | ||
identifier_to_path_and_permissions, | ||
test_document.get('uuid') | ||
) | ||
shutil.rmtree(sourcepath, ignore_errors=True) | ||
|
||
TEST_SUITE = make_test_suite(DocumentAndLegacyRestrictionsTest,) | ||
|
||
if __name__ == "__main__": | ||
run_test_suite(TEST_SUITE) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# This file is part of Invenio. | ||
# Copyright (C) 2015 CERN. | ||
# | ||
# Invenio is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License as | ||
# published by the Free Software Foundation; either version 2 of the | ||
# License, or (at your option) any later version. | ||
# | ||
# Invenio is distributed in the hope that it will be useful, but | ||
# WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
# General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with Invenio; if not, write to the Free Software Foundation, Inc., | ||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. | ||
|
||
"""Document utils.""" | ||
|
||
from flask import abort | ||
|
||
__all__ = ('identifier_to_path', 'identifier_to_path_and_permissions', ) | ||
|
||
|
||
def identifier_to_path(identifier): | ||
"""Convert the identifier to path. | ||
:param str identifier: A unique file identifier | ||
:raises NotFound: if the file fullpath is empty | ||
""" | ||
fullpath = identifier_to_path_and_permissions(identifier, path_only=True) | ||
return fullpath or abort(404) | ||
|
||
|
||
def identifier_to_path_and_permissions(identifier, path_only=False): | ||
"""Convert the identifier to path. | ||
:param str identifier: A unique file identifier | ||
:raises Forbidden: if the user doesn't have permissions | ||
""" | ||
if identifier.startswith('recid:'): | ||
record_id, filename = _parse_legacy_syntax(identifier) | ||
|
||
fullpath, permissions = _get_legacy_bibdoc( | ||
record_id, filename=filename | ||
) | ||
else: | ||
fullpath, permissions = _get_document(identifier) | ||
|
||
if path_only: | ||
return fullpath | ||
|
||
try: | ||
assert (permissions[0] == 0) | ||
except AssertionError: | ||
return abort(403) | ||
|
||
|
||
def _get_document(uuid): | ||
"""Get the document fullpath. | ||
:param str uuid: The document's uuid | ||
""" | ||
from invenio.modules.documents.api import Document | ||
from invenio.modules.documents.errors import ( | ||
DocumentNotFound, DeletedDocument | ||
) | ||
|
||
try: | ||
document = Document.get_document(uuid) | ||
except (DocumentNotFound, DeletedDocument): | ||
path = _simulate_file_not_found() | ||
else: | ||
path = document.get('uri', ''), document.is_authorized() | ||
finally: | ||
return path | ||
|
||
|
||
def _get_legacy_bibdoc(recid, filename=None): | ||
"""Get the the fullpath of legacy bibdocfile. | ||
:param int recid: The record id | ||
:param str filename: A specific filename | ||
:returns: bibdocfile full path | ||
:rtype: str | ||
""" | ||
from invenio.ext.login import current_user | ||
from invenio.legacy.bibdocfile.api import BibRecDocs | ||
paths = [ | ||
(bibdoc.fullpath, bibdoc.is_restricted(current_user)) | ||
for bibdoc in BibRecDocs(recid).list_latest_files(list_hidden=False) | ||
if not bibdoc.subformat and not filename or | ||
bibdoc.name + bibdoc.superformat == filename | ||
] | ||
try: | ||
path = paths[0] | ||
except IndexError: | ||
path = _simulate_file_not_found() | ||
finally: | ||
return path | ||
|
||
|
||
def _parse_legacy_syntax(identifier): | ||
"""Parse legacy syntax. | ||
.. note:: | ||
It can handle requests such as `recid:{recid}` or | ||
`recid:{recid}-{filename}`. | ||
""" | ||
if '-' in identifier: | ||
record_id, filename = identifier.split('recid:')[1].split('-') | ||
else: | ||
record_id, filename = identifier.split('recid:')[1], None | ||
return record_id, filename | ||
|
||
|
||
def _simulate_file_not_found(): | ||
"""Simulate file not found situation. | ||
..note :: | ||
It simulates an file not found situation, this will always raise `404` | ||
error. | ||
""" | ||
return '', (0, '') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters