From 8a204a6edb77e31928b32acf2b7ca1719e1598bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Frnka?= Date: Fri, 14 Aug 2026 10:21:20 +0200 Subject: [PATCH 1/4] Updated permissions --- .../ai_document_plugin_service/api/auth.py | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/service/src/ai_document_plugin_service/api/auth.py b/service/src/ai_document_plugin_service/api/auth.py index 6e06671..913f942 100644 --- a/service/src/ai_document_plugin_service/api/auth.py +++ b/service/src/ai_document_plugin_service/api/auth.py @@ -12,6 +12,7 @@ DSW_USER_VALIDATION_TIMEOUT_SECONDS = 10.0 DSW_USER_VALIDATION_SUCCESS_STATUS = 200 DSW_ADMIN_ROLE = 'admin' +DSW_ADMIN_PERMISSION = 'SettingsManageRolePermission' @dataclass(frozen=True) @@ -20,11 +21,7 @@ class AuthenticatedUser: api_url: str user_uuid: UUID tenant_uuid: UUID - role: str - - @property - def is_admin(self) -> bool: - return self.role.strip().lower() == DSW_ADMIN_ROLE + is_admin: bool def is_allowed_request(api_url: str, tenant_uuid: UUID, allowed_apis: tuple[AllowedApi, ...]) -> bool: @@ -77,16 +74,23 @@ def _fetch_dsw_user(api_url: str, token: str) -> dict[str, object] | None: return user -def _extract_role(user: dict[str, object], api_url: str) -> str: - role = user.get('role') - if not isinstance(role, str) or not role.strip(): - msg = ( - f'Unexpected response from DSW at {api_url}: the /users/current payload ' - f'did not include a valid string "role" (got {role!r}). ' - f'The tenant may be running an incompatible DSW version. Received payload: {user}' - ) - raise ValueError(msg) - return role +def _is_admin(user: dict[str, object], api_url: str) -> bool: + if 'role' in user: + # Handle DSW version < 0.4.33 + role = user.get('role') + return role == DSW_ADMIN_ROLE + if 'permissions' in user: + # Handle DSW version >= 0.4.33 + permissions = user.get('permissions') + if not isinstance(permissions, list): + return DSW_ADMIN_PERMISSION in permissions + return False + msg = ( + f'Unexpected response from DSW at {api_url}: the /users/current payload ' + f'did not include a valid "role" or "permissions" field. ' + f'The tenant may be running an incompatible DSW version. Received payload: {user}' + ) + raise ValueError(msg) def verify_authenticated( @@ -116,11 +120,10 @@ def verify_authenticated( if user is None: raise fastapi.HTTPException(status_code=401, detail='Unauthorized') - role = _extract_role(user, normalized_api_url) return AuthenticatedUser( token=token, api_url=normalized_api_url, user_uuid=user_uuid, tenant_uuid=tenant_uuid, - role=role, + is_admin=_is_admin(user, normalized_api_url), ) From 3dafee7fa44f8e809422b3a2c2f067fe915aeebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Frnka?= Date: Fri, 14 Aug 2026 10:25:04 +0200 Subject: [PATCH 2/4] Fix lint --- .../ai_document_plugin_service/api/auth.py | 2 +- .../tests/service/test_template_service.py | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/service/src/ai_document_plugin_service/api/auth.py b/service/src/ai_document_plugin_service/api/auth.py index 913f942..53fa716 100644 --- a/service/src/ai_document_plugin_service/api/auth.py +++ b/service/src/ai_document_plugin_service/api/auth.py @@ -82,7 +82,7 @@ def _is_admin(user: dict[str, object], api_url: str) -> bool: if 'permissions' in user: # Handle DSW version >= 0.4.33 permissions = user.get('permissions') - if not isinstance(permissions, list): + if isinstance(permissions, list): return DSW_ADMIN_PERMISSION in permissions return False msg = ( diff --git a/service/tests/service/test_template_service.py b/service/tests/service/test_template_service.py index df6c686..21514e3 100644 --- a/service/tests/service/test_template_service.py +++ b/service/tests/service/test_template_service.py @@ -38,13 +38,13 @@ def _database() -> AsyncMock: return database -def _user(*, role: str = 'researcher', user_uuid: uuid.UUID = USER_UUID) -> AuthenticatedUser: +def _user(*, is_admin: bool = False, user_uuid: uuid.UUID = USER_UUID) -> AuthenticatedUser: return AuthenticatedUser( token='token', api_url='https://dsw.example.com/wizard-api', user_uuid=user_uuid, tenant_uuid=TENANT_UUID, - role=role, + is_admin=is_admin, ) @@ -144,7 +144,7 @@ async def test_create_tenant_template_by_admin_has_no_owner() -> None: database.create_template.return_value = template_uuid payload = TemplateCreateRequest(title='Common', content=VALID_CONTENT, scope=TemplateScope.TENANT) - detail = await TemplateService(database).create(_user(role='admin'), payload) + detail = await TemplateService(database).create(_user(is_admin=True), payload) database.create_template.assert_awaited_once_with( title='Common', @@ -160,7 +160,7 @@ async def test_create_tenant_template_by_non_admin_is_denied() -> None: payload = TemplateCreateRequest(title='Common', content=VALID_CONTENT, scope=TemplateScope.TENANT) with pytest.raises(AccessDeniedError): - await TemplateService(database).create(_user(role='researcher'), payload) + await TemplateService(database).create(_user(is_admin=False), payload) database.create_template.assert_not_awaited() @@ -172,7 +172,7 @@ async def test_create_authorizes_before_validating() -> None: payload = TemplateCreateRequest(title='', content={}, scope=TemplateScope.TENANT) with pytest.raises(AccessDeniedError): - await TemplateService(database).create(_user(role='researcher'), payload) + await TemplateService(database).create(_user(is_admin=False), payload) async def test_create_rejects_blank_title() -> None: @@ -228,7 +228,7 @@ async def test_update_tenant_template_by_admin() -> None: database.update_template.return_value = True payload = TemplateUpdateRequest(title='Renamed', content=VALID_CONTENT) - detail = await TemplateService(database).update(_user(role='admin'), template_uuid, payload) + detail = await TemplateService(database).update(_user(is_admin=True), template_uuid, payload) assert detail.scope is TemplateScope.TENANT database.update_template.assert_awaited_once() @@ -241,7 +241,7 @@ async def test_update_tenant_template_by_non_admin_is_denied() -> None: payload = TemplateUpdateRequest(title='Renamed', content=VALID_CONTENT) with pytest.raises(AccessDeniedError): - await TemplateService(database).update(_user(role='researcher'), template_uuid, payload) + await TemplateService(database).update(_user(is_admin=False), template_uuid, payload) database.update_template.assert_not_awaited() @@ -293,7 +293,7 @@ async def test_delete_tenant_template_by_admin() -> None: database = _database() database.get_template.return_value = _record(template_uuid=template_uuid, user_uuid=None) - await TemplateService(database).delete(_user(role='admin'), template_uuid) + await TemplateService(database).delete(_user(is_admin=True), template_uuid) database.delete_template.assert_awaited_once_with(template_uuid, TENANT_UUID) @@ -304,7 +304,7 @@ async def test_delete_tenant_template_by_non_admin_is_denied() -> None: database.get_template.return_value = _record(template_uuid=template_uuid, user_uuid=None) with pytest.raises(AccessDeniedError): - await TemplateService(database).delete(_user(role='researcher'), template_uuid) + await TemplateService(database).delete(_user(is_admin=False), template_uuid) database.delete_template.assert_not_awaited() @@ -325,7 +325,7 @@ async def test_delete_other_users_by_admin_is_hidden() -> None: database.get_template.return_value = _record(template_uuid=template_uuid, user_uuid=OTHER_USER_UUID) with pytest.raises(NotFoundError): - await TemplateService(database).delete(_user(role="admin"), template_uuid) + await TemplateService(database).delete(_user(is_admin=True), template_uuid) database.delete_template.assert_not_awaited() From 03671b2af893bda86657e1c01afe9755e3292ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Frnka?= Date: Fri, 14 Aug 2026 10:33:18 +0200 Subject: [PATCH 3/4] Fix logic --- .../src/ai_document_plugin_service/api/auth.py | 15 +++++++-------- service/tests/api/test_auth.py | 8 +++++++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/service/src/ai_document_plugin_service/api/auth.py b/service/src/ai_document_plugin_service/api/auth.py index 53fa716..48647c5 100644 --- a/service/src/ai_document_plugin_service/api/auth.py +++ b/service/src/ai_document_plugin_service/api/auth.py @@ -75,19 +75,18 @@ def _fetch_dsw_user(api_url: str, token: str) -> dict[str, object] | None: def _is_admin(user: dict[str, object], api_url: str) -> bool: - if 'role' in user: - # Handle DSW version < 0.4.33 - role = user.get('role') + role = user.get('role') + if isinstance(role, str): + # DSW version < 0.4.33 return role == DSW_ADMIN_ROLE - if 'permissions' in user: - # Handle DSW version >= 0.4.33 - permissions = user.get('permissions') + if isinstance(role, dict): + # DSW version >= 0.4.33 + permissions = role.get('permissions') if isinstance(permissions, list): return DSW_ADMIN_PERMISSION in permissions - return False msg = ( f'Unexpected response from DSW at {api_url}: the /users/current payload ' - f'did not include a valid "role" or "permissions" field. ' + f'did not include a valid "role" string or a "role" object with a "permissions" list. ' f'The tenant may be running an incompatible DSW version. Received payload: {user}' ) raise ValueError(msg) diff --git a/service/tests/api/test_auth.py b/service/tests/api/test_auth.py index 6b4c3c5..2b948bf 100644 --- a/service/tests/api/test_auth.py +++ b/service/tests/api/test_auth.py @@ -113,7 +113,13 @@ def test_protected_route_succeeds_when_dsw_validates_user( mock_httpx_get.return_value = httpx.Response( 200, request=httpx.Request('GET', ALLOWED_URL), - json={'role': 'researcher'}, + json={ + 'role': { + 'name': 'Researcher', + 'permissions': ['ProjectsViewRolePermission'], + 'uuid': '31ccc093-3ab0-4459-b109-ab1d8dc2313f', + } + }, ) template_uuid = UUID('99999999-9999-9999-9999-999999999999') mock_postgres_db.return_value.list_templates = AsyncMock( From 075947ea9e6169c5b400a1c4aa62c00fe7b81dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Frnka?= Date: Fri, 14 Aug 2026 10:35:24 +0200 Subject: [PATCH 4/4] Added test --- service/tests/api/test_auth.py | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/service/tests/api/test_auth.py b/service/tests/api/test_auth.py index 2b948bf..ecf1411 100644 --- a/service/tests/api/test_auth.py +++ b/service/tests/api/test_auth.py @@ -143,3 +143,39 @@ def test_protected_route_succeeds_when_dsw_validates_user( assert response.status_code == 200 assert response.json() == [{'uuid': str(template_uuid), 'title': 'Template 1', 'scope': 'tenant'}] mock_httpx_get.assert_called_once() + + +@patch('ai_document_plugin_service.api.auth.httpx.get') +@patch('ai_document_plugin_service.di.PostgresDB') +def test_protected_route_succeeds_when_dsw_validates_user( + mock_postgres_db: MagicMock, + mock_httpx_get: MagicMock, + monkeypatch, +) -> None: + mock_httpx_get.return_value = httpx.Response( + 200, + request=httpx.Request('GET', ALLOWED_URL), + json={'role': 'researcher'}, # Test DSW version below 4.33 + ) + template_uuid = UUID('99999999-9999-9999-9999-999999999999') + mock_postgres_db.return_value.list_templates = AsyncMock( + return_value=[ + TemplateRecord( + uuid=template_uuid, + title='Template 1', + content={'sections': []}, + tenant_uuid=UUID(ALLOWED_TENANT_UUID), + user_uuid=None, + ), + ], + ) + mock_postgres_db.return_value.dispose = AsyncMock() + + _use_test_config(monkeypatch) + + client = TestClient(create_app(run_migrations=False)) + response = client.get('/templates', headers=_auth_headers()) + + assert response.status_code == 200 + assert response.json() == [{'uuid': str(template_uuid), 'title': 'Template 1', 'scope': 'tenant'}] + mock_httpx_get.assert_called_once() \ No newline at end of file