refactor(idm): share directory dependency resolution
Module Package Release / publish-packages (push) Successful in 18s
Module Package Release / publish-packages (push) Successful in 18s
Release v0.1.26. Coordinated integrity review: GovOPlaN/govoplan-core#298.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/idm-webui",
|
"name": "@govoplan/idm-webui",
|
||||||
"version": "0.1.25",
|
"version": "0.1.26",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-idm"
|
name = "govoplan-idm"
|
||||||
version = "0.1.25"
|
version = "0.1.26"
|
||||||
description = "GovOPlaN identity management bridge module."
|
description = "GovOPlaN identity management bridge module."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""IDM route dependencies; resolve the current optional Core capability per call."""
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||||
|
|
||||||
|
|
||||||
|
def require_identity_directory(registry: object | None) -> IdentityDirectory:
|
||||||
|
if registry is None or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="Identity directory is unavailable",
|
||||||
|
)
|
||||||
|
capability = registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
|
||||||
|
if not isinstance(capability, IdentityDirectory):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}",
|
||||||
|
)
|
||||||
|
return capability
|
||||||
@@ -15,7 +15,6 @@ from govoplan_core.core.events import (
|
|||||||
emit_platform_event,
|
emit_platform_event,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.identity import (
|
from govoplan_core.core.identity import (
|
||||||
CAPABILITY_IDENTITY_DIRECTORY,
|
|
||||||
IdentityDirectory,
|
IdentityDirectory,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.idm import (
|
from govoplan_core.core.idm import (
|
||||||
@@ -34,6 +33,7 @@ from govoplan_idm.backend.db.models import (
|
|||||||
IdmTypedGroup,
|
IdmTypedGroup,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .directory_dependencies import require_identity_directory
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
IdentityRelationshipCreateRequest,
|
IdentityRelationshipCreateRequest,
|
||||||
IdentityRelationshipDecisionItem,
|
IdentityRelationshipDecisionItem,
|
||||||
@@ -154,19 +154,7 @@ def _tenant_row(session: Session, model, item_id: str, tenant_id: str, label: st
|
|||||||
|
|
||||||
|
|
||||||
def _identity_directory() -> IdentityDirectory:
|
def _identity_directory() -> IdentityDirectory:
|
||||||
registry = get_registry()
|
return require_identity_directory(get_registry())
|
||||||
if registry is None or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="Identity directory is unavailable",
|
|
||||||
)
|
|
||||||
capability = registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
|
|
||||||
if not isinstance(capability, IdentityDirectory):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}",
|
|
||||||
)
|
|
||||||
return capability
|
|
||||||
|
|
||||||
|
|
||||||
def _relationship_directory() -> IdmRelationshipDirectory:
|
def _relationship_directory() -> IdmRelationshipDirectory:
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from govoplan_core.core.configuration_control import (
|
|||||||
)
|
)
|
||||||
from govoplan_core.core.principal_cache import invalidate_auth_principals
|
from govoplan_core.core.principal_cache import invalidate_auth_principals
|
||||||
from govoplan_core.core.identity import (
|
from govoplan_core.core.identity import (
|
||||||
CAPABILITY_IDENTITY_DIRECTORY,
|
|
||||||
CAPABILITY_IDENTITY_SEARCH,
|
CAPABILITY_IDENTITY_SEARCH,
|
||||||
IdentityDirectory,
|
IdentityDirectory,
|
||||||
IdentityRef,
|
IdentityRef,
|
||||||
@@ -44,6 +43,7 @@ from govoplan_idm.backend.assignment_transitions import (
|
|||||||
from govoplan_idm.backend.assignment_events import emit_assignment_event
|
from govoplan_idm.backend.assignment_events import emit_assignment_event
|
||||||
from govoplan_idm.backend.db.models import IdmOrganizationFunctionAssignment, IdmTenantSettings
|
from govoplan_idm.backend.db.models import IdmOrganizationFunctionAssignment, IdmTenantSettings
|
||||||
|
|
||||||
|
from .directory_dependencies import require_identity_directory
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
IdmSettingsItem,
|
IdmSettingsItem,
|
||||||
IdmSettingsUpdateRequest,
|
IdmSettingsUpdateRequest,
|
||||||
@@ -187,19 +187,7 @@ def _default_settings(tenant_id: str) -> IdmSettingsItem:
|
|||||||
|
|
||||||
|
|
||||||
def _identity_directory() -> IdentityDirectory:
|
def _identity_directory() -> IdentityDirectory:
|
||||||
registry = get_registry()
|
return require_identity_directory(get_registry())
|
||||||
if registry is None or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="Identity directory is unavailable",
|
|
||||||
)
|
|
||||||
capability = registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
|
|
||||||
if not isinstance(capability, IdentityDirectory):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}",
|
|
||||||
)
|
|
||||||
return capability
|
|
||||||
|
|
||||||
|
|
||||||
def _identity_search() -> IdentitySearchProvider:
|
def _identity_search() -> IdentitySearchProvider:
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ from govoplan_idm.backend.search_source import create_idm_search_source
|
|||||||
from govoplan_idm.backend.scim import SCIM_EXTERNAL_PROVIDER_ID
|
from govoplan_idm.backend.scim import SCIM_EXTERNAL_PROVIDER_ID
|
||||||
|
|
||||||
|
|
||||||
MODULE_VERSION = "0.1.25"
|
MODULE_VERSION = "0.1.26"
|
||||||
|
|
||||||
IDM_READ_SCOPES = (
|
IDM_READ_SCOPES = (
|
||||||
"idm:organization_assignment:read",
|
"idm:organization_assignment:read",
|
||||||
|
|||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||||
|
from govoplan_idm.backend.api.v1 import function_changes, relationships, routes
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryDependencyTests(unittest.TestCase):
|
||||||
|
def test_identical_routes_preserve_missing_invalid_and_success_contracts(self) -> None:
|
||||||
|
valid = Mock(spec=IdentityDirectory)
|
||||||
|
for route in (relationships, routes):
|
||||||
|
for registry, expected_status, expected_detail in (
|
||||||
|
(None, 503, "Identity directory is unavailable"),
|
||||||
|
(Mock(has_capability=Mock(return_value=False)), 503, "Identity directory is unavailable"),
|
||||||
|
(Mock(has_capability=Mock(return_value=True), require_capability=Mock(return_value=object())), 500,
|
||||||
|
f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}"),
|
||||||
|
):
|
||||||
|
with self.subTest(route=route.__name__, status=expected_status), patch.object(route, "get_registry", return_value=registry):
|
||||||
|
with self.assertRaises(HTTPException) as caught:
|
||||||
|
route._identity_directory()
|
||||||
|
self.assertEqual(expected_status, caught.exception.status_code)
|
||||||
|
self.assertEqual(expected_detail, caught.exception.detail)
|
||||||
|
registry = Mock(has_capability=Mock(return_value=True), require_capability=Mock(return_value=valid))
|
||||||
|
with patch.object(route, "get_registry", return_value=registry):
|
||||||
|
self.assertIs(valid, route._identity_directory())
|
||||||
|
registry.has_capability.assert_called_once_with(CAPABILITY_IDENTITY_DIRECTORY)
|
||||||
|
registry.require_capability.assert_called_once_with(CAPABILITY_IDENTITY_DIRECTORY)
|
||||||
|
|
||||||
|
def test_each_call_resolves_current_registry_without_caching_authority(self) -> None:
|
||||||
|
for route in (relationships, routes):
|
||||||
|
valid = Mock(spec=IdentityDirectory)
|
||||||
|
registry = Mock(has_capability=Mock(return_value=True), require_capability=Mock(return_value=valid))
|
||||||
|
with patch.object(route, "get_registry", side_effect=[registry, None]) as get_registry:
|
||||||
|
self.assertIs(valid, route._identity_directory())
|
||||||
|
with self.assertRaises(HTTPException) as caught:
|
||||||
|
route._identity_directory()
|
||||||
|
self.assertEqual(503, caught.exception.status_code)
|
||||||
|
self.assertEqual(2, get_registry.call_count)
|
||||||
|
|
||||||
|
def test_lookup_failure_is_not_silently_replaced_or_retried(self) -> None:
|
||||||
|
failure = RuntimeError("registry changed during lookup")
|
||||||
|
for route in (relationships, routes):
|
||||||
|
registry = Mock(has_capability=Mock(return_value=True), require_capability=Mock(side_effect=failure))
|
||||||
|
with patch.object(route, "get_registry", return_value=registry), self.assertRaises(RuntimeError) as caught:
|
||||||
|
route._identity_directory()
|
||||||
|
self.assertIs(failure, caught.exception)
|
||||||
|
self.assertEqual(1, registry.require_capability.call_count)
|
||||||
|
|
||||||
|
def test_function_changes_keeps_its_distinct_unavailable_contract(self) -> None:
|
||||||
|
registry = SimpleNamespace(capability=lambda _name: object())
|
||||||
|
with patch.object(function_changes, "get_registry", return_value=registry), self.assertRaises(HTTPException) as caught:
|
||||||
|
function_changes._identity_directory()
|
||||||
|
self.assertEqual(503, caught.exception.status_code)
|
||||||
|
self.assertEqual("The Identity directory is unavailable.", caught.exception.detail)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/idm-webui",
|
"name": "@govoplan/idm-webui",
|
||||||
"version": "0.1.25",
|
"version": "0.1.26",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user