feat: preserve governed configuration context
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# GovOPlaN Access Codex Guide
|
||||
|
||||
## Documentation Contract
|
||||
|
||||
- Treat documentation as part of every behavior change. Update this module's manifest-driven `DocumentationTopic` contributions for affected user and administrator behavior.
|
||||
- Keep feature content here; `govoplan-docs` projects it without importing Access internals.
|
||||
- Maintain a static user/admin baseline and run `/mnt/DATA/git/govoplan/tools/checks/check-manifest-shapes.py` after behavior or manifest changes.
|
||||
|
||||
## Scope
|
||||
|
||||
This repository owns the GovOPlaN access platform module seed: identity,
|
||||
|
||||
@@ -179,6 +179,18 @@ Governance-template metadata CRUD is not access-owned. It is contributed by
|
||||
`govoplan-admin`; access only materializes those templates into access-owned
|
||||
groups and roles through the `access.governanceMaterializer` capability.
|
||||
|
||||
The configuration-package Admin routes remain in Access as a compatibility
|
||||
surface. Their preflight context is assembled from the active Core registry,
|
||||
including module-owned external-provider declarations. This allows an
|
||||
integration package to validate installed provider authority and maturity
|
||||
without importing provider modules into Access. For dry-run, apply, and export,
|
||||
Access also asks the active registry for tenant-scoped, sanitized runtime
|
||||
provider state using the request database transaction. Package preflight can
|
||||
therefore select an exact stable binding and evaluate its authority, health,
|
||||
freshness, and recovery readiness. When no provider state is available,
|
||||
preflight reports it as unverified rather than inferring health from
|
||||
installation.
|
||||
|
||||
## Verification References
|
||||
|
||||
Focused verification is run from `/mnt/DATA/git/govoplan-core`.
|
||||
|
||||
@@ -165,6 +165,10 @@ from govoplan_core.core.configuration_control import (
|
||||
record_configuration_change_applied,
|
||||
)
|
||||
from govoplan_core.core.configuration_safety import configuration_safety_catalog, plan_configuration_change
|
||||
from govoplan_core.core.provider_governance import (
|
||||
ExternalProviderStateContext,
|
||||
collect_external_provider_states,
|
||||
)
|
||||
from govoplan_core.core.access import CAPABILITY_ACCESS_EXPLANATION, AccessExplanationService, AccessDecisionProvenance, PrincipalRef
|
||||
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory, OrganizationFunctionAssignmentRef
|
||||
@@ -916,9 +920,10 @@ def configuration_package_catalog_validation(
|
||||
@router.post("/configuration-packages/dry-run", response_model=ConfigurationPackageDryRunResponse)
|
||||
def configuration_package_dry_run_endpoint(
|
||||
payload: ConfigurationPackageRunRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope("admin:settings:read", "admin:policies:read", "system:settings:read", "system:governance:read")),
|
||||
):
|
||||
result = dry_run_configuration_package(payload.package, _configuration_providers(), _configuration_context(principal, tenant_id=payload.tenant_id, supplied_data=payload.supplied_data))
|
||||
result = dry_run_configuration_package(payload.package, _configuration_providers(), _configuration_context(principal, tenant_id=payload.tenant_id, supplied_data=payload.supplied_data, session=session))
|
||||
return ConfigurationPackageDryRunResponse(
|
||||
diagnostics=[item.to_dict() for item in result.diagnostics],
|
||||
required_data=[item.to_dict() for item in result.required_data],
|
||||
@@ -944,7 +949,7 @@ def configuration_package_apply_endpoint(
|
||||
)
|
||||
except ConfigurationControlError as exc:
|
||||
raise _configuration_control_http_error(exc) from exc
|
||||
result = apply_configuration_package(payload.package, _configuration_providers(), _configuration_context(principal, tenant_id=payload.tenant_id, supplied_data=payload.supplied_data))
|
||||
result = apply_configuration_package(payload.package, _configuration_providers(), _configuration_context(principal, tenant_id=payload.tenant_id, supplied_data=payload.supplied_data, session=session))
|
||||
record_configuration_change_applied(
|
||||
session,
|
||||
key="configuration_packages.apply",
|
||||
@@ -975,6 +980,7 @@ def configuration_package_apply_endpoint(
|
||||
@router.post("/configuration-packages/export", response_model=ConfigurationPackageExportResponse)
|
||||
def configuration_package_export_endpoint(
|
||||
payload: ConfigurationPackageExportRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope("admin:settings:read", "admin:policies:read", "system:settings:read", "system:governance:read")),
|
||||
):
|
||||
selection = ConfigurationExportSelection(
|
||||
@@ -983,7 +989,7 @@ def configuration_package_export_endpoint(
|
||||
module_ids=tuple(payload.module_ids),
|
||||
object_refs=tuple(payload.object_refs),
|
||||
)
|
||||
result = export_configuration_package(_configuration_providers(), selection, _configuration_context(principal, tenant_id=payload.tenant_id))
|
||||
result = export_configuration_package(_configuration_providers(), selection, _configuration_context(principal, tenant_id=payload.tenant_id, session=session))
|
||||
return ConfigurationPackageExportResponse(
|
||||
fragments=[item.to_dict() for item in result.fragments],
|
||||
data_requirements=[item.to_dict() for item in result.data_requirements],
|
||||
@@ -1010,21 +1016,43 @@ def _configuration_context(
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
supplied_data: dict[str, Any] | None = None,
|
||||
session: Session | None = None,
|
||||
) -> ConfigurationPreflightContext:
|
||||
registry = get_registry()
|
||||
installed_modules: dict[str, str] = {"access": "0.1.6"}
|
||||
capabilities = {CONFIGURATION_PROVIDER_CAPABILITY, ACCESS_CONFIGURATION_CAPABILITY}
|
||||
external_provider_declarations: dict[str, dict[str, object]] = {}
|
||||
external_provider_states: dict[str, dict[str, object]] = {}
|
||||
if registry is not None and hasattr(registry, "manifests"):
|
||||
manifests = registry.manifests()
|
||||
installed_modules = {manifest.id: manifest.version for manifest in manifests}
|
||||
if hasattr(registry, "capability_names"):
|
||||
capabilities.update(registry.capability_names())
|
||||
if hasattr(registry, "external_provider_declarations"):
|
||||
external_provider_declarations = {
|
||||
declaration.id: declaration.to_dict()
|
||||
for declaration in registry.external_provider_declarations()
|
||||
}
|
||||
if session is not None and hasattr(
|
||||
registry,
|
||||
"external_provider_state_providers",
|
||||
):
|
||||
external_provider_states = collect_external_provider_states(
|
||||
registry.external_provider_state_providers(),
|
||||
ExternalProviderStateContext(
|
||||
session=session,
|
||||
tenant_id=tenant_id or principal.tenant_id,
|
||||
principal=principal,
|
||||
),
|
||||
)
|
||||
return ConfigurationPreflightContext(
|
||||
tenant_id=tenant_id or principal.tenant_id,
|
||||
operator_user_id=principal.user.id,
|
||||
supplied_data=supplied_data or {},
|
||||
installed_modules=installed_modules,
|
||||
capabilities=frozenset(capabilities),
|
||||
external_provider_declarations=external_provider_declarations,
|
||||
external_provider_states=external_provider_states,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityD
|
||||
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory
|
||||
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, OrganizationDirectory
|
||||
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
||||
from govoplan_core.core.provider_governance import declared_module_architecture
|
||||
from govoplan_core.core.modules import (
|
||||
DocumentationCondition,
|
||||
DocumentationLink,
|
||||
@@ -741,6 +742,18 @@ manifest = ModuleManifest(
|
||||
ACCESS_CONFIGURATION_CAPABILITY: _configuration_provider,
|
||||
},
|
||||
documentation=ACCESS_DOCUMENTATION,
|
||||
architecture=declared_module_architecture(
|
||||
layer="institutional_foundation",
|
||||
kind="foundation",
|
||||
maturity="vertical_slice",
|
||||
documentation_ref="docs/ACCESS_MODULE_BOUNDARY.md",
|
||||
test_ref="tests/test_login_security.py",
|
||||
known_limits=("Recovery and upgrade evidence is not yet complete enough for supported maturity.",),
|
||||
owned_concepts=("account authentication", "application role", "permission evaluation", "service account"),
|
||||
non_owned_concepts=("person identity", "organization structure", "function incumbency", "policy definition"),
|
||||
security_docs=("docs/ACCESS_MODULE_BOUNDARY.md",),
|
||||
operations_docs=("README.md",),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from govoplan_access.backend.api.v1.routes import _configuration_context
|
||||
from govoplan_core.core.provider_governance import (
|
||||
ExternalProviderRuntimeState,
|
||||
ExternalProviderStateProviderRegistration,
|
||||
)
|
||||
|
||||
|
||||
class ConfigurationPackageContextTests(unittest.TestCase):
|
||||
def test_context_projects_installed_external_provider_declarations(self) -> None:
|
||||
declaration = SimpleNamespace(
|
||||
id="connectors.example",
|
||||
to_dict=lambda: {
|
||||
"id": "connectors.example",
|
||||
"maturity": "read",
|
||||
"authority_modes": ["external_mirror"],
|
||||
},
|
||||
)
|
||||
registry = SimpleNamespace(
|
||||
manifests=lambda: (
|
||||
SimpleNamespace(id="access", version="0.1.14"),
|
||||
SimpleNamespace(id="connectors", version="0.1.14"),
|
||||
),
|
||||
capability_names=lambda: ("connectors.profiles",),
|
||||
external_provider_declarations=lambda: (declaration,),
|
||||
)
|
||||
principal = SimpleNamespace(
|
||||
tenant_id="tenant-1",
|
||||
user=SimpleNamespace(id="user-1"),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"govoplan_access.backend.api.v1.routes.get_registry",
|
||||
return_value=registry,
|
||||
):
|
||||
context = _configuration_context(principal)
|
||||
|
||||
self.assertEqual("0.1.14", context.installed_modules["connectors"])
|
||||
self.assertIn("connectors.profiles", context.capabilities)
|
||||
self.assertEqual(
|
||||
"external_mirror",
|
||||
context.external_provider_declarations["connectors.example"][
|
||||
"authority_modes"
|
||||
][0],
|
||||
)
|
||||
|
||||
def test_context_projects_tenant_runtime_provider_state(self) -> None:
|
||||
declaration = SimpleNamespace(
|
||||
id="calendar.caldav_sync",
|
||||
to_dict=lambda: {
|
||||
"id": "calendar.caldav_sync",
|
||||
"maturity": "synchronize",
|
||||
"authority_modes": ["governed_sync"],
|
||||
},
|
||||
)
|
||||
registration = ExternalProviderStateProviderRegistration(
|
||||
module_id="calendar",
|
||||
provider_id="calendar.caldav_sync",
|
||||
provider=lambda context: (
|
||||
ExternalProviderRuntimeState(
|
||||
provider_id="calendar.caldav_sync",
|
||||
binding_ref="calendar:sync-source:one",
|
||||
authority_mode="governed_sync",
|
||||
observed_at=datetime(2026, 8, 1, 12, 0, tzinfo=UTC),
|
||||
configured=True,
|
||||
active=True,
|
||||
health="healthy",
|
||||
freshness="current",
|
||||
conflict="clear",
|
||||
recovery="ready",
|
||||
metrics={"tenant_matches": context.tenant_id == "tenant-1"},
|
||||
),
|
||||
),
|
||||
)
|
||||
registry = SimpleNamespace(
|
||||
manifests=lambda: (SimpleNamespace(id="calendar", version="0.1.8"),),
|
||||
capability_names=lambda: (),
|
||||
external_provider_declarations=lambda: (declaration,),
|
||||
external_provider_state_providers=lambda: (registration,),
|
||||
)
|
||||
principal = SimpleNamespace(
|
||||
tenant_id="tenant-1",
|
||||
user=SimpleNamespace(id="user-1"),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"govoplan_access.backend.api.v1.routes.get_registry",
|
||||
return_value=registry,
|
||||
):
|
||||
context = _configuration_context(principal, session=object())
|
||||
|
||||
state = context.external_provider_states["calendar.caldav_sync"]
|
||||
self.assertEqual("healthy", state["health"])
|
||||
self.assertEqual("calendar:sync-source:one", state["binding_ref"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user