Prepare GovOPlaN self-hosted release workflow
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user