feat: define bulk governance projection contract
This commit is contained in:
@@ -24,6 +24,7 @@ CAPABILITY_ACCESS_TENANT_PROVISIONER = "access.tenantProvisioner"
|
||||
CAPABILITY_ACCESS_FIRST_ADMIN_PROVISIONER = "access.firstAdminProvisioner"
|
||||
CAPABILITY_ACCESS_ADMINISTRATION = "access.administration"
|
||||
CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER = "access.governanceMaterializer"
|
||||
CAPABILITY_ACCESS_GOVERNANCE_PROJECTION_V1 = "access.governanceProjection.v1"
|
||||
CAPABILITY_POLICY_ACCESS_EXPLANATION_SUBJECTS = (
|
||||
"policy.access_explanation_subjects"
|
||||
)
|
||||
@@ -52,6 +53,7 @@ ACCESS_CAPABILITY_NAMES = frozenset(
|
||||
CAPABILITY_ACCESS_FIRST_ADMIN_PROVISIONER,
|
||||
CAPABILITY_ACCESS_ADMINISTRATION,
|
||||
CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER,
|
||||
CAPABILITY_ACCESS_GOVERNANCE_PROJECTION_V1,
|
||||
CAPABILITY_TENANCY_TENANT_RESOLVER,
|
||||
CAPABILITY_AUDIT_SINK,
|
||||
CAPABILITY_AUDIT_RECORDER,
|
||||
@@ -390,6 +392,82 @@ class GovernanceTemplateMaterialization:
|
||||
required: bool = False
|
||||
|
||||
|
||||
GovernanceProjectionOperation = Literal["upsert", "remove"]
|
||||
GovernanceProjectionStatus = Literal[
|
||||
"created",
|
||||
"updated",
|
||||
"unchanged",
|
||||
"removed",
|
||||
"absent",
|
||||
"blocked",
|
||||
"failed",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GovernanceProjectionCommand:
|
||||
"""Stable Access-owned input for one governance assignment projection."""
|
||||
|
||||
assignment_id: str
|
||||
operation: GovernanceProjectionOperation
|
||||
template: GovernanceTemplateMaterialization
|
||||
provenance: Mapping[str, str] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.assignment_id or len(self.assignment_id) > 255:
|
||||
raise ValueError("Governance projection assignment ids must contain at most 255 characters.")
|
||||
if len(self.provenance) > 20:
|
||||
raise ValueError("Governance projection provenance supports at most 20 entries.")
|
||||
for key, value in self.provenance.items():
|
||||
if not key or len(key) > 100 or len(value) > 500:
|
||||
raise ValueError("Governance projection provenance entries exceed their bounds.")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GovernanceProjectionBatch:
|
||||
"""Versioned, bounded reconciliation request independent of Admin internals."""
|
||||
|
||||
operation_id: str
|
||||
commands: tuple[GovernanceProjectionCommand, ...]
|
||||
version: Literal["1"] = "1"
|
||||
dry_run: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.operation_id or len(self.operation_id) > 255:
|
||||
raise ValueError("Governance projection operation ids must contain at most 255 characters.")
|
||||
if not self.commands or len(self.commands) > 500:
|
||||
raise ValueError("Governance projection batches must contain between 1 and 500 commands.")
|
||||
assignment_ids = [command.assignment_id for command in self.commands]
|
||||
if len(assignment_ids) != len(set(assignment_ids)):
|
||||
raise ValueError("Governance projection assignment ids must be unique within a batch.")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GovernanceProjectionOutcome:
|
||||
assignment_id: str
|
||||
template_id: str
|
||||
tenant_id: str
|
||||
kind: Literal["group", "role"]
|
||||
operation: GovernanceProjectionOperation
|
||||
status: GovernanceProjectionStatus
|
||||
resource_id: str | None = None
|
||||
blocker_codes: tuple[str, ...] = ()
|
||||
message: str | None = None
|
||||
provenance: Mapping[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GovernanceProjectionResult:
|
||||
operation_id: str
|
||||
outcomes: tuple[GovernanceProjectionOutcome, ...]
|
||||
version: Literal["1"] = "1"
|
||||
dry_run: bool = False
|
||||
|
||||
@property
|
||||
def blocked(self) -> tuple[GovernanceProjectionOutcome, ...]:
|
||||
return tuple(item for item in self.outcomes if item.status in {"blocked", "failed"})
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AuditEvent:
|
||||
event_type: str
|
||||
@@ -693,6 +771,18 @@ class AccessGovernanceMaterializer(Protocol):
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AccessGovernanceProjectionV1(Protocol):
|
||||
"""Bulk reconciliation boundary for Admin-owned governance assignments."""
|
||||
|
||||
def reconcile(
|
||||
self,
|
||||
session: object,
|
||||
batch: GovernanceProjectionBatch,
|
||||
) -> GovernanceProjectionResult:
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AuditSink(Protocol):
|
||||
def record(self, event: AuditEvent) -> None:
|
||||
|
||||
Reference in New Issue
Block a user