diff --git a/modules/weko-deposit/tests/conftest.py b/modules/weko-deposit/tests/conftest.py index 92c771e83d..fb03ba0f59 100644 --- a/modules/weko-deposit/tests/conftest.py +++ b/modules/weko-deposit/tests/conftest.py @@ -53,6 +53,7 @@ from invenio_files_rest.views import blueprint as invenio_files_rest_blueprint from invenio_files_rest.models import Bucket, Location, ObjectVersion from invenio_pidstore.models import PersistentIdentifier, PIDStatus, Redirect +from invenio_records.models import RecordMetadata from invenio_i18n import InvenioI18N from weko_index_tree.api import Indexes from weko_index_tree.models import Index @@ -301,6 +302,41 @@ def db(app): # drop_database(str(db_.engine.url)) +@pytest.fixture() +def item_edit_permission_records(db): + """Create records and recids for item edit permission tests.""" + record_uuid = uuid.uuid4() + record = RecordMetadata(id=record_uuid, json={}, version_id=1) + db.session.add(record) + record_pid = PersistentIdentifier.create( + "recid", + "item-edit", + object_type="rec", + object_uuid=record_uuid, + status=PIDStatus.REGISTERED, + ) + parent_pid = PersistentIdentifier.create( + "recid", + "item-edit-parent", + object_type="rec", + object_uuid=record_uuid, + status=PIDStatus.REGISTERED, + ) + record_missing_pid = PersistentIdentifier.create( + "recid", + "item-edit-orphan", + object_type="rec", + object_uuid=uuid.uuid4(), + status=PIDStatus.REGISTERED, + ) + db.session.commit() + return { + "record_pid": record_pid.pid_value, + "parent_pid": parent_pid.pid_value, + "record_missing_pid": record_missing_pid.pid_value, + } + + @pytest.fixture() def records(db): record_data = json_data("data/test_records.json") diff --git a/modules/weko-deposit/tests/test_rest.py b/modules/weko-deposit/tests/test_rest.py index 5e61b35c7c..800f98654c 100644 --- a/modules/weko-deposit/tests/test_rest.py +++ b/modules/weko-deposit/tests/test_rest.py @@ -28,13 +28,160 @@ from elasticsearch import ElasticsearchException from sqlalchemy.exc import SQLAlchemyError -from flask import url_for +from flask import session, url_for +from werkzeug.exceptions import Forbidden, NotFound, Unauthorized from weko_deposit.api import WekoDeposit, WekoRecord +from weko_deposit.rest import require_item_edit_permission from invenio_accounts.testutils import login_user_via_session # .tox/c1/bin/pytest --cov=weko_deposit tests/test_rest.py -vv -s --cov-branch --cov-report=term --cov-report=html --basetemp=/code/modules/weko-deposit/.tox/c1/tmp + +# .tox/c1/bin/pytest --cov=weko_deposit tests/test_rest.py::test_require_item_edit_permission -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-deposit/.tox/c1/tmp +def test_require_item_edit_permission(app, item_edit_permission_records): + def create_protected_view(): + view = MagicMock(return_value="permitted") + return view, require_item_edit_permission(view) + + def permission_factory(can_edit): + permission = MagicMock() + permission.can.return_value = can_edit + return patch( + "weko_items_ui.permissions.edit_permission_factory", + return_value=permission, + ) + + # 未認証かつゲストトークンなしの場合は401を返す。 + view, protected_view = create_protected_view() + with patch( + "weko_deposit.rest.current_user", + MagicMock(is_authenticated=False), + ), app.test_request_context(): + with pytest.raises(Unauthorized) as exc_info: + protected_view(pid_value=item_edit_permission_records["record_pid"]) + + assert exc_info.value.code == 401 + view.assert_not_called() + + # 認証済みかつゲストトークンありの場合は401を返さない。 + view, protected_view = create_protected_view() + with patch( + "weko_deposit.rest.current_user", + MagicMock(is_authenticated=True), + ), permission_factory(False), app.test_request_context(): + session["guest_token"] = "guest-token" + assert protected_view( + pid_value=item_edit_permission_records["record_pid"] + ) == "permitted" + + view.assert_called_once_with( + pid_value=item_edit_permission_records["record_pid"] + ) + + # 未認証でもゲストトークンがあれば401を返さない。 + view, protected_view = create_protected_view() + with patch( + "weko_deposit.rest.current_user", + MagicMock(is_authenticated=False), + ), permission_factory(False), app.test_request_context(): + session["guest_token"] = "guest-token" + assert protected_view( + pid_value=item_edit_permission_records["record_pid"] + ) == "permitted" + + view.assert_called_once_with( + pid_value=item_edit_permission_records["record_pid"] + ) + + # バージョン付きPIDは親のrecidを使って編集権限を確認する。 + view, protected_view = create_protected_view() + with patch( + "weko_deposit.rest.current_user", + MagicMock(is_authenticated=True), + ), permission_factory(True), app.test_request_context(): + versioned_pid = "{}.1".format( + item_edit_permission_records["parent_pid"] + ) + assert protected_view(pid_value=versioned_pid) == "permitted" + + view.assert_called_once_with(pid_value=versioned_pid) + + # PIDが存在しない場合は404とItem not foundを返す。 + view, protected_view = create_protected_view() + with patch( + "weko_deposit.rest.current_user", + MagicMock(is_authenticated=True), + ), app.test_request_context(): + with pytest.raises(NotFound) as exc_info: + protected_view(pid_value="item-edit-missing") + + assert exc_info.value.code == 404 + assert exc_info.value.description == "Item not found" + view.assert_not_called() + + # PIDに対応するレコードが存在しない場合は404とItem not foundを返す。 + view, protected_view = create_protected_view() + with patch( + "weko_deposit.rest.current_user", + MagicMock(is_authenticated=True), + ), app.test_request_context(): + with pytest.raises(NotFound) as exc_info: + protected_view( + pid_value=item_edit_permission_records["record_missing_pid"] + ) + + assert exc_info.value.code == 404 + assert exc_info.value.description == "Item not found" + view.assert_not_called() + + # 編集権限もゲストトークンもない場合は403を返す。 + view, protected_view = create_protected_view() + with patch( + "weko_deposit.rest.current_user", + MagicMock(is_authenticated=True), + ), permission_factory(False), app.test_request_context(): + with pytest.raises(Forbidden) as exc_info: + protected_view( + pid_value=item_edit_permission_records["record_pid"] + ) + + assert exc_info.value.code == 403 + assert exc_info.value.description == ( + "You do not have permission to edit this item" + ) + view.assert_not_called() + + # 編集権限があればゲストトークンがなくても403を返さない。 + view, protected_view = create_protected_view() + with patch( + "weko_deposit.rest.current_user", + MagicMock(is_authenticated=True), + ), permission_factory(True), app.test_request_context(): + assert protected_view( + pid_value=item_edit_permission_records["record_pid"] + ) == "permitted" + + view.assert_called_once_with( + pid_value=item_edit_permission_records["record_pid"] + ) + + # 編集権限がなくてもゲストトークンがあれば403を返さない。 + view, protected_view = create_protected_view() + with patch( + "weko_deposit.rest.current_user", + MagicMock(is_authenticated=False), + ), permission_factory(False), app.test_request_context(): + session["guest_token"] = "guest-token" + assert protected_view( + pid_value=item_edit_permission_records["record_pid"] + ) == "permitted" + + view.assert_called_once_with( + pid_value=item_edit_permission_records["record_pid"] + ) + + # def test_publish(app, location): # deposit = WekoDeposit.create({}) # kwargs = { diff --git a/modules/weko-deposit/weko_deposit/rest.py b/modules/weko-deposit/weko_deposit/rest.py index 2e511d3174..89fbcfb900 100644 --- a/modules/weko-deposit/weko_deposit/rest.py +++ b/modules/weko-deposit/weko_deposit/rest.py @@ -24,6 +24,7 @@ import sys from functools import wraps from wsgiref.util import request_uri +from flask import session import redis from redis import sentinel @@ -69,7 +70,8 @@ def require_item_edit_permission(f): def decorated(*args, **kwargs): # 循環 import を避けるため関数内で読み込む from weko_items_ui.permissions import edit_permission_factory - if not (current_user and current_user.is_authenticated): + guest_token = session.get("guest_token") + if not (current_user and current_user.is_authenticated) and not guest_token: abort(401) pid_value = kwargs.get('pid_value') pid_value = str(getattr(pid_value, 'value', pid_value)) @@ -86,7 +88,7 @@ def decorated(*args, **kwargs): r = RecordMetadata.query.filter_by(id=pid.object_uuid).first() if r is None: abort(404, "Item not found") - if not edit_permission_factory(r.json).can(): + if not edit_permission_factory(r.json).can() and not guest_token: abort(403, "You do not have permission to edit this item") return f(*args, **kwargs) return decorated diff --git a/modules/weko-groups/tests/test_views.py b/modules/weko-groups/tests/test_views.py index 1263cec028..341f0ad648 100644 --- a/modules/weko-groups/tests/test_views.py +++ b/modules/weko-groups/tests/test_views.py @@ -48,6 +48,53 @@ dbsession_clean ) + +# def grouplist(): +def test_grouplist(app_2): + group_list = [{"id": 1, "name": "Test group"}] + + # 未認証かつゲストトークンなしの場合はログイン画面へリダイレクトする。 + with app_2.test_client() as client: + with patch( + "flask_login.utils._get_user", + return_value=MagicMock(is_authenticated=False), + ): + res = client.get(url_for("weko_groups.grouplist")) + + assert res.status_code == 302 + + # 未認証でもゲストトークンがあればグループ一覧を返す。 + with app_2.test_client() as client: + with client.session_transaction() as sess: + sess["guest_token"] = "guest-token" + + with patch( + "flask_login.utils._get_user", + return_value=MagicMock(is_authenticated=False), + ), patch( + "weko_groups.views.Group.get_group_list", + return_value=group_list, + ): + res = client.get(url_for("weko_groups.grouplist")) + + assert res.status_code == 200 + assert json.loads(res.data) == group_list + + # 認証済みユーザーの場合はグループ一覧を返す。 + with app_2.test_client() as client: + with patch( + "flask_login.utils._get_user", + return_value=MagicMock(is_authenticated=True), + ), patch( + "weko_groups.views.Group.get_group_list", + return_value=group_list, + ): + res = client.get(url_for("weko_groups.grouplist")) + + assert res.status_code == 200 + assert json.loads(res.data) == group_list + + # def sanitize_html_group(value): def test_sanitize_html_group(app): value = "value" @@ -93,7 +140,7 @@ def test__has_admin_access(app_2, db_2): # def index(): def test_index(app_2, users): - # "Encountered unknown tag 'assets'. Jinja was looking for the following tags: 'endblock'. The innermost block that needs to be closed is 'block'.", + # "Encountered unknown tag 'assets'. Jinja was looking for the following tags: 'endblock'. The innermost block that needs to be closed is 'block'.", # But upon testing on the actual url on the browser "https://localhost/accounts/settings/groups/index" there is no problem with app_2.test_request_context(): with app_2.test_client() as client: @@ -133,7 +180,7 @@ def test_invitations(app_2, users): "page": 1, "per_page": 5 } - + with patch("flask_login.utils._get_user", return_value=users[3]["obj"]): with patch("weko_groups.views.request", return_value=request): res = client.get(url_for('weko_groups.invitations')) @@ -154,7 +201,7 @@ def test_new(app_2, users, id, status_code): def validate_on_submit_True(): return True - + def validate_on_submit_False(): return False @@ -162,12 +209,12 @@ def validate_on_submit_False(): form.validate_on_submit = validate_on_submit_False group = MagicMock() group.name = "group_name" - + with app_2.test_request_context(): with app_2.test_client() as client: with patch("flask_login.utils._get_user", return_value=users[id]["obj"]): with patch("weko_groups.views.GroupForm", return_value=form): - # "Encountered unknown tag 'assets'. Jinja was looking for the following tags: 'endblock'. The innermost block that needs to be closed is 'block'.", + # "Encountered unknown tag 'assets'. Jinja was looking for the following tags: 'endblock'. The innermost block that needs to be closed is 'block'.", # But upon testing on the actual url on the browser "https://localhost/accounts/settings/groups/new" there is no problem try: client.get(url_for('weko_groups.new')) @@ -201,7 +248,7 @@ def validate_on_submit_False(): def test_manage(app_2, users, id, status_code): def validate_on_submit_True(): return True - + def can_edit_True(item): return True @@ -220,7 +267,7 @@ def update_func(item): with app_2.test_request_context(): with app_2.test_client() as client: - # "Encountered unknown tag 'assets'. Jinja was looking for the following tags: 'endblock'. The innermost block that needs to be closed is 'block'.", + # "Encountered unknown tag 'assets'. Jinja was looking for the following tags: 'endblock'. The innermost block that needs to be closed is 'block'.", # But upon testing on the actual url on the browser "https://localhost/accounts/settings/groups/1" there is no problem # But upon testing on the actual url on the browser "https://localhost/accounts/settings/groups/1/manage" there is no problem with patch("flask_login.utils._get_user", return_value=users[id]["obj"]): @@ -231,7 +278,7 @@ def update_func(item): assert res.status_code == status_code except: pass - + def test_manage_2(app_2, users): def validate_on_submit_True(): @@ -239,9 +286,9 @@ def validate_on_submit_True(): form = MagicMock() form.validate_on_submit = validate_on_submit_True - + with app_2.test_request_context(): - # "Encountered unknown tag 'assets'. Jinja was looking for the following tags: 'endblock'. The innermost block that needs to be closed is 'block'.", + # "Encountered unknown tag 'assets'. Jinja was looking for the following tags: 'endblock'. The innermost block that needs to be closed is 'block'.", # But upon testing on the actual url on the browser "https://localhost/accounts/settings/groups/1" there is no problem # But upon testing on the actual url on the browser "https://localhost/accounts/settings/groups/1/manage" there is no problem group = Group.create(name="name") @@ -285,7 +332,7 @@ def delete_func(): with patch("weko_groups.views.Group", return_value=group): res = client.post(url_for('weko_groups.delete', group_id=1)) assert res.status_code == status_code - + def test_delete_2(app_2, users): with app_2.test_request_context(): @@ -359,7 +406,7 @@ def can_leave_True(item): with patch("weko_groups.views.Group", return_value=group): res = client.post(url_for('weko_groups.leave', group_id=1)) assert res.status_code == 302 - + def test_leave_2(app_2, users): with app_2.test_request_context(): @@ -370,7 +417,7 @@ def test_leave_2(app_2, users): assert res.status_code == 302 -# def approve(group_id, user_id): +# def approve(group_id, user_id): user_results = [ (0, 403), (1, 302), @@ -392,7 +439,7 @@ def can_edit_True(item): with patch("weko_groups.views.Membership", return_value=membership): res = client.post(url_for('weko_groups.approve', group_id=1, user_id=users[3]["obj"].id)) assert res.status_code == status_code - + def test_approve_2(app_2, users): with app_2.test_request_context(): @@ -403,7 +450,7 @@ def test_approve_2(app_2, users): assert res.status_code == 404 -# def remove(group_id, user_id): +# def remove(group_id, user_id): user_results = [ (0, 403), (1, 302), @@ -425,7 +472,7 @@ def can_edit_True(item): with patch("weko_groups.views.Group", return_value=group): res = client.post(url_for('weko_groups.remove', group_id=1, user_id=users[3]["obj"].id)) assert res.status_code == status_code - + def test_remove_2(app_2, users): with app_2.test_request_context(): @@ -436,11 +483,11 @@ def test_remove_2(app_2, users): assert res.status_code == 302 -# def accept(group_id): +# def accept(group_id): def test_accept(app_2, users): def can_edit_True(item): return True - + def accept(): return True @@ -456,11 +503,11 @@ def accept(): assert res.status_code == 302 -# def reject(group_id): +# def reject(group_id): def test_reject(app_2, users): def can_edit_True(item): return True - + def reject(): return True @@ -488,10 +535,10 @@ def reject(): def test_new_member(app_2, users, id, status_code): def validate_on_submit_True(): return True - + def can_invite_others(item): return True - + def invite_by_emails(item): return True @@ -504,7 +551,7 @@ def invite_by_emails(item): with app_2.test_request_context(): # "Encountered unknown tag 'assets'. Jinja was looking for the following tags: 'endblock'. The innermost block that needs to be closed is 'block'." - # But upon testing on the actual url on the browser "https://localhost/accounts/settings/groups/1/members/new" there is no problem + # But upon testing on the actual url on the browser "https://localhost/accounts/settings/groups/1/members/new" there is no problem with app_2.test_client() as client: with patch("flask_login.utils._get_user", return_value=users[id]["obj"]): with patch("weko_groups.views.Group", return_value=group): @@ -525,10 +572,10 @@ def invite_by_emails(item): def test_new_member_2(app_2, users): def validate_on_submit_True(): return True - + def can_invite_others(item): return True - + def invite_by_emails(item): return True @@ -541,7 +588,7 @@ def invite_by_emails(item): with app_2.test_request_context(): # "Encountered unknown tag 'assets'. Jinja was looking for the following tags: 'endblock'. The innermost block that needs to be closed is 'block'." - # But upon testing on the actual url on the browser "https://localhost/accounts/settings/groups/1/members/new" there is no problem + # But upon testing on the actual url on the browser "https://localhost/accounts/settings/groups/1/members/new" there is no problem with app_2.test_client() as client: with patch("flask_login.utils._get_user", return_value=users[3]["obj"]): with patch("weko_groups.views.Group", return_value=group): @@ -549,7 +596,7 @@ def invite_by_emails(item): client.get(url_for('weko_groups.new_member', group_id=1)) except: pass - + def test_new_member_3(app_2, users): with app_2.test_request_context(): @@ -566,6 +613,5 @@ def test_remove_csrf(app): form.data = { "not_csrf_token_key": "not_csrf_token_value" } - - assert remove_csrf(form=form) != None + assert remove_csrf(form=form) != None diff --git a/modules/weko-groups/weko_groups/views.py b/modules/weko-groups/weko_groups/views.py index 162231d86f..58a8e14cd8 100644 --- a/modules/weko-groups/weko_groups/views.py +++ b/modules/weko-groups/weko_groups/views.py @@ -33,7 +33,7 @@ from six.moves.urllib.parse import urlparse from sqlalchemy.exc import IntegrityError from invenio_db import db -from weko_accounts.utils import roles_required +from weko_accounts.utils import roles_required, login_required_customize from weko_admin.config import WEKO_ADMIN_PERMISSION_ROLE_REPO, \ WEKO_ADMIN_PERMISSION_ROLE_SYSTEM, WEKO_ADMIN_PERMISSION_ROLE_COMMUNITY @@ -119,7 +119,7 @@ def groupcount(): @blueprint.route('/grouplist', methods=['GET']) -@login_required +@login_required_customize def grouplist(): """ Get logined group list info. @@ -635,4 +635,4 @@ def dbsession_clean(exception): db.session.commit() except: db.session.rollback() - db.session.remove() \ No newline at end of file + db.session.remove() diff --git a/modules/weko-items-ui/tests/test_views.py b/modules/weko-items-ui/tests/test_views.py index 0624b2763f..eb27482032 100644 --- a/modules/weko-items-ui/tests/test_views.py +++ b/modules/weko-items-ui/tests/test_views.py @@ -9,8 +9,9 @@ import os import pytest from sqlalchemy.exc import SQLAlchemyError -from flask import json, url_for, make_response +from flask import json, session, url_for, make_response from jinja2.exceptions import TemplateNotFound +from werkzeug.exceptions import Forbidden, NotFound from invenio_accounts.testutils import login_user_via_session from invenio_oauth2server.provider import oauth2 @@ -27,9 +28,90 @@ default_view_method, prepare_delete_item, prepare_edit_item, + require_activity_user_or_item_access, to_links_js, ) +# .tox/c1/bin/pytest --cov=weko_items_ui tests/test_views.py::test_require_activity_user_or_item_access -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-items-ui/.tox/c1/tmp +def test_require_activity_user_or_item_access(app): + def protected_view(): + return "permitted" + + activity = MagicMock(extra_info={"user_mail": "owner@example.org"}) + + # ゲストトークンがあれば403を返さない。 + with patch( + "weko_items_ui.views.WorkActivity.get_activity_by_id", + return_value=activity, + ), patch( + "weko_items_ui.views.item_permission.can", + return_value=False, + ), patch( + "weko_items_ui.views.current_user", + MagicMock(email="other@example.org"), + ), app.test_request_context(json={"activity_id": "activity-1"}): + session["guest_token"] = "guest-token" + assert require_activity_user_or_item_access( + protected_view + )() == "permitted" + + # item-access があれば403を返さない。 + with patch( + "weko_items_ui.views.WorkActivity.get_activity_by_id", + return_value=activity, + ), patch( + "weko_items_ui.views.item_permission.can", + return_value=True, + ), patch( + "weko_items_ui.views.current_user", + MagicMock(email="other@example.org"), + ), app.test_request_context(json={"activity_id": "activity-1"}): + assert require_activity_user_or_item_access( + protected_view + )() == "permitted" + + # アクティビティ作成者のメールアドレスと一致すれば403を返さない。 + with patch( + "weko_items_ui.views.WorkActivity.get_activity_by_id", + return_value=activity, + ), patch( + "weko_items_ui.views.item_permission.can", + return_value=False, + ), patch( + "weko_items_ui.views.current_user", + MagicMock(email="owner@example.org"), + ), app.test_request_context(json={"activity_id": "activity-1"}): + assert require_activity_user_or_item_access( + protected_view + )() == "permitted" + + # 対象アクティビティが存在しない場合は404を返す。 + with patch( + "weko_items_ui.views.WorkActivity.get_activity_by_id", + return_value=None, + ), app.test_request_context(json={"activity_id": "activity-missing"}): + with pytest.raises(NotFound) as exc_info: + require_activity_user_or_item_access(protected_view)() + + assert exc_info.value.code == 404 + + # ゲストトークン、item-access、メールアドレス一致がない場合は403を返す。 + activity = MagicMock(extra_info={"user_mail": []}) + with patch( + "weko_items_ui.views.WorkActivity.get_activity_by_id", + return_value=activity, + ), patch( + "weko_items_ui.views.item_permission.can", + return_value=False, + ), patch( + "weko_items_ui.views.current_user", + MagicMock(email="other@example.org"), + ), app.test_request_context(json={"activity_id": "activity-1"}): + with pytest.raises(Forbidden) as exc_info: + require_activity_user_or_item_access(protected_view)() + + assert exc_info.value.code == 403 + # def index(item_type_id=0): # .tox/c1/bin/pytest --cov=weko_items_ui tests/test_views.py::test_index_acl_nologin -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-items-ui/.tox/c1/tmp @@ -20978,9 +21060,18 @@ def test_get_search_data_acl_user(client_api, users, db_userprofile, db_sessionl (7, 403), # user item-access なし ], ) -def test_validate_user_email_and_index_login(client_api, users, id, status_code): +def test_validate_user_email_and_index_login( + client_api, db_sessionlifetime, users, id, status_code +): login_user_via_session(client=client_api, email=users[id]["email"]) - with patch("weko_items_ui.views.validate_user_mail_and_index", return_value=""): + activity = MagicMock(extra_info={"user_mail": []}) + with patch( + "weko_items_ui.views.WorkActivity.get_activity_by_id", + return_value=activity, + ), patch( + "weko_items_ui.views.validate_user_mail_and_index", + return_value="", + ): res = client_api.post( "/api/items/validate_email_and_index", data=json.dumps({}), @@ -20990,7 +21081,9 @@ def test_validate_user_email_and_index_login(client_api, users, id, status_code) # .tox/c1/bin/pytest --cov=weko_items_ui tests/test_views.py::test_validate_user_email_and_index_guest -v --cov-branch --cov-report=term --basetemp=/code/modules/weko-items-ui/.tox/c1/tmp -def test_validate_user_email_and_index_guest(client_api, users): +def test_validate_user_email_and_index_guest( + client_api, db_sessionlifetime, users +): with patch("weko_items_ui.views.validate_user_mail_and_index", return_value=""): res = client_api.post( "/api/items/validate_email_and_index", @@ -20999,6 +21092,100 @@ def test_validate_user_email_and_index_guest(client_api, users): ) assert res.status_code == 401 +# .tox/c1/bin/pytest --cov=weko_items_ui tests/test_views.py::test_validate_user_email_and_index_rejects_non_json_content_type -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-items-ui/.tox/c1/tmp +def test_validate_user_email_and_index_rejects_non_json_content_type( + client_api, db_sessionlifetime, users +): + login_user_via_session(client=client_api, email=users[0]["email"]) + + res = client_api.post( + "/api/items/validate_email_and_index", + data="{}", + content_type="text/plain", + ) + + assert res.status_code == 200 + assert json.loads(res.data)["error"] == "Header Error" + + +# .tox/c1/bin/pytest --cov=weko_items_ui tests/test_views.py::test_validate_user_email_and_index_with_guest_token -v --cov-branch --cov-report=term --basetemp=/code/modules/weko-items-ui/.tox/c1/tmp +def test_validate_user_email_and_index_with_guest_token( + client_api, db_sessionlifetime +): + with client_api.session_transaction() as sess: + sess["guest_token"] = "guest-token" + + activity = MagicMock(extra_info={"user_mail": []}) + with patch( + "weko_items_ui.views.WorkActivity.get_activity_by_id", + return_value=activity, + ), patch( + "weko_items_ui.views.validate_user_mail_and_index", + return_value="", + ): + res = client_api.post( + "/api/items/validate_email_and_index", + data=json.dumps({}), + content_type="application/json", + ) + + assert res.status_code == 200 + +# .tox/c1/bin/pytest --cov=weko_items_ui tests/test_views.py::test_validate_user_email_and_index_checks_activity_email -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-items-ui/.tox/c1/tmp +def test_validate_user_email_and_index_checks_activity_email( + client_api, db_sessionlifetime, users +): + login_user_via_session(client=client_api, email=users[4]["email"]) + request_data = {"activity_id": "activity-1"} + + activity = MagicMock( + extra_info={ + "user_mail": [users[4]["email"]], + } + ) + with patch( + "weko_items_ui.views.WorkActivity.get_activity_by_id", + return_value=activity, + ), patch( + "weko_items_ui.views.validate_user_mail_and_index", + return_value="", + ): + res = client_api.post( + "/api/items/validate_email_and_index", + data=json.dumps(request_data), + content_type="application/json", + ) + + assert res.status_code == 200 + + activity.extra_info = { + "user_mail": ["another-user@example.org"], + } + with patch( + "weko_items_ui.views.WorkActivity.get_activity_by_id", + return_value=activity, + ): + res = client_api.post( + "/api/items/validate_email_and_index", + data=json.dumps(request_data), + content_type="application/json", + ) + + assert res.status_code == 403 + + activity.extra_info = {} + with patch( + "weko_items_ui.views.WorkActivity.get_activity_by_id", + return_value=activity, + ): + res = client_api.post( + "/api/items/validate_email_and_index", + data=json.dumps(request_data), + content_type="application/json", + ) + + assert res.status_code == 403 + # def validate_user_info(): # .tox/c1/bin/pytest --cov=weko_items_ui tests/test_views.py::test_validate_user_info_login -v --cov-branch --cov-report=term --basetemp=/code/modules/weko-items-ui/.tox/c1/tmp diff --git a/modules/weko-items-ui/weko_items_ui/views.py b/modules/weko-items-ui/weko_items_ui/views.py index 2ff3f5ce49..220d46b333 100644 --- a/modules/weko-items-ui/weko_items_ui/views.py +++ b/modules/weko-items-ui/weko_items_ui/views.py @@ -32,6 +32,7 @@ from copy import deepcopy from datetime import date, datetime, timedelta +from functools import wraps from flask import ( Blueprint, abort, current_app, flash, jsonify, redirect, @@ -123,6 +124,37 @@ url_prefix="/items", ) + +def require_activity_user_or_item_access(func): + """Require activity email, item access, or a guest token.""" + + @wraps(func) + def decorated_view(*args, **kwargs): + if request.headers.get('Content-Type') != 'application/json': + return func(*args, **kwargs) + + data = request.get_json() + activity_id = data.get('activity_id') + activity = WorkActivity().get_activity_by_id(activity_id) + if activity is None: + abort(404) + + user_email = getattr(current_user, "email", None) + mail_addresses = (activity.extra_info or {}).get('user_mail', []) + if isinstance(mail_addresses, str): + mail_addresses = [mail_addresses] + + if ( + not session.get("guest_token") + and not item_permission.can() + and user_email not in mail_addresses + ): + abort(403) + + return func(*args, **kwargs) + + return decorated_view + # OAポリシー取得エンドポイント @blueprint.route("/api/oa_policies", methods=["GET"]) @login_required @@ -861,8 +893,8 @@ def get_search_data(data_type=''): @blueprint_api.route('/validate_email_and_index', methods=['POST']) -@login_required -@item_permission.require(http_exception=403) +@login_required_customize +@require_activity_user_or_item_access def validate_user_email_and_index(): """Validate user mail and index.