Harden module update compatibility cleanup
This commit is contained in:
@@ -76,7 +76,8 @@ from govoplan_core.core.campaigns import (
|
||||
CampaignRetentionProvider,
|
||||
)
|
||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
||||
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, OrganizationDirectory as CoreOrganizationDirectory, OrganizationFunctionRef as CoreOrganizationFunctionRef
|
||||
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, OrganizationFunctionAssignmentRef
|
||||
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, OrganizationDirectory as CoreOrganizationDirectory, OrganizationFunctionRef as CoreOrganizationFunctionRef, OrganizationUnitRef as CoreOrganizationUnitRef
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.core.runtime import clear_runtime, configure_runtime
|
||||
from govoplan_core.db.base import Base
|
||||
@@ -85,6 +86,7 @@ from govoplan_core.server.config import GovoplanServerConfig
|
||||
from govoplan_core.tenancy.scope import create_scope_tables
|
||||
from govoplan_access.backend.db.models import (
|
||||
Account,
|
||||
ExternalFunctionRoleAssignment,
|
||||
Function,
|
||||
FunctionAssignment,
|
||||
FunctionRoleAssignment,
|
||||
@@ -1031,6 +1033,207 @@ class AccessContractTests(unittest.TestCase):
|
||||
clear_runtime()
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
|
||||
def test_access_explanation_includes_idm_function_role_sources(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-access-explanation-"))
|
||||
try:
|
||||
tenant_id = "tenant-access-explanation"
|
||||
account_id = "account-access-explanation"
|
||||
user_id = "user-access-explanation"
|
||||
role_id = "role-access-explanation"
|
||||
function_id = "function-access-explanation"
|
||||
organization_unit_id = "unit-access-explanation"
|
||||
assignment_id = "assignment-access-explanation"
|
||||
identity_id = "identity-access-explanation"
|
||||
|
||||
assignment = OrganizationFunctionAssignmentRef(
|
||||
id=assignment_id,
|
||||
tenant_id=tenant_id,
|
||||
identity_id=identity_id,
|
||||
account_id=account_id,
|
||||
function_id=function_id,
|
||||
organization_unit_id=organization_unit_id,
|
||||
applies_to_subunits=True,
|
||||
source="direct",
|
||||
)
|
||||
|
||||
class FakeIdmDirectory:
|
||||
def get_organization_function_assignment(self, requested_assignment_id: str):
|
||||
return assignment if requested_assignment_id == assignment_id else None
|
||||
|
||||
def organization_function_assignments_for_identity(self, requested_identity_id: str, *, tenant_id: str | None = None):
|
||||
if requested_identity_id == identity_id and tenant_id in (None, assignment.tenant_id):
|
||||
return (assignment,)
|
||||
return ()
|
||||
|
||||
def organization_function_assignments_for_account(self, requested_account_id: str, *, tenant_id: str | None = None):
|
||||
if requested_account_id == account_id and tenant_id in (None, assignment.tenant_id):
|
||||
return (assignment,)
|
||||
return ()
|
||||
|
||||
class FakeOrganizationDirectory(CoreOrganizationDirectory):
|
||||
def get_organization_unit(self, requested_organization_unit_id: str):
|
||||
if requested_organization_unit_id != organization_unit_id:
|
||||
return None
|
||||
return CoreOrganizationUnitRef(
|
||||
id=organization_unit_id,
|
||||
tenant_id=tenant_id,
|
||||
slug="registry",
|
||||
name="Registry Office",
|
||||
)
|
||||
|
||||
def organization_units_for_tenant(self, requested_tenant_id: str):
|
||||
if requested_tenant_id != tenant_id:
|
||||
return ()
|
||||
unit = self.get_organization_unit(organization_unit_id)
|
||||
return (unit,) if unit is not None else ()
|
||||
|
||||
def get_function(self, requested_function_id: str):
|
||||
if requested_function_id != function_id:
|
||||
return None
|
||||
return CoreOrganizationFunctionRef(
|
||||
id=function_id,
|
||||
tenant_id=tenant_id,
|
||||
organization_unit_id=organization_unit_id,
|
||||
slug="registry-clerk",
|
||||
name="Registry Clerk",
|
||||
)
|
||||
|
||||
def functions_for_organization_unit(self, requested_organization_unit_id: str, *, include_subunits: bool = False):
|
||||
del include_subunits
|
||||
if requested_organization_unit_id != organization_unit_id:
|
||||
return ()
|
||||
function = self.get_function(function_id)
|
||||
return (function,) if function is not None else ()
|
||||
|
||||
idm_directory = FakeIdmDirectory()
|
||||
organization_directory = FakeOrganizationDirectory()
|
||||
|
||||
with temporary_database(f"sqlite:///{root / 'test.db'}") as database:
|
||||
create_scope_tables(database.engine)
|
||||
Base.metadata.create_all(bind=database.engine)
|
||||
with database.session() as session:
|
||||
tenant = Tenant(id=tenant_id, slug="access-explanation", name="Access Explanation", settings={})
|
||||
account = Account(
|
||||
id=account_id,
|
||||
email="explanation@example.test",
|
||||
normalized_email="explanation@example.test",
|
||||
display_name="Access Explanation User",
|
||||
)
|
||||
user = User(
|
||||
id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
email=account.email,
|
||||
display_name=account.display_name,
|
||||
settings={},
|
||||
mail_profile_policy={},
|
||||
)
|
||||
role = Role(
|
||||
id=role_id,
|
||||
tenant_id=tenant_id,
|
||||
slug="file-reader",
|
||||
name="File Reader",
|
||||
permissions=["files:file:read"],
|
||||
)
|
||||
mapping = ExternalFunctionRoleAssignment(
|
||||
tenant_id=tenant_id,
|
||||
source_module="organizations",
|
||||
function_id=function_id,
|
||||
role_id=role_id,
|
||||
)
|
||||
session.add_all([tenant, account, user, role, mapping])
|
||||
session.commit()
|
||||
|
||||
from govoplan_access.backend.explanation import SqlAccessExplanationService, build_user_access_explanation
|
||||
|
||||
explanation = build_user_access_explanation(
|
||||
session,
|
||||
user,
|
||||
idm_directory=idm_directory,
|
||||
organization_directory=organization_directory,
|
||||
)
|
||||
|
||||
self.assertEqual(1, len(explanation.function_facts))
|
||||
self.assertEqual("Registry Clerk", explanation.function_facts[0].function_name)
|
||||
self.assertEqual((role_id,), explanation.function_facts[0].role_ids)
|
||||
source = next(item for item in explanation.role_sources if item.source_type == "idm_function_role")
|
||||
self.assertEqual("Registry Clerk", source.function_name)
|
||||
self.assertEqual("File Reader", source.role_name)
|
||||
scope = next(item for item in explanation.scopes if item.scope == "files:file:read")
|
||||
self.assertEqual(["idm_function_role"], [item.source_type for item in scope.sources])
|
||||
|
||||
service = SqlAccessExplanationService(
|
||||
idm_directory=idm_directory,
|
||||
organization_directory=organization_directory,
|
||||
)
|
||||
provenance = service.explain_scope_provenance(
|
||||
PrincipalRef(
|
||||
account_id=account_id,
|
||||
membership_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
identity_id=identity_id,
|
||||
scopes=frozenset({"files:file:read"}),
|
||||
),
|
||||
"files:file:read",
|
||||
)
|
||||
self.assertTrue(any(item.kind == "function" and item.label == "Registry Clerk" for item in provenance))
|
||||
self.assertTrue(any(item.kind == "role" and item.label == "File Reader" for item in provenance))
|
||||
|
||||
registry = PlatformRegistry()
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
id="idm",
|
||||
name="IDM",
|
||||
version="test",
|
||||
capability_factories={CAPABILITY_IDM_DIRECTORY: lambda context: idm_directory},
|
||||
)
|
||||
)
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
id="organizations",
|
||||
name="Organizations",
|
||||
version="test",
|
||||
capability_factories={CAPABILITY_ORGANIZATION_DIRECTORY: lambda context: organization_directory},
|
||||
)
|
||||
)
|
||||
context = ModuleContext(registry=registry, settings=object())
|
||||
registry.configure_capability_context(context)
|
||||
configure_runtime(context)
|
||||
|
||||
from govoplan_access.backend.api.v1.routes import router as admin_router
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(admin_router)
|
||||
|
||||
def principal_override() -> ApiPrincipal:
|
||||
with database.session() as session:
|
||||
account = session.get(Account, account_id)
|
||||
user = session.get(User, user_id)
|
||||
assert account is not None
|
||||
assert user is not None
|
||||
return ApiPrincipal(
|
||||
principal=PrincipalRef(
|
||||
account_id=account_id,
|
||||
membership_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
scopes=frozenset({"admin:users:read"}),
|
||||
),
|
||||
account=account,
|
||||
user=user,
|
||||
)
|
||||
|
||||
app.dependency_overrides[get_api_principal] = principal_override
|
||||
with TestClient(app) as client:
|
||||
response = client.get(f"/admin/users/{user_id}/access-explanation")
|
||||
self.assertEqual(200, response.status_code, response.text)
|
||||
payload = response.json()
|
||||
self.assertIn("files:file:read", payload["user"]["effective_scopes"])
|
||||
self.assertEqual("idm_function_role", payload["role_sources"][0]["source_type"])
|
||||
self.assertEqual("Registry Clerk", payload["function_facts"][0]["function_name"])
|
||||
finally:
|
||||
clear_runtime()
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
|
||||
def test_api_principal_dependency_uses_access_resolver_capability(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-access-contract-"))
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user