Add typed IDM relationship contracts

This commit is contained in:
2026-08-02 14:44:41 +02:00
parent 5783d43547
commit c6ef644842
3 changed files with 236 additions and 2 deletions
+149 -1
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal, Protocol, runtime_checkable
@@ -10,9 +10,12 @@ IDM_MODULE_ID = "idm"
CAPABILITY_IDM_DIRECTORY = "idm.directory"
CAPABILITY_IDM_FUNCTION_ASSIGNMENTS = "idm.function_assignments"
CAPABILITY_IDM_ASSIGNMENT_LIFECYCLE = "idm.assignment_lifecycle"
CAPABILITY_IDM_RELATIONSHIPS = "idm.relationships"
IdmStatus = Literal["active", "inactive", "suspended"]
OrganizationFunctionAssignmentSource = Literal["direct", "delegated", "acting_for", "directory", "governance", "system"]
TypedGroupStatus = Literal["active", "inactive"]
IdentityRelationshipStatus = Literal["active", "revoked"]
@dataclass(frozen=True, slots=True)
@@ -44,6 +47,78 @@ class OrganizationFunctionIncumbencyRef:
return not self.assignments
@dataclass(frozen=True, slots=True)
class TypedGroupRef:
"""Provider-neutral IDM group fact scoped to one tenant."""
id: str
tenant_id: str
key: str
name: str
group_type: str
description: str | None = None
status: TypedGroupStatus = "active"
source_provider: str = "local"
source_resource_type: str | None = None
source_resource_id: str | None = None
source_revision: str | None = None
properties: Mapping[str, object] = field(default_factory=dict)
provenance: Mapping[str, object] = field(default_factory=dict)
revision: int = 1
@dataclass(frozen=True, slots=True)
class IdentityRelationshipRef:
"""An effective-dated relationship from an identity to a typed target."""
id: str
tenant_id: str
relationship_kind: str
subject_identity_id: str
target_group_id: str | None = None
related_identity_id: str | None = None
role: str | None = None
valid_from: datetime | None = None
valid_until: datetime | None = None
status: IdentityRelationshipStatus = "active"
revoked_at: datetime | None = None
revoked_by: str | None = None
revocation_reason: str | None = None
source_provider: str = "local"
source_resource_type: str | None = None
source_resource_id: str | None = None
source_revision: str | None = None
properties: Mapping[str, object] = field(default_factory=dict)
provenance: Mapping[str, object] = field(default_factory=dict)
revision: int = 1
@dataclass(frozen=True, slots=True)
class IdentityRelationshipDecisionRef:
relationship: IdentityRelationshipRef
included: bool
code: str
explanation: str
identity_status: IdmStatus | None = None
@dataclass(frozen=True, slots=True)
class TypedGroupMembershipResolutionRef:
group: TypedGroupRef
effective_at: datetime
decisions: tuple[IdentityRelationshipDecisionRef, ...] = ()
@property
def identity_ids(self) -> tuple[str, ...]:
return tuple(
dict.fromkeys(
item.relationship.subject_identity_id
for item in self.decisions
if item.included
)
)
@runtime_checkable
class IdmDirectory(Protocol):
def get_organization_function_assignment(self, assignment_id: str) -> OrganizationFunctionAssignmentRef | None:
@@ -109,6 +184,79 @@ class IdmFunctionAssignmentDirectory(Protocol):
...
@runtime_checkable
class IdmRelationshipDirectory(Protocol):
"""Tenant-safe forward/reverse lookup for typed IDM relationships."""
def get_typed_group(
self,
group_id: str,
*,
tenant_id: str | None = None,
) -> TypedGroupRef | None:
...
def list_typed_groups(
self,
*,
tenant_id: str,
query: str | None = None,
group_types: Sequence[str] = (),
include_inactive: bool = False,
limit: int = 100,
) -> Sequence[TypedGroupRef]:
...
def identity_relationships_for_identity(
self,
identity_id: str,
*,
tenant_id: str,
effective_at: datetime | None = None,
relationship_kinds: Sequence[str] = (),
) -> Sequence[IdentityRelationshipRef]:
...
def identity_relationships_for_identities(
self,
identity_ids: Sequence[str],
*,
tenant_id: str,
effective_at: datetime | None = None,
relationship_kinds: Sequence[str] = (),
) -> Mapping[str, Sequence[IdentityRelationshipRef]]:
...
def identity_relationships_for_group(
self,
group_id: str,
*,
tenant_id: str,
effective_at: datetime | None = None,
relationship_kinds: Sequence[str] = (),
) -> Sequence[IdentityRelationshipRef]:
...
def identity_relationships_for_groups(
self,
group_ids: Sequence[str],
*,
tenant_id: str,
effective_at: datetime | None = None,
relationship_kinds: Sequence[str] = (),
) -> Mapping[str, Sequence[IdentityRelationshipRef]]:
...
def resolve_typed_group_memberships(
self,
group_ids: Sequence[str],
*,
tenant_id: str,
effective_at: datetime | None = None,
relationship_kinds: Sequence[str] = ("member",),
) -> Mapping[str, TypedGroupMembershipResolutionRef]:
...
@runtime_checkable
class IdmAssignmentLifecycle(Protocol):
"""Worker boundary for time-driven function-assignment transitions."""