diff --git a/sonar/modules/config.py b/sonar/modules/config.py index 2cf87c97..a2ba24c5 100644 --- a/sonar/modules/config.py +++ b/sonar/modules/config.py @@ -47,3 +47,9 @@ command line or progams like postman.""" SONAR_APP_UI_VERSION = '0.1.11' + +SONAR_APP_INTERNAL_IPS = ['127.0.0.1'] +"""Internal IPs for accessing files.""" + +SONAR_APP_DEFAULT_ORGANISATION = 'sonar' +"""Default organisation key.""" diff --git a/sonar/modules/documents/cli/rerodoc.py b/sonar/modules/documents/cli/rerodoc.py index 020b1b8c..b238dbc5 100644 --- a/sonar/modules/documents/cli/rerodoc.py +++ b/sonar/modules/documents/cli/rerodoc.py @@ -58,7 +58,6 @@ def save_records(ids): indexer.bulk_index(ids) indexer.process_bulk_queue() - click.secho(permissions_file.name) try: with open(permissions_file.name, 'r') as file: reader = csv.reader(file, delimiter=';') diff --git a/sonar/modules/documents/marshmallow/json.py b/sonar/modules/documents/marshmallow/json.py index 53867d4a..3065caa6 100644 --- a/sonar/modules/documents/marshmallow/json.py +++ b/sonar/modules/documents/marshmallow/json.py @@ -22,7 +22,9 @@ from invenio_records_rest.schemas import StrictKeysMixin from invenio_records_rest.schemas.fields import PersistentIdentifier, \ SanitizedUnicode -from marshmallow import fields +from marshmallow import fields, pre_dump + +from sonar.modules.documents.views import is_file_restricted class DocumentMetadataSchemaV1(StrictKeysMixin): @@ -49,6 +51,29 @@ class DocumentMetadataSchemaV1(StrictKeysMixin): identifiedBy = fields.List(fields.Dict()) subjects = fields.List(fields.Dict()) + @pre_dump + def add_files_restrictions(self, item): + """Add restrictions to file before dumping data. + + :param item: Item object to process + :returns: Modified item + """ + if not item.get('_files'): + return item + + for key, file in enumerate(item['_files']): + if file['type'] == 'file': + restricted = is_file_restricted(file, item) + + # Format date before serialization + if restricted.get('date'): + restricted['date'] = restricted['date'].strftime( + '%Y-%m-%d') + + item['_files'][key]['restricted'] = restricted + + return item + class DocumentSchemaV1(StrictKeysMixin): """Document schema.""" diff --git a/sonar/modules/documents/views.py b/sonar/modules/documents/views.py index 15cf0abe..6478bc85 100644 --- a/sonar/modules/documents/views.py +++ b/sonar/modules/documents/views.py @@ -20,8 +20,9 @@ from __future__ import absolute_import, print_function import re +from datetime import datetime -from flask import Blueprint, current_app, g, render_template +from flask import Blueprint, current_app, g, render_template, request from flask_babelex import gettext as _ from sonar.modules.utils import change_filename_extension @@ -280,6 +281,45 @@ def part_of_format(part_of): return ', '.join(items) +@blueprint.app_template_filter() +def is_file_restricted(file, record): + """Check if current file can be displayed. + + :param file: File dict + :param record: Current record + :returns object containing result and possibly embargo date + """ + restricted = {'restricted': False, 'date': None} + + try: + embargo_date = datetime.strptime(file.get('embargo_date'), '%Y-%m-%d') + except Exception: + embargo_date = None + + # Store embargo date if greater than now + if embargo_date and embargo_date > datetime.now(): + restricted['restricted'] = True + restricted['date'] = embargo_date + + # File is restricted by institution + if file.get('restricted'): + if file['restricted'] in ['rero', 'internal']: + result = request.remote_addr not in current_app.config.get( + 'SONAR_APP_INTERNAL_IPS') + restricted['restricted'] = result + else: + # compare with current organisation + organisation = get_current_organisation() + restricted['restricted'] = organisation == current_app.config.get( + 'SONAR_APP_DEFAULT_ORGANISATION' + ) or organisation != record['institution']['pid'] + + if not restricted['restricted']: + restricted['date'] = None + + return restricted + + def get_language_from_bibliographic_code(language_code): """Return language code from bibliographic language. @@ -327,3 +367,15 @@ def get_preferred_languages(force_language=None): preferred_languages.insert(0, force_language) return list(dict.fromkeys(preferred_languages)) + + +def get_current_organisation(): + """Return current organisation by globals or query parameter.""" + organisation = request.args.get('view') + if organisation: + return organisation + + if g.get('ir'): + return g.ir + + return None diff --git a/sonar/theme/static/images/restricted.png b/sonar/theme/static/images/restricted.png new file mode 100644 index 00000000..bfc12f16 Binary files /dev/null and b/sonar/theme/static/images/restricted.png differ diff --git a/sonar/theme/templates/sonar/macros/macro.html b/sonar/theme/templates/sonar/macros/macro.html index 985d667a..f43113ad 100644 --- a/sonar/theme/templates/sonar/macros/macro.html +++ b/sonar/theme/templates/sonar/macros/macro.html @@ -68,6 +68,8 @@ {% set file_title = file | file_title %} {% set externalUrl = record | has_external_urls_for_files %}
+ {% set restricted = file | is_file_restricted(record) %} + {% if not restricted.restricted %} {% if externalUrl %} {% if file.external_url %} @@ -91,6 +93,15 @@ class="fa fa-download">

{% endif %} + {% else %} + {{ file.key }} +

{{ file_title }}

+ {% if restricted.date %} + + {{ _('Public access from') }} {{ restricted.date.strftime('%d/%m/%Y') }} + + {% endif %} + {% endif %}
{% endif %} {% endmacro %} diff --git a/tests/api/test_api_simple_flow.py b/tests/api/test_api_simple_flow.py index 010a0e7f..db94c5f7 100644 --- a/tests/api/test_api_simple_flow.py +++ b/tests/api/test_api_simple_flow.py @@ -42,3 +42,13 @@ def test_simple_flow(client, document_json_fixture): assert res.status_code == 200 assert response.json['metadata']['title'][0]['mainTitle'][0][ 'value'] == 'Title of the document' + + +def test_add_files_restrictions(client, document_with_file): + """Test adding file restrictions before dumping object.""" + res = client.get('https://localhost:5000/documents/10000') + assert res.status_code == 200 + assert res.json['metadata']['_files'][0]['restricted'] == { + 'restricted': False, + 'date': None + } diff --git a/tests/ui/documents/test_documents_views.py b/tests/ui/documents/test_documents_views.py index ccffd662..379e1888 100644 --- a/tests/ui/documents/test_documents_views.py +++ b/tests/ui/documents/test_documents_views.py @@ -17,6 +17,8 @@ """Test documents views.""" +import datetime + import pytest import sonar.modules.documents.views as views @@ -384,3 +386,105 @@ def test_part_of_format(): assert views.part_of_format({ 'numberingYear': '2015', }) == '2015' + + +def test_is_file_restricted(app): + """Test if a file is restricted by embargo date and/or institution.""" + views.pull_ir(None, {'ir': 'sonar'}) + + record = {'institution': {'pid': 'unisi'}} + + # No restricution and no embargo date + assert views.is_file_restricted({}, {}) == { + 'date': None, + 'restricted': False + } + + # Restricted by internal, but IP is allowed + with app.test_request_context(environ_base={'REMOTE_ADDR': '127.0.0.1'}): + assert views.is_file_restricted({'restricted': 'internal'}, {}) == { + 'date': None, + 'restricted': False + } + + # Restricted by internal, but IP is not allowed + with app.test_request_context(environ_base={'REMOTE_ADDR': '10.1.2.3'}): + assert views.is_file_restricted({'restricted': 'internal'}, {}) == { + 'date': None, + 'restricted': True + } + + # Restricted by institution and current institution don't match + assert views.is_file_restricted({'restricted': 'institution'}, record) == { + 'date': None, + 'restricted': True + } + + # Restricted by institution and current institution match + views.pull_ir(None, {'ir': 'unisi'}) + assert views.is_file_restricted({'restricted': 'institution'}, record) == { + 'date': None, + 'restricted': False + } + + # Restricted by embargo date only, but embargo date is in the past + assert views.is_file_restricted({'embargo_date': '2020-01-01'}, {}) == { + 'date': None, + 'restricted': False + } + + # Restricted by embargo date only and embargo date is in the future + with app.test_request_context(environ_base={'REMOTE_ADDR': '10.1.2.3'}): + assert views.is_file_restricted({'embargo_date': '2021-01-01'}, + {}) == { + 'date': datetime.datetime( + 2021, 1, 1, 0, 0), + 'restricted': True + } + + # Restricted by embargo date and institution + views.pull_ir(None, {'ir': 'sonar'}) + with app.test_request_context(environ_base={'REMOTE_ADDR': '10.1.2.3'}): + assert views.is_file_restricted( + { + 'embargo_date': '2021-01-01', + 'restricted': 'institution' + }, record) == { + 'restricted': True, + 'date': datetime.datetime(2021, 1, 1, 0, 0) + } + + # Restricted by embargo date but internal IP gives access + with app.test_request_context(environ_base={'REMOTE_ADDR': '127.0.0.1'}): + assert views.is_file_restricted( + { + 'embargo_date': '2021-01-01', + 'restricted': 'internal' + }, {}) == { + 'date': None, + 'restricted': False + } + + +def test_get_current_organisation(app): + """Test get current organisation.""" + # No globals and no args + assert views.get_current_organisation() == 'sonar' + + # Default globals and no args + views.pull_ir(None, {'ir': 'sonar'}) + assert views.get_current_organisation() == 'sonar' + + # Organisation globals and no args + views.pull_ir(None, {'ir': 'unisi'}) + assert views.get_current_organisation() == 'unisi' + + # Args is global + with app.test_request_context() as req: + req.request.args = {'view': 'sonar'} + assert views.get_current_organisation() == 'sonar' + + # Args has organisation view + with app.test_request_context() as req: + req.request.args = {'view': 'unisi'} + assert views.get_current_organisation() == 'unisi'