From 2762ad302bee86ff3e1230941018fd7464a34ac5 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Fri, 30 Oct 2026 01:07:02 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20REST=20API=20=E3=82=A8=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=E3=81=AE=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=82=92=E8=A6=8B=E7=9B=B4=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - records 系エンドポイントの permission factory を明示的に設定 - deposit エンドポイントにメソッド単位でデコレータを指定できる create_decorators / update_decorators オプションを追加 Co-Authored-By: Claude Opus 5.5 --- .../invenio_deposit/views/rest.py | 34 +++++++- .../invenio-deposit/tests/test_views_rest.py | 77 +++++++++++++++++++ .../weko-deposit/tests/test_weko_deposit.py | 16 ++++ modules/weko-deposit/weko_deposit/config.py | 2 + .../tests/test_weko_search_ui.py | 14 ++++ .../weko-search-ui/weko_search_ui/config.py | 11 +++ 6 files changed, 153 insertions(+), 1 deletion(-) diff --git a/modules/invenio-deposit/invenio_deposit/views/rest.py b/modules/invenio-deposit/invenio_deposit/views/rest.py index ff3914d780..a0f0148d59 100644 --- a/modules/invenio-deposit/invenio_deposit/views/rest.py +++ b/modules/invenio-deposit/invenio_deposit/views/rest.py @@ -28,7 +28,7 @@ import json from copy import deepcopy -from functools import partial +from functools import partial, wraps from flask import Blueprint, abort, current_app, make_response, request, \ url_for @@ -63,6 +63,26 @@ def create_error_handlers(blueprint): records_rest_error_handlers(blueprint) +def decorate_view(view_func, decorators, methods): + """Apply ``decorators`` to the requests of ``view_func`` in ``methods``. + + :param view_func: The view function. + :param decorators: List of decorators or their import strings. + :param methods: HTTP methods the decorators are applied to. + :returns: The wrapped view function. + """ + decorated = view_func + for decorator in reversed(decorators): + decorated = obj_or_import_string(decorator)(decorated) + + @wraps(view_func) + def dispatch(*args, **kwargs): + if request.method in methods: + return decorated(*args, **kwargs) + return view_func(*args, **kwargs) + return dispatch + + def create_blueprint(endpoints): """Create Invenio-Deposit-REST blueprint. @@ -116,6 +136,13 @@ def dbsession_clean(exception): '{0}/files/'.format(options['item_route']) ) + route_decorators = { + options['list_route']: ( + options.pop('create_decorators', []), ('POST',)), + options['item_route']: ( + options.pop('update_decorators', []), ('PUT', 'PATCH')), + } + options.setdefault('search_class', DepositSearch) search_class = obj_or_import_string(options['search_class']) @@ -127,6 +154,11 @@ def dbsession_clean(exception): options.setdefault('indexer_class', None) for rule in records_rest_url_rules(endpoint, **options): + decorators, methods = route_decorators.get( + rule['rule'], ([], ())) + if decorators: + rule['view_func'] = decorate_view( + rule['view_func'], decorators, methods) blueprint.add_url_rule(**rule) search_class_kwargs = {} diff --git a/modules/invenio-deposit/tests/test_views_rest.py b/modules/invenio-deposit/tests/test_views_rest.py index 69acd486a8..7153dbf2ea 100644 --- a/modules/invenio-deposit/tests/test_views_rest.py +++ b/modules/invenio-deposit/tests/test_views_rest.py @@ -623,3 +623,80 @@ def test_simple_rest_flow(app, test_client, api, es, location, fake_schemas, use # assert res.status_code == 200 # data = json.loads(res.data.decode('utf-8')) # assert 'Revision 2' == data['metadata']['title'] + + +def test_decorate_view(): + """Test decorators are applied to the given methods only.""" + from flask import Flask, abort + + from invenio_deposit.views.rest import decorate_view + + def deny(f): + def decorated(*args, **kwargs): + abort(418) + return decorated + + def view(): + return 'ok' + + app = Flask('testapp') + app.add_url_rule( + '/items', view_func=decorate_view(view, [deny], ('POST', 'PUT')), + methods=['GET', 'POST', 'PUT', 'PATCH']) + with app.test_client() as client: + assert client.get('/items').status_code == 200 + assert client.patch('/items').status_code == 200 + assert client.post('/items').status_code == 418 + assert client.put('/items').status_code == 418 + + +def test_create_blueprint_route_decorators(): + """Test create_decorators and update_decorators options.""" + from flask import Flask + from invenio_db import InvenioDB + + from invenio_deposit.views.rest import create_blueprint + + endpoints = dict( + depid=dict( + pid_type='depid', + pid_minter='deposit', + pid_fetcher='deposit', + record_serializers={ + 'application/json': ('invenio_records_rest.serializers' + ':json_v1_response'), + }, + search_serializers={ + 'application/json': ('invenio_records_rest.serializers' + ':json_v1_search'), + }, + list_route='/deposits/', + item_route='/deposits/', + default_media_type='application/json', + create_decorators=[_abort_factory(418)], + update_decorators=[_abort_factory(409)], + ) + ) + app = Flask('testapp') + app.config.update( + SQLALCHEMY_DATABASE_URI='sqlite://', + SQLALCHEMY_TRACK_MODIFICATIONS=False, + ) + InvenioDB(app) + app.register_blueprint(create_blueprint(endpoints)) + with app.test_client() as client: + assert client.post('/deposits/').status_code == 418 + assert client.put('/deposits/1').status_code == 409 + assert client.patch('/deposits/1').status_code == 409 + assert 'create_decorators' in endpoints['depid'] + assert 'update_decorators' in endpoints['depid'] + + +def _abort_factory(code): + from flask import abort + + def decorator(f): + def decorated(*args, **kwargs): + abort(code) + return decorated + return decorator diff --git a/modules/weko-deposit/tests/test_weko_deposit.py b/modules/weko-deposit/tests/test_weko_deposit.py index ed9cedeebe..7df8cc40f0 100644 --- a/modules/weko-deposit/tests/test_weko_deposit.py +++ b/modules/weko-deposit/tests/test_weko_deposit.py @@ -81,3 +81,19 @@ def test_ItemResource_put(app, db): res = client.put("/deposits/redirect/1", data=json.dumps(data), headers=headers) assert res.status_code == 200 + + +def test_deposit_rest_endpoints_route_decorators(): + """Test route decorators of the deposit REST endpoints.""" + from weko_deposit.config import (DEPOSIT_REST_ENDPOINTS, + WEKO_DEPOSIT_REST_ENDPOINTS) + + expected = ['weko_accounts.utils:login_required_customize'] + assert DEPOSIT_REST_ENDPOINTS['depid']['create_decorators'] == expected + assert WEKO_DEPOSIT_REST_ENDPOINTS['depid']['create_decorators'] \ + == expected + + expected = ['weko_deposit.rest:require_item_edit_permission'] + assert DEPOSIT_REST_ENDPOINTS['depid']['update_decorators'] == expected + assert WEKO_DEPOSIT_REST_ENDPOINTS['depid']['update_decorators'] \ + == expected diff --git a/modules/weko-deposit/weko_deposit/config.py b/modules/weko-deposit/weko_deposit/config.py index 93e3f7c47f..f5e2edcdcb 100644 --- a/modules/weko-deposit/weko_deposit/config.py +++ b/modules/weko-deposit/weko_deposit/config.py @@ -128,6 +128,8 @@ # read_permission_factory_imp='', # update_permission_factory_imp='', delete_permission_factory_imp=deny_all, + create_decorators=['weko_accounts.utils:login_required_customize'], + update_decorators=['weko_deposit.rest:require_item_edit_permission'], ) ) diff --git a/modules/weko-search-ui/tests/test_weko_search_ui.py b/modules/weko-search-ui/tests/test_weko_search_ui.py index f3989b76d9..0f1500f1d8 100644 --- a/modules/weko-search-ui/tests/test_weko_search_ui.py +++ b/modules/weko-search-ui/tests/test_weko_search_ui.py @@ -43,3 +43,17 @@ def test_init(app): # assert 'weko-search-ui' not in app.extensions # ext.init_app(app) # assert 'weko-search-ui' in app.extensions + + +def test_records_rest_endpoints_permission_factories(): + """Test permission factories of the records REST endpoints.""" + from weko_search_ui.config import RECORDS_REST_ENDPOINTS + + for endpoint in ("recid", "opensearch", "worksapce"): + options = RECORDS_REST_ENDPOINTS[endpoint] + for key in ( + "create_permission_factory_imp", + "update_permission_factory_imp", + "delete_permission_factory_imp", + ): + assert options[key] == "invenio_records_rest.utils:deny_all" diff --git a/modules/weko-search-ui/weko_search_ui/config.py b/modules/weko-search-ui/weko_search_ui/config.py index 6869fca640..5b6ab5a1b2 100644 --- a/modules/weko-search-ui/weko_search_ui/config.py +++ b/modules/weko-search-ui/weko_search_ui/config.py @@ -138,6 +138,17 @@ "application/json": ("weko_records.serializers:opensearch_v1_search"), } +for _endpoint in ("recid", "opensearch", "worksapce"): + RECORDS_REST_ENDPOINTS[_endpoint][ + "create_permission_factory_imp" + ] = "invenio_records_rest.utils:deny_all" + RECORDS_REST_ENDPOINTS[_endpoint][ + "update_permission_factory_imp" + ] = "invenio_records_rest.utils:deny_all" + RECORDS_REST_ENDPOINTS[_endpoint][ + "delete_permission_factory_imp" + ] = "invenio_records_rest.utils:deny_all" + # RECORDS_REST_ENDPOINTS['recid']['read_permission_factory_imp'] = allow_all INDEXER_DEFAULT_INDEX = "{}-weko-item-v1.0.0".format(index_prefix) # Use direct index From 6133fc804faff3e1ede8df3607078dc94dd97909 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Fri, 30 Oct 2026 01:07:02 +0000 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E3=82=B2=E3=82=B9=E3=83=88?= =?UTF-8?q?=E7=94=A8=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=E3=81=AE=E7=99=BA?= =?UTF-8?q?=E8=A1=8C=E3=83=BB=E5=8F=97=E4=BB=98=E3=82=92=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=81=A7=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED / WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED を追加(既定値は False) - scripts/instance.cfg で両設定を True に設定 Co-Authored-By: Claude Opus 5.5 --- modules/weko-admin/tests/conftest.py | 2 + modules/weko-admin/tests/test_utils.py | 30 ++++++++ modules/weko-admin/weko_admin/utils.py | 6 +- modules/weko-records-ui/tests/conftest.py | 2 + modules/weko-records-ui/tests/test_utils.py | 16 ++++ .../weko-records-ui/weko_records_ui/rest.py | 3 + .../weko-records-ui/weko_records_ui/utils.py | 5 +- modules/weko-workflow/tests/conftest.py | 2 + modules/weko-workflow/tests/test_utils.py | 74 +++++++++++++++++++ modules/weko-workflow/weko_workflow/config.py | 6 ++ modules/weko-workflow/weko_workflow/errors.py | 7 ++ modules/weko-workflow/weko_workflow/ext.py | 2 + modules/weko-workflow/weko_workflow/utils.py | 53 ++++++++++++- modules/weko-workflow/weko_workflow/views.py | 4 +- modules/weko-workspace/tests/conftest.py | 2 + scripts/instance.cfg | 4 + 16 files changed, 214 insertions(+), 4 deletions(-) diff --git a/modules/weko-admin/tests/conftest.py b/modules/weko-admin/tests/conftest.py index adaf320fb0..22a91ffe5d 100644 --- a/modules/weko-admin/tests/conftest.py +++ b/modules/weko-admin/tests/conftest.py @@ -132,6 +132,8 @@ def base_app(instance_path, cache_config,request ,search_class): os.environ['INVENIO_ELASTICSEARCH_HOST']='elasticsearch_test' app_ = Flask('test_weko_admin_app', instance_path=instance_path) app_.config.update( + WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED=True, + WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED=True, SERVER_NAME='test_server', ACCOUNTS_USE_CELERY=False, SECRET_KEY='SECRET_KEY', diff --git a/modules/weko-admin/tests/test_utils.py b/modules/weko-admin/tests/test_utils.py index 97d0fa57da..e92f73dce0 100755 --- a/modules/weko-admin/tests/test_utils.py +++ b/modules/weko-admin/tests/test_utils.py @@ -2328,3 +2328,33 @@ def test_get_detail_search_list(i18n_app, users): with patch("flask_login.utils._get_user", return_value=users[3]['obj']): result = get_detail_search_list() assert result + + +# class UsageReport: send_reminder_mail +def test_usage_report_send_reminder_mail_guest_token_disabled(): + from flask import Flask + + from weko_admin.utils import UsageReport + + app = Flask('testapp') + app.config.update( + THEME_SITEURL='https://example.org', + THEME_INSTITUTION_NAME={'ja': '', 'en': ''}, + WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED=False, + ) + from mock import MagicMock + + activity = MagicMock() + activity.extra_info = {'is_guest': True, 'guest_mail': 'a@example.org'} + with app.test_request_context(): + with patch('weko_admin.utils.UsageReport._UsageReport__get_site_info', + return_value=('', '')), \ + patch('weko_admin.utils.UsageReport.' + '_UsageReport__get_default_mail_sender', + return_value=''): + usage_report = UsageReport() + with patch.object(usage_report, + '_UsageReport__process_send_mail') as send: + assert usage_report.send_reminder_mail( + [], 'mail-id', [activity]) is True + send.assert_not_called() diff --git a/modules/weko-admin/weko_admin/utils.py b/modules/weko-admin/weko_admin/utils.py index dc9e4a1194..a257c5b093 100755 --- a/modules/weko-admin/weko_admin/utils.py +++ b/modules/weko-admin/weko_admin/utils.py @@ -1875,12 +1875,13 @@ def __init__(self): from weko_workflow.api import WorkActivity from weko_workflow.models import ActionStatusPolicy from weko_workflow.utils import generate_guest_activity_token_value, \ - process_send_mail + is_guest_token_issue_enabled, process_send_mail self.__activities_id = [] self.__activities_number = 0 self.__work_activity = WorkActivity() self.__action_status_policy = ActionStatusPolicy() self.__activity_token_value = generate_guest_activity_token_value + self.__is_guest_token_issue_enabled = is_guest_token_issue_enabled self.__process_send_mail = process_send_mail self.__page_number = 1 self.__usage_report_activities_data = [] @@ -1991,6 +1992,9 @@ def send_reminder_mail(self, activities_id: list, .get("WEKO_WORKFLOW_REQUEST_FOR_REGISTER_USAGE_REPORT") for activity in activities: + if (activity.extra_info or {}).get('is_guest') \ + and not self.__is_guest_token_issue_enabled(): + continue if activity.item_id: records_id.append(activity.item_id) diff --git a/modules/weko-records-ui/tests/conftest.py b/modules/weko-records-ui/tests/conftest.py index 55819effdb..ddfa91f6fe 100644 --- a/modules/weko-records-ui/tests/conftest.py +++ b/modules/weko-records-ui/tests/conftest.py @@ -190,6 +190,8 @@ def base_app(instance_path): # os.environ['FPDF_FONT_DIR'] = "/code/modules/weko-records-ui/weko_records_ui/fonts/" app_.config.update( + WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED=True, + WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED=True, CELERY_ALWAYS_EAGER=True, CELERY_CACHE_BACKEND="memory", CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, diff --git a/modules/weko-records-ui/tests/test_utils.py b/modules/weko-records-ui/tests/test_utils.py index 680831e434..b7aff7be06 100644 --- a/modules/weko-records-ui/tests/test_utils.py +++ b/modules/weko-records-ui/tests/test_utils.py @@ -1879,3 +1879,19 @@ def test_delete_version(app, records): "weko_records_ui.utils.call_external_system") as mock_external: delete_version("2.0") mock_external.assert_not_called() + + +# def create_usage_report_for_user +def test_create_usage_report_for_user_guest_token_disabled(): + from flask import Flask + + from weko_records_ui.utils import create_usage_report_for_user + + app = Flask('testapp') + app.config['WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED'] = False + with app.app_context(): + with patch('weko_records_ui.utils.WorkActivity') as work_activity: + assert create_usage_report_for_user( + {'usage_application_activity_id': 'A-1', + 'is_guest': True}) is None + work_activity.assert_not_called() diff --git a/modules/weko-records-ui/weko_records_ui/rest.py b/modules/weko-records-ui/weko_records_ui/rest.py index 23d964d489..8e3a2cab34 100644 --- a/modules/weko-records-ui/weko_records_ui/rest.py +++ b/modules/weko-records-ui/weko_records_ui/rest.py @@ -61,6 +61,7 @@ from weko_workflow.models import GuestActivity from weko_workflow.scopes import activity_scope from weko_workflow.utils import check_etag, check_pretty, init_activity_for_guest_user +from weko_workflow.utils import is_guest_token_issue_enabled from .errors import AvailableFilesNotFoundRESTError, ContentsNotFoundError, DateFormatRESTError, \ FilesNotFoundRESTError, InternalServerError, InvalidEmailError, InvalidRequestError, \ @@ -502,6 +503,8 @@ def post_v1(self, **kwargs): except Exception as ex: # invalid email raise InvalidEmailError() # 400 Error + if not is_guest_token_issue_enabled(): + raise PermissionError() # 403 Error # Check pretty check_pretty(param_pretty) diff --git a/modules/weko-records-ui/weko_records_ui/utils.py b/modules/weko-records-ui/weko_records_ui/utils.py index 31d433ed95..f6215d796e 100644 --- a/modules/weko-records-ui/weko_records_ui/utils.py +++ b/modules/weko-records-ui/weko_records_ui/utils.py @@ -1094,7 +1094,10 @@ def create_usage_report_for_user(onetime_download_extra_info: dict): is_guest = onetime_download_extra_info.get('is_guest', False) # Get Usage Application Activity. - from weko_workflow.utils import create_record_metadata_for_user + from weko_workflow.utils import create_record_metadata_for_user, \ + is_guest_token_issue_enabled + if is_guest and not is_guest_token_issue_enabled(): + return None usage_application_activity = WorkActivity().get_activity_by_id( activity_id) diff --git a/modules/weko-workflow/tests/conftest.py b/modules/weko-workflow/tests/conftest.py index 72a09f514f..27be349178 100644 --- a/modules/weko-workflow/tests/conftest.py +++ b/modules/weko-workflow/tests/conftest.py @@ -218,6 +218,8 @@ def base_app(instance_path, search_class, cache_config): """Flask application fixture.""" app_ = Flask('testapp', instance_path=instance_path) app_.config.update( + WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED=True, + WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED=True, SECRET_KEY='SECRET_KEY', TESTING=True, SERVER_NAME='TEST_SERVER.localdomain', diff --git a/modules/weko-workflow/tests/test_utils.py b/modules/weko-workflow/tests/test_utils.py index 803d1a0b7a..fbcce9c8b7 100644 --- a/modules/weko-workflow/tests/test_utils.py +++ b/modules/weko-workflow/tests/test_utils.py @@ -5824,3 +5824,77 @@ def test_reset_flow_action_roles_restricted_access(app,db,db_register_full_actio assert action_role.specify_property == None assert action_role.action_item_registrant == False + +# def is_guest_token_issue_enabled / is_guest_token_accept_enabled +# def guest_token_issue_required / clear_guest_session +def test_guest_token_settings(): + from flask import Flask + + from weko_workflow.errors import GuestTokenDisabledError + from weko_workflow.utils import ( + clear_guest_session, generate_guest_activity_token_value, + guest_token_issue_required, init_activity_for_guest_user, + is_guest_token_accept_enabled, is_guest_token_issue_enabled, + validate_guest_activity_token) + + app = Flask('testapp') + app.config.update( + SECRET_KEY='test', + WEKO_RECORDS_UI_SECRET_KEY='test', + WEKO_WORKFLOW_DATE_FORMAT='%Y-%m-%d', + WEKO_WORKFLOW_ACTIVITY_TOKEN_PATTERN=( + 'activity={} file_name={} date={} email={}'), + ) + + @guest_token_issue_required + def view(): + return 'ok' + + with app.test_request_context(): + # default + assert is_guest_token_issue_enabled() is False + assert is_guest_token_accept_enabled() is False + + # enabled + app.config['WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED'] = True + app.config['WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED'] = True + assert is_guest_token_issue_enabled() is True + assert is_guest_token_accept_enabled() is True + token = generate_guest_activity_token_value( + 'A-1', 'a.txt', datetime.datetime(2026, 1, 1), 'a@example.org') + assert validate_guest_activity_token(token, 'a.txt')[0] is True + assert view() == 'ok' + + # issue disabled + app.config['WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED'] = False + with pytest.raises(GuestTokenDisabledError): + generate_guest_activity_token_value( + 'A-1', 'a.txt', datetime.datetime(2026, 1, 1), + 'a@example.org') + with pytest.raises(GuestTokenDisabledError): + init_activity_for_guest_user({'extra_info': {}}) + with pytest.raises(GuestTokenDisabledError): + view() + assert validate_guest_activity_token(token, 'a.txt')[0] is True + + # accept disabled + app.config['WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED'] = True + app.config['WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED'] = False + assert validate_guest_activity_token(token, 'a.txt') \ + == (False, None, None) + + session['guest_token'] = token + session['guest_email'] = 'a@example.org' + session['guest_url'] = '/workflow/activity/guest-user/a.txt' + session['other'] = 'value' + clear_guest_session() + assert 'guest_token' not in session + assert 'guest_email' not in session + assert 'guest_url' not in session + assert session['other'] == 'value' + + # accept enabled keeps the guest session + app.config['WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED'] = True + session['guest_token'] = token + clear_guest_session() + assert session['guest_token'] == token diff --git a/modules/weko-workflow/weko_workflow/config.py b/modules/weko-workflow/weko_workflow/config.py index f07eac0971..1c1e563fc0 100644 --- a/modules/weko-workflow/weko_workflow/config.py +++ b/modules/weko-workflow/weko_workflow/config.py @@ -477,6 +477,12 @@ WEKO_WORKFLOW_ACTIVITY_TOKEN_PATTERN = "activity={} file_name={} date={} email={}" """Token pattern.""" +WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED = False +"""Issue tokens for guest activities.""" + +WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED = False +"""Accept tokens for guest activities.""" + WEKO_WORKFLOW_USAGE_APPLICATION_ITEM_TYPES_LIST = [31001, 31002, 31004, 31005, 31006, 31007, 31008] WEKO_WORKFLOW_USAGE_REPORT_ITEM_TYPES_LIST = [3007, 31003] diff --git a/modules/weko-workflow/weko_workflow/errors.py b/modules/weko-workflow/weko_workflow/errors.py index 5fcf5af5b4..2516f67ff8 100644 --- a/modules/weko-workflow/weko_workflow/errors.py +++ b/modules/weko-workflow/weko_workflow/errors.py @@ -59,6 +59,13 @@ class ExpiredActivityError(RESTException): code = 400 description = _('Expired token.') + +class GuestTokenDisabledError(RESTException): + """Guest token is disabled.""" + + code = 403 + description = _('This function is not available.') + class IndexNotFoundError(RESTException): """Index not found.""" diff --git a/modules/weko-workflow/weko_workflow/ext.py b/modules/weko-workflow/weko_workflow/ext.py index 21dd40855c..70c6eb82b8 100644 --- a/modules/weko-workflow/weko_workflow/ext.py +++ b/modules/weko-workflow/weko_workflow/ext.py @@ -41,8 +41,10 @@ def init_app(self, app): """ from weko_deposit.signals import item_created from .sessions import upt_activity_item + from .utils import clear_guest_session from .views import depositactivity_blueprint, workflow_blueprint self.init_config(app) + app.before_request(clear_guest_session) item_created.connect(upt_activity_item, app) app.register_blueprint(workflow_blueprint) app.register_blueprint(depositactivity_blueprint) diff --git a/modules/weko-workflow/weko_workflow/utils.py b/modules/weko-workflow/weko_workflow/utils.py index b5296de704..a6b45054e3 100644 --- a/modules/weko-workflow/weko_workflow/utils.py +++ b/modules/weko-workflow/weko_workflow/utils.py @@ -27,6 +27,7 @@ from collections import OrderedDict from copy import deepcopy from datetime import datetime, timedelta +from functools import wraps import sys from typing import List, NoReturn, Optional, Tuple, Union, cast import traceback @@ -87,7 +88,7 @@ WorkActivityHistory, WorkFlow from .config import DOI_VALIDATION_INFO, DOI_VALIDATION_INFO_CROSSREF, DOI_VALIDATION_INFO_DATACITE, IDENTIFIER_GRANT_SELECT_DICT, \ WEKO_SERVER_CNRI_HOST_LINK, WEKO_STR_TRUE -from .errors import InvalidParameterValueError +from .errors import GuestTokenDisabledError, InvalidParameterValueError from .models import Action as _Action from .models import ActionStatusPolicy, Activity, ActivityStatusPolicy, \ FlowAction, FlowActionRole, GuestActivity @@ -3553,6 +3554,45 @@ def send_mail_url_guest_user(mail_info): return True +def is_guest_token_issue_enabled() -> bool: + """Check whether tokens for guest activities are issued. + + Returns: + bool: True if ``WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED`` is set. + """ + return bool(current_app.config.get( + 'WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED', False)) + + +def is_guest_token_accept_enabled() -> bool: + """Check whether tokens for guest activities are accepted. + + Returns: + bool: True if ``WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED`` is set. + """ + return bool(current_app.config.get( + 'WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED', False)) + + +def guest_token_issue_required(f): + """Abort with 403 unless tokens for guest activities are issued.""" + @wraps(f) + def decorated(*args, **kwargs): + if not is_guest_token_issue_enabled(): + raise GuestTokenDisabledError() + return f(*args, **kwargs) + return decorated + + +def clear_guest_session(): + """Drop the guest session unless guest tokens are accepted.""" + if is_guest_token_accept_enabled(): + return + for key in ('guest_token', 'guest_email', 'guest_url'): + if key in session: + session.pop(key) + + def generate_guest_activity_token_value( activity_id: str, file_name: str, activity_date: datetime, guest_mail: str ) -> str: @@ -3567,7 +3607,12 @@ def generate_guest_activity_token_value( Returns: [str]: token value string. + Raises: + GuestTokenDisabledError: tokens for guest activities are not issued. + """ + if not is_guest_token_issue_enabled(): + raise GuestTokenDisabledError() date_form_str = current_app.config['WEKO_WORKFLOW_DATE_FORMAT'] token_pattern = current_app.config['WEKO_WORKFLOW_ACTIVITY_TOKEN_PATTERN'] activity_date = activity_date.strftime(date_form_str) @@ -3589,7 +3634,11 @@ def init_activity_for_guest_user( @param data: @param is_usage_report: @return: + @raise GuestTokenDisabledError: tokens for guest activities are not issued. """ + if not is_guest_token_issue_enabled(): + raise GuestTokenDisabledError() + def _get_guest_activity(): _guest_activity = { "user_mail": guest_mail, @@ -3698,6 +3747,8 @@ def validate_guest_activity_token( @param file_name: @return: """ + if not is_guest_token_accept_enabled(): + return False, None, None try: secret_key = current_app.config['WEKO_RECORDS_UI_SECRET_KEY'] decode_param = base64.b64decode(token.encode()).decode() diff --git a/modules/weko-workflow/weko_workflow/views.py b/modules/weko-workflow/weko_workflow/views.py index 8d4135ee57..64b96e7d34 100644 --- a/modules/weko-workflow/weko_workflow/views.py +++ b/modules/weko-workflow/weko_workflow/views.py @@ -93,7 +93,8 @@ get_pid_and_record, get_pid_value_by_activity_detail, \ get_record_by_root_ver, get_thumbnails, get_usage_data, \ get_workflow_item_type_names, grant_access_rights_to_all_open_restricted_files, handle_finish_workflow, \ - init_activity_for_guest_user, is_enable_item_name_link, \ + guest_token_issue_required, init_activity_for_guest_user, \ + is_enable_item_name_link, \ is_hidden_pubdate, is_show_autofill_metadata, \ is_usage_application_item_type, prepare_data_for_guest_activity, \ prepare_doi_link_workflow, process_send_approval_mails, \ @@ -630,6 +631,7 @@ def list_activity(): @workflow_blueprint.route('/activity/init-guest', methods=['POST']) +@guest_token_issue_required def init_activity_guest(): """Init workflow activity for guest user. Return URL of workflow activity made from the request body. diff --git a/modules/weko-workspace/tests/conftest.py b/modules/weko-workspace/tests/conftest.py index d9ec4ed68b..c44b33998d 100644 --- a/modules/weko-workspace/tests/conftest.py +++ b/modules/weko-workspace/tests/conftest.py @@ -184,6 +184,8 @@ def base_app(instance_path, search_class, cache_config): """Flask application fixture.""" app_ = Flask('testapp', instance_path=instance_path) app_.config.update( + WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED=True, + WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED=True, SECRET_KEY='SECRET_KEY', TESTING=True, SERVER_NAME='TEST_SERVER.localdomain', diff --git a/scripts/instance.cfg b/scripts/instance.cfg index dd1a126c08..0f3ad4fd5b 100644 --- a/scripts/instance.cfg +++ b/scripts/instance.cfg @@ -764,6 +764,10 @@ WEKO_WORKFLOW_ENABLE_AUTO_SEND_EMAIL = True WEKO_WORKFLOW_ENABLE_AUTO_SET_INDEX_FOR_ITEM_TYPE = True #--- Enable showing term of use WEKO_WORKFLOW_ENABLE_SHOWING_TERM_OF_USE = True +#--- Enable issuing tokens for guest activities +WEKO_WORKFLOW_GUEST_TOKEN_ISSUE_ENABLED = True +#--- Enable accepting tokens for guest activities +WEKO_WORKFLOW_GUEST_TOKEN_ACCEPT_ENABLED = True from weko_user_profiles.config import WEKO_USERPROFILES_GENERAL_ROLE, WEKO_USERPROFILES_GRADUATED_STUDENT_ROLE, WEKO_USERPROFILES_STUDENT_ROLE