Prepare GovOPlaN self-hosted release workflow
Some checks failed
Dependency Audit / dependency-audit (push) Failing after 50s
Module Matrix / module-matrix (push) Failing after 49s

This commit is contained in:
2026-07-10 21:57:22 +02:00
parent 8dd5123aab
commit 94236a7d7e
51 changed files with 3576 additions and 803 deletions

View File

@@ -22,6 +22,8 @@ from govoplan_core.core.access import (
CAPABILITY_ACCESS_EXPLANATION,
CAPABILITY_ACCESS_SEMANTIC_DIRECTORY,
CAPABILITY_ACCESS_TENANT_PROVISIONER,
CAPABILITY_AUDIT_RECORDER,
CAPABILITY_AUDIT_RETENTION,
CAPABILITY_AUDIT_SINK,
CAPABILITY_TENANCY_TENANT_RESOLVER,
AccessDecision,
@@ -34,7 +36,12 @@ from govoplan_core.core.access import (
AccessSubjectRef,
AccountRef,
AuditEvent,
AuditRecordRef,
AuditRecorder,
AuditRetentionProvider,
AuditSink,
CreatedApiKeyRef,
DevelopmentBootstrapRef,
FunctionAssignmentRef,
FunctionDelegationRef,
FunctionRef,
@@ -69,7 +76,9 @@ 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.registry import PlatformRegistry
from govoplan_core.core.runtime import clear_runtime, configure_runtime
from govoplan_core.db.base import Base
from govoplan_core.server.app import create_app
from govoplan_core.server.config import GovoplanServerConfig
@@ -265,8 +274,19 @@ class _FakeTenantAccessProvisioner:
del session, tenant, owner_account_id
return "user-1"
def ensure_development_admin(self, session: object, *, tenant: object, user_email: str, user_password: str, api_key_secret: str | None, scopes):
del session, tenant, user_password, scopes
return DevelopmentBootstrapRef(
user=UserRef(id="user-1", account_id="account-1", tenant_id="tenant-1", email=user_email),
created_api_key=CreatedApiKeyRef(id="api-key-1", secret=api_key_secret) if api_key_secret else None,
)
class _FakeAccessAdministration:
def tenant_counts(self, session: object, tenant_id: str):
del session, tenant_id
return {"users": 1, "active_users": 1, "groups": 2, "api_keys": 3}
def system_account_count(self, session: object) -> int:
del session
return 1
@@ -287,6 +307,22 @@ class _FakeAccessAdministration:
del session, operator, value
return ("user-1",)
def user_settings(self, session: object, user_id: str, *, tenant_id: str):
del session, user_id, tenant_id
return {"privacy_retention_policy": {"audit_detail_level": "minimal"}}
def set_user_settings(self, session: object, user_id: str, *, tenant_id: str, settings):
del session, user_id, tenant_id
return dict(settings)
def group_settings(self, session: object, group_id: str, *, tenant_id: str):
del session, group_id, tenant_id
return {"privacy_retention_policy": {"audit_detail_level": "redacted"}}
def set_group_settings(self, session: object, group_id: str, *, tenant_id: str, settings):
del session, group_id, tenant_id
return dict(settings)
class _FakeAccessGovernanceMaterializer:
def __init__(self) -> None:
@@ -408,6 +444,28 @@ class _FakeAuditSink:
self.events.append(event)
class _FakeAuditRecorder:
def record_event(self, session: object, event: AuditEvent) -> AuditRecordRef:
del session
return AuditRecordRef(
id="audit-1",
scope=event.scope,
tenant_id=event.tenant_id,
user_id=event.user_id,
api_key_id=event.api_key_id,
action=event.action,
object_type=event.resource_type,
object_id=event.resource_id,
details=event.details,
)
class _FakeAuditRetentionProvider:
def apply_detail_retention(self, session: object, *, dry_run: bool, now, policy_for_record):
del session, dry_run, now, policy_for_record
return {"eligible": 1, "redacted": 0, "already_redacted": 0}
class AccessContractTests(unittest.TestCase):
def test_access_capability_names_are_stable(self) -> None:
self.assertEqual("access", ACCESS_MODULE_ID)
@@ -428,6 +486,8 @@ class AccessContractTests(unittest.TestCase):
self.assertEqual("tenancy.tenantResolver", CAPABILITY_TENANCY_TENANT_RESOLVER)
self.assertEqual("security.secretProvider", CAPABILITY_SECURITY_SECRET_PROVIDER)
self.assertEqual("audit.sink", CAPABILITY_AUDIT_SINK)
self.assertEqual("audit.recorder", CAPABILITY_AUDIT_RECORDER)
self.assertEqual("audit.retention", CAPABILITY_AUDIT_RETENTION)
self.assertEqual(len(ACCESS_CAPABILITY_NAMES), len(set(ACCESS_CAPABILITY_NAMES)))
def test_access_refs_are_small_immutable_dtos(self) -> None:
@@ -533,6 +593,8 @@ class AccessContractTests(unittest.TestCase):
self.assertIsInstance(_FakeCampaignRetentionProvider(), CampaignRetentionProvider)
self.assertIsInstance(_FakeSecretProvider(), SecretProvider)
self.assertIsInstance(_FakeAuditSink(), AuditSink)
self.assertIsInstance(_FakeAuditRecorder(), AuditRecorder)
self.assertIsInstance(_FakeAuditRetentionProvider(), AuditRetentionProvider)
def test_campaign_mail_policy_context_capability_shape(self) -> None:
provider = _FakeCampaignMailPolicyContextProvider()
@@ -843,6 +905,132 @@ class AccessContractTests(unittest.TestCase):
finally:
shutil.rmtree(root, ignore_errors=True)
def test_external_function_role_mappings_require_organizations_directory(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-access-external-function-map-"))
try:
tenant_id = "tenant-external-function-map"
account_id = "account-external-function-map"
user_id = "user-external-function-map"
role_id = "role-external-function-map"
function_id = "function-external-function-map"
with temporary_database(f"sqlite:///{root / 'external-function-map.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="external-function-map", name="External Function Map", settings={})
account = Account(
id=account_id,
email="external-function-map@example.test",
normalized_email="external-function-map@example.test",
display_name="External Function Map Admin",
)
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="external-function-reader",
name="External Function Reader",
permissions=["files:file:read"],
is_assignable=True,
)
session.add_all([tenant, account, user, role])
session.commit()
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:roles:read", "admin:roles:write", "access:role:assign"}),
),
account=account,
user=user,
)
app.dependency_overrides[get_api_principal] = principal_override
clear_runtime()
with TestClient(app) as client:
blocked_response = client.post(
"/admin/external-function-role-mappings",
json={"source_module": "organizations", "function_id": function_id, "role_id": role_id},
)
self.assertEqual(409, blocked_response.status_code, blocked_response.text)
self.assertEqual("organizations_required", blocked_response.json()["detail"]["code"])
class FakeOrganizationDirectory(CoreOrganizationDirectory):
def get_organization_unit(self, organization_unit_id: str):
del organization_unit_id
return None
def organization_units_for_tenant(self, tenant_id: str):
del tenant_id
return ()
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="unit-external-function-map",
slug="external-function",
name="External Function",
)
def functions_for_organization_unit(self, organization_unit_id: str, *, include_subunits: bool = False):
del organization_unit_id, include_subunits
return ()
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="organizations",
name="Organizations",
version="test",
capability_factories={CAPABILITY_ORGANIZATION_DIRECTORY: lambda context: FakeOrganizationDirectory()},
)
)
context = ModuleContext(registry=registry, settings=object())
registry.configure_capability_context(context)
configure_runtime(context)
created_response = client.post(
"/admin/external-function-role-mappings",
json={"source_module": "organizations", "function_id": function_id, "role_id": role_id},
)
self.assertEqual(201, created_response.status_code, created_response.text)
self.assertEqual(function_id, created_response.json()["function_id"])
unsupported_response = client.post(
"/admin/external-function-role-mappings",
json={"source_module": "legacy", "function_id": function_id, "role_id": role_id},
)
self.assertEqual(422, unsupported_response.status_code, unsupported_response.text)
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: