generated from coulomb/repo-seed
Implement identity canon alignment
This commit is contained in:
@@ -7,6 +7,9 @@ make test
|
||||
```
|
||||
|
||||
See `docs/development.md`, `docs/configuration.md`, `docs/contracts.md`,
|
||||
`docs/examples.md`, `docs/scenarios.md`, `docs/operability.md`,
|
||||
`docs/release.md`, `docs/ui-contracts.md`, and `docs/final-assessment.md`
|
||||
for implementation boundaries, contracts, examples, and release readiness.
|
||||
`docs/canon-mapping.md`, `docs/canon-interface-card.yaml`,
|
||||
`docs/evidence-gap-examples.md`, `docs/examples.md`, `docs/scenarios.md`,
|
||||
`docs/operability.md`, `docs/release.md`, `docs/ui-contracts.md`,
|
||||
`docs/identity-domain-naming-decision.md`, and `docs/final-assessment.md`
|
||||
for implementation boundaries, contracts, canon mappings, examples, and release
|
||||
readiness.
|
||||
|
||||
29
SCOPE.md
29
SCOPE.md
@@ -2,27 +2,35 @@
|
||||
|
||||
## One-Liner
|
||||
|
||||
Headless user-domain and profile engine for accounts, identity links,
|
||||
preferences, memberships, application catalogs, projections, audit, and
|
||||
events.
|
||||
Headless user-domain and identity-domain integration engine for accounts,
|
||||
identity links, actor/principal/subject context, preferences, memberships,
|
||||
application catalogs, projections, evidence references, audit, and events.
|
||||
|
||||
## In Scope
|
||||
|
||||
- user and account records;
|
||||
- account lifecycle state;
|
||||
- external identity links;
|
||||
- actor, authenticated subject, authorization principal, account, and user
|
||||
context mappings;
|
||||
- global, tenant, application, and membership profile values;
|
||||
- preference values;
|
||||
- tenant, application, team, and scope memberships;
|
||||
- identity-context read models for domain consumers;
|
||||
- canon interface cards, entity mappings, relationship mappings, and explicit
|
||||
gap records;
|
||||
- application registry for profile consumers;
|
||||
- customization catalog registry and validation;
|
||||
- effective profile resolution;
|
||||
- projection APIs for self-service, admin, application runtime, audit, and
|
||||
agent contexts;
|
||||
- audit records and lifecycle/profile-change events;
|
||||
- local evidence references derived from audit and event records;
|
||||
- local standalone development mode;
|
||||
- integration ports for identity claims, authorization checks, events, and
|
||||
runtime secrets.
|
||||
runtime secrets;
|
||||
- adapter contracts for evidence export, policy/control references, and
|
||||
lifecycle task handoff.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
@@ -30,16 +38,21 @@ events.
|
||||
- password, passkey, session, and MFA lifecycle;
|
||||
- OIDC/SAML token issuance;
|
||||
- final authorization policy decisions;
|
||||
- durable authorization grant authority outside user-engine-owned memberships;
|
||||
- policy, control, access-review, exception, and organization source-of-truth
|
||||
ownership;
|
||||
- runtime secret custody;
|
||||
- UI implementation;
|
||||
- full SCIM server or enterprise directory replacement in the initial product.
|
||||
|
||||
## Boundary Rule
|
||||
|
||||
user-engine owns user-domain facts and projections. Other systems may provide
|
||||
identity, authorization, deployment, event transport, or UI surfaces, but they
|
||||
must integrate through explicit interfaces rather than becoming hidden sources
|
||||
of profile truth.
|
||||
user-engine owns user-domain facts, identity-context mappings, and projections.
|
||||
Other systems may provide authentication, IAM claims, authorization decisions,
|
||||
policy/control authority, deployment, event transport, durable audit, secrets,
|
||||
organization records, or UI surfaces, but they must integrate through explicit
|
||||
interfaces rather than becoming hidden sources of profile or identity-domain
|
||||
truth.
|
||||
|
||||
## Current Planning
|
||||
|
||||
|
||||
118
docs/canon-interface-card.yaml
Normal file
118
docs/canon-interface-card.yaml
Normal file
@@ -0,0 +1,118 @@
|
||||
subsystem: user-engine
|
||||
description: >
|
||||
NetKingdom identity-domain integration layer for user, account, identity-link,
|
||||
tenant, membership, profile, lifecycle, and evidence-facing context.
|
||||
status: candidate
|
||||
owner: codex
|
||||
updated: "2026-06-05"
|
||||
|
||||
implements:
|
||||
- identity-canon conceptual model as an implementation-facing domain facade
|
||||
- InfoTechCanon user-engine evaluation pack
|
||||
- small-saas user-management alignment surface
|
||||
|
||||
produces:
|
||||
- User
|
||||
- Account
|
||||
- Identity Record
|
||||
- Scoped Identifier
|
||||
- Profile
|
||||
- Tenant
|
||||
- Membership Relationship
|
||||
- Authenticated Subject reference
|
||||
- Authorization Principal reference
|
||||
- Evidence Source reference
|
||||
- Access Grant or grant-like membership fact
|
||||
|
||||
consumes:
|
||||
- NetKingdom IAM Profile claims
|
||||
- verified issuer and subject identifiers
|
||||
- assurance and principal type claims
|
||||
- authorization decisions and obligations
|
||||
- policy, control, review, exception, and evidence references
|
||||
- lifecycle task references from downstream task systems
|
||||
- platform audit and event sinks
|
||||
|
||||
owned_concepts:
|
||||
user_record: User-engine local user record mapped to identity-canon User as a convenience term.
|
||||
account_record: Operational account state for a user-engine scope.
|
||||
external_identity_link: Source-specific issuer and subject link to a user record.
|
||||
profile_value: Scoped profile or preference value.
|
||||
membership_fact: User-domain relationship to tenant, team, application, or scope.
|
||||
identity_context: Canon-facing read model over user, account, actor, subject, principal, scope, memberships, profile, and evidence references.
|
||||
|
||||
mapped_not_owned:
|
||||
Actor: Consumed from verified identity claims and represented as canon reference.
|
||||
Authenticated Subject: Projected from issuer and subject after authentication.
|
||||
Authorization Principal: Projected for the authorization system; final decisions remain outside user-engine.
|
||||
Policy: Referenced from authorization/governance systems.
|
||||
Control: Referenced from security/governance systems.
|
||||
Evidence Source: Referenced from audit, review, approval, or verification systems.
|
||||
AccessReview: Referenced from governance or review systems.
|
||||
Organization: Referenced when tenants, teams, or users map to organization systems.
|
||||
Credential: Explicitly not stored or lifecycle-managed by user-engine.
|
||||
|
||||
accepted_relationships:
|
||||
- identity_link
|
||||
- belongs_to_tenant
|
||||
- authenticates_as
|
||||
- evaluated_as
|
||||
- member_of
|
||||
- role_label
|
||||
- scoped_to
|
||||
- governed_by
|
||||
- implemented_by
|
||||
- evidenced_by
|
||||
- creates_task
|
||||
|
||||
emitted_events:
|
||||
- user.created
|
||||
- account.status_changed
|
||||
- tenant_account.status_changed
|
||||
- identity.linked
|
||||
- membership.added
|
||||
- profile.value_set
|
||||
- application.registered
|
||||
- catalog.published
|
||||
|
||||
required_identifiers:
|
||||
actor_key: "issuer + subject"
|
||||
user_id: "opaque user-engine user id"
|
||||
account_id: "opaque user-engine account id"
|
||||
identity_id: "opaque user-engine external identity id"
|
||||
tenant: "NetKingdom-aligned tenant or platform scope"
|
||||
application_id: "registered application id when projection is application-scoped"
|
||||
correlation_id: "operation-level audit and event correlation id"
|
||||
|
||||
mapping_rules:
|
||||
- Resolve source terms such as user, group, role, tenant, subject, and principal into identity-canon layers before exposing them as implementation concepts.
|
||||
- Keep account records, authenticated subjects, and authorization principals distinct even when they share issuer or subject identifiers.
|
||||
- Treat memberships as relationship facts that may produce grant-like access facts, not as final authorization decisions.
|
||||
- Preserve source system, scope, lifecycle state, and evidence reference whenever a relationship affects access, privacy, or lifecycle.
|
||||
- Link policy, control, review, exception, and task concepts by reference unless user-engine is the actual source of truth.
|
||||
|
||||
validation_rules:
|
||||
- User, Account, Actor, Authenticated Subject, Authorization Principal, Tenant, Team, Membership, and Profile must remain distinguishable in the identity context read model.
|
||||
- Tenant-scoped privileged membership must expose scope and evidence or an explicit evidence gap.
|
||||
- Cross-tenant context must be denied unless the actor is a platform operator.
|
||||
- Claims enrichment projections must not imply token issuance ownership.
|
||||
- Credential, MFA, policy decision, and durable platform audit ownership must remain outside user-engine.
|
||||
|
||||
source_of_truth:
|
||||
user_records: user-engine
|
||||
account_state: user-engine
|
||||
external_identity_links: user-engine
|
||||
profile_values: user-engine
|
||||
membership_facts_created_here: user-engine
|
||||
authentication: NetKingdom IAM infrastructure
|
||||
credential_assurance: NetKingdom IAM/security infrastructure
|
||||
authorization_decisions: NetKingdom authorization infrastructure
|
||||
policy_and_control_definitions: NetKingdom governance/security infrastructure
|
||||
durable_platform_audit: NetKingdom audit infrastructure
|
||||
organization_authority: NetKingdom organization or directory systems
|
||||
|
||||
known_deviations:
|
||||
- User remains a local implementation class even though identity-canon treats user as a non-root convenience term; mappings must state whether it represents actor-facing profile holder, account owner, or local user record.
|
||||
- Access Grant is currently a grant-like reference derived from memberships, not a durable authorization grant table.
|
||||
- Evidence Source references currently derive from local audit records unless an external evidence exporter is supplied.
|
||||
- AccessReview, Policy, Control, Exception, and lifecycle Task are references or gaps, not first-class owned records.
|
||||
103
docs/canon-mapping.md
Normal file
103
docs/canon-mapping.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Canon Mapping
|
||||
|
||||
Status: candidate
|
||||
Updated: 2026-06-05
|
||||
|
||||
This document maps current `user-engine` implementation concepts to
|
||||
identity-canon and InfoTechCanon concepts. It is intentionally explicit about
|
||||
owned facts, consumed facts, references, and gaps.
|
||||
|
||||
## Mapping Stance
|
||||
|
||||
`identity-canon` treats `User` as a convenience term, not the root concept.
|
||||
`user-engine` keeps the existing `User` implementation class for compatibility,
|
||||
but exposes canon-facing context through `identity_context` so consumers can see
|
||||
the distinct account, actor, subject, principal, tenant, membership, profile,
|
||||
and evidence references.
|
||||
|
||||
## Entity Mapping
|
||||
|
||||
| user-engine concept | Canon concept | Ownership | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `Actor` | Actor | consumed/reference | Normalized from verified IAM claims; not persisted as source of truth. |
|
||||
| `Actor.issuer + Actor.subject` | Authenticated Subject / Scoped Identifier | consumed/reference | Protocol-level identity after authentication. |
|
||||
| `User` | User convenience term / profile holder | owned local record | Not treated as Natural Person unless a future mapping proves that relationship. |
|
||||
| `Account` | Account | owned | Operational account state for the user-engine scope. |
|
||||
| `TenantAccount` | Account scoped to Tenant | owned | Tenant-local account lifecycle state. |
|
||||
| `ExternalIdentity` | Identity Record / Scoped Identifier / Synonymity Assertion seed | owned link | Links issuer-subject pairs to local user records without destructive merge. |
|
||||
| `Application` | Scope / Application Service reference | owned local registry | Application-specific profile consumer and catalog owner. |
|
||||
| `ApplicationBinding` | Trust Relationship / integration binding | owned local registry | Binds user-engine app IDs to external clients, protected systems, events, and catalogs. |
|
||||
| `Membership` | Membership Relationship | owned when created here | Relationship from user/account context to tenant, team, application, group, or other scope. |
|
||||
| `Membership.kind` | Role label / grant-like fact | owned local label | Role-like string is not a final authorization decision. |
|
||||
| `ProfileValue` | Profile | owned | Scoped presentation or preference value. |
|
||||
| `Catalog` / `AttributeDefinition` | Profile schema / governance metadata | owned | Attribute ownership, sensitivity, visibility, and mutability metadata. |
|
||||
| `AuthorizationRequest` | Authorization request | emitted/consumed | Sent to authorization port; policy decision remains external. |
|
||||
| `AuthorizationDecision` | Authorization decision | consumed/reference | Enforced locally; decision authority remains external. |
|
||||
| `AuditRecord` | Evidence Source | owned local evidence, exportable | Local operational evidence; durable platform audit is external. |
|
||||
| `OutboxEvent` | Lifecycle event / evidence source | owned local event | Integration event queue for downstream systems. |
|
||||
|
||||
## Relationship Mapping
|
||||
|
||||
| Relationship | Source | Target | Implementation source |
|
||||
| --- | --- | --- | --- |
|
||||
| `identity_link` | Identity Record | User | `ExternalIdentity.user_id` |
|
||||
| `belongs_to_tenant` | User or Account | Tenant | `TenantAccount` and tenant-scoped operations |
|
||||
| `authenticates_as` | Account | Authenticated Subject | normalized IAM claims |
|
||||
| `evaluated_as` | Authenticated Subject | Authorization Principal | `identity_context` principal projection |
|
||||
| `member_of` | User | Team / Group / Scope | `Membership(scope_type, scope_id)` |
|
||||
| `role_label` | Membership Relationship | Role | `Membership.kind` |
|
||||
| `scoped_to` | Access Grant or grant-like fact | Tenant / Scope | `Membership.tenant` and operation tenant |
|
||||
| `governed_by` | Grant-like fact / decision | Policy reference | external policy/control resolver gap |
|
||||
| `implemented_by` | Policy reference | Control reference | external policy/control resolver gap |
|
||||
| `evidenced_by` | Identity-domain claim or grant-like fact | Evidence Source | local audit or external evidence exporter |
|
||||
| `creates_task` | Evidence gap / integration gap | lifecycle task reference | lifecycle task sink gap |
|
||||
|
||||
## Read Model
|
||||
|
||||
`UserEngineService.identity_context(...)` returns the current implementation
|
||||
surface for canon-facing identity context. It includes:
|
||||
|
||||
- the normalized actor;
|
||||
- local user and account records;
|
||||
- external identity links;
|
||||
- tenant and optional application scope;
|
||||
- membership facts;
|
||||
- entity references for User, Account, Actor, Authenticated Subject,
|
||||
Authorization Principal, Tenant, Identity Record, Scoped Identifier, Team,
|
||||
Scope, Membership Relationship, and Role;
|
||||
- relationship references for identity link, tenant scope, authentication,
|
||||
principal evaluation, membership, role label, and grant-like scope;
|
||||
- access grant-like references derived from memberships;
|
||||
- evidence references derived from audit records;
|
||||
- an optional effective profile projection;
|
||||
- explicit gaps when evidence is missing.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- `Natural Person` is not modeled directly. A user record may represent a human
|
||||
profile holder, but that relationship is not currently asserted.
|
||||
- `Synonymity Assertion` is implicit in identity linking. Strong/weak link
|
||||
confidence, verification method, privacy scope, and revocation state are not
|
||||
first-class yet.
|
||||
- `Organization`, `Customer`, `Vendor`, and `Legal Entity` are referenced only
|
||||
through tenant, team, owner, or external system identifiers.
|
||||
- `AccessReview`, `Policy`, `Control`, `Exception`, and lifecycle `Task`
|
||||
references depend on external NetKingdom systems or future adapter
|
||||
implementations.
|
||||
- `Access Grant` is currently an exported grant-like reference derived from
|
||||
memberships. It is not a durable authorization grant table.
|
||||
- Evidence references currently derive from local audit records unless an
|
||||
`EvidenceReferenceExporter` is supplied.
|
||||
|
||||
## Validation Hooks
|
||||
|
||||
The first executable checks live in `tests/test_identity_canon_alignment.py`.
|
||||
They verify that:
|
||||
|
||||
- user, account, authenticated subject, authorization principal, tenant, team,
|
||||
membership, and grant-like references remain distinct;
|
||||
- service accounts project into service principals without becoming natural
|
||||
persons;
|
||||
- cross-tenant identity context is denied without platform scope;
|
||||
- claims-enrichment context can be read without implying token issuance
|
||||
ownership.
|
||||
@@ -10,9 +10,32 @@ HTTP or RPC adapters should preserve these operation names:
|
||||
- `resolve_tenant_context`, `set_tenant_account_status`, `add_membership`,
|
||||
`tenant_diagnostics`
|
||||
- `register_application`, `publish_catalog`
|
||||
- `set_profile_value`, `effective_profile`, `projection`
|
||||
- `set_profile_value`, `effective_profile`, `projection`, `identity_context`
|
||||
- `audit_records`, `outbox_events`
|
||||
|
||||
## Identity Context Contract
|
||||
|
||||
`identity_context` is the first canon-facing read model for NetKingdom
|
||||
identity-domain consumers. It resolves a verified actor into the local user,
|
||||
account, external identity links, tenant scope, memberships, optional
|
||||
application scope, optional effective profile, canon entity references,
|
||||
relationship references, grant-like membership facts, and evidence references.
|
||||
|
||||
The method keeps these concepts distinct:
|
||||
|
||||
- user-engine `User` record;
|
||||
- operational `Account`;
|
||||
- external `Identity Record` and scoped issuer/subject identifier;
|
||||
- `Actor` from verified claims;
|
||||
- `Authenticated Subject` projected from issuer and subject;
|
||||
- `Authorization Principal` projected for policy evaluation;
|
||||
- `Tenant`, `Team`, `Scope`, `Membership Relationship`, and `Role` references.
|
||||
|
||||
Evidence references are currently derived from local audit records. External
|
||||
policy, control, access-review, exception, and lifecycle task references belong
|
||||
to adapter contracts and remain non-owned unless a later workplan assigns
|
||||
source-of-truth responsibility to user-engine.
|
||||
|
||||
## Error Taxonomy
|
||||
|
||||
- `ValidationError`: caller supplied an invalid shape, state transition, or
|
||||
@@ -42,6 +65,9 @@ Every mutating service operation appends an audit record and outbox event with
|
||||
the same correlation id and resolved tenant. Authorization denials are audited
|
||||
without emitting outbox events.
|
||||
|
||||
Local audit records may be exported as identity-canon `Evidence Source`
|
||||
references. Durable platform audit custody remains outside user-engine.
|
||||
|
||||
## Migration Contract
|
||||
|
||||
The isolated store exposes `SCHEMA_VERSION = 0001_initial` and a `migrate`
|
||||
|
||||
62
docs/evidence-gap-examples.md
Normal file
62
docs/evidence-gap-examples.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Evidence Gap Examples
|
||||
|
||||
Status: candidate
|
||||
Updated: 2026-06-05
|
||||
|
||||
`user-engine` should not pretend missing review or governance material exists.
|
||||
When identity-domain context lacks evidence, policy, control, review, or task
|
||||
references, the gap must be explicit and handoff-ready.
|
||||
|
||||
## Gap Shape
|
||||
|
||||
```yaml
|
||||
gap_id: evidence:no-audit-records
|
||||
subject:
|
||||
concept: Account
|
||||
identifier: acct_example
|
||||
scope: tenant:acme
|
||||
reason: No local audit or external evidence reference supports this identity-domain claim.
|
||||
proposed_disposition: create_or_link_lifecycle_task
|
||||
owner: user-engine adapter boundary
|
||||
```
|
||||
|
||||
## Privileged Membership Without External Review
|
||||
|
||||
```yaml
|
||||
gap_id: review:tenant-admin-membership
|
||||
subject:
|
||||
concept: Access Grant
|
||||
identifier: mem_example
|
||||
scope: tenant:acme
|
||||
reason: Tenant admin membership has local audit evidence but no external access review reference.
|
||||
proposed_disposition: link AccessReview through EvidenceReferenceExporter or create review task through LifecycleTaskSink.
|
||||
```
|
||||
|
||||
## Policy Or Control Reference Missing
|
||||
|
||||
```yaml
|
||||
gap_id: control:tenant-isolation-reference
|
||||
subject:
|
||||
concept: Membership Relationship
|
||||
identifier: mem_example
|
||||
scope: tenant:acme
|
||||
reason: Membership is tenant-scoped, but no external policy/control reference was supplied.
|
||||
proposed_disposition: resolve policy and control through PolicyControlReferenceResolver.
|
||||
```
|
||||
|
||||
## Lifecycle Task Handoff
|
||||
|
||||
```yaml
|
||||
task_reference:
|
||||
concept: Task
|
||||
identifier: task_from_lifecycle_sink
|
||||
source_gap: review:tenant-admin-membership
|
||||
summary: Review tenant-admin membership for tenant:acme.
|
||||
evidence:
|
||||
- concept: Evidence Source
|
||||
identifier: aud_example
|
||||
```
|
||||
|
||||
These examples are intentionally adapter-neutral. The task, review, policy, and
|
||||
control source of truth belongs to the surrounding NetKingdom systems unless a
|
||||
future workplan assigns one of those responsibilities to `user-engine`.
|
||||
55
docs/identity-domain-naming-decision.md
Normal file
55
docs/identity-domain-naming-decision.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Identity Domain Naming Decision
|
||||
|
||||
Status: candidate
|
||||
Decision date: 2026-06-05
|
||||
|
||||
## Context
|
||||
|
||||
`user-engine` is moving from a narrow user/profile service toward a NetKingdom
|
||||
identity-domain integration layer. That raises the question of whether the
|
||||
repository should be renamed to `identity-engine`, `identity-domain-engine`, or
|
||||
`organization-engine`.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep the repository named `user-engine` for USER-WP-0007 implementation.
|
||||
|
||||
Treat `identity-engine` or `identity-domain-engine` as future candidate names
|
||||
only after the canon interface card, mapping export, identity-context read
|
||||
model, NetKingdom adapter contracts, and conformance tests are established and
|
||||
reviewed.
|
||||
|
||||
Do not use `organization-engine` for this scope.
|
||||
|
||||
## Rationale
|
||||
|
||||
- `user-engine` is already the implemented package, module, docs, and workplan
|
||||
identity.
|
||||
- The current implementation still owns user-domain facts: users, accounts,
|
||||
identity links, profiles, memberships, catalogs, projections, audit, and
|
||||
events.
|
||||
- `identity-engine` could imply ownership of the full identity provider,
|
||||
credential lifecycle, federation, MFA, token issuance, and IAM substrate unless
|
||||
the boundary is already well proven.
|
||||
- `organization-engine` would imply ownership of organization, HR, authority,
|
||||
responsibility, reporting, legal entity, customer, vendor, and community
|
||||
modeling. Those are adjacent canon concepts, not this repo's current source of
|
||||
truth.
|
||||
|
||||
## Rename Criteria
|
||||
|
||||
Revisit the name when all of the following are true:
|
||||
|
||||
- consumers primarily use the identity-context API rather than only user/profile
|
||||
APIs;
|
||||
- NetKingdom IAM, authorization, audit, and evidence adapters are stable;
|
||||
- identity-canon mappings are validated by executable scenarios;
|
||||
- docs consistently describe the repo as an identity-domain facade and not as an
|
||||
identity provider;
|
||||
- downstream consumers would be misled by the old `user-engine` name.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Existing imports, package names, workplans, and docs remain stable.
|
||||
- The intent and interface card carry the strategic direction explicitly.
|
||||
- Naming pressure becomes a tracked decision rather than hidden scope drift.
|
||||
@@ -23,6 +23,9 @@ See also the cross-repo assessment in the net-kingdom repo:
|
||||
| `MembershipFactExporter` | outbound | Export user-engine-owned membership facts as read models for authorization systems. |
|
||||
| `EventOutbox` | outbound | Publish durable lifecycle/profile/catalog/membership events after commit. |
|
||||
| `AuditWriter` | local/outbound | Persist local audit and provide redacted summaries for platform audit sinks. |
|
||||
| `EvidenceReferenceExporter` | outbound | Export local audit/review material as identity-canon evidence references without owning the platform audit system. |
|
||||
| `PolicyControlReferenceResolver` | outbound | Resolve policy, control, review, exception, or waiver references for identity-domain traces. |
|
||||
| `LifecycleTaskSink` | outbound | Create or link lifecycle, review, remediation, or integration-gap work in a task system. |
|
||||
| `SecretProvider` | inbound | Load runtime secrets through environment/local config in standalone mode and scoped secret providers in platform mode. |
|
||||
|
||||
## Source-Of-Truth Rules
|
||||
@@ -33,6 +36,11 @@ See also the cross-repo assessment in the net-kingdom repo:
|
||||
- Identity providers are the source of truth for authentication-time subject,
|
||||
issuer, assurance, coarse role, and group claims.
|
||||
- Authorization systems decide whether an actor may perform an action.
|
||||
- Governance, security, and authorization systems own policy, control, review,
|
||||
exception, and waiver definitions. user-engine can reference them in identity
|
||||
context traces.
|
||||
- Task systems own lifecycle work queues. user-engine can create or link
|
||||
identity-domain work through an explicit task sink.
|
||||
- Event sinks and audit stores consume user-engine events; they do not become
|
||||
profile stores.
|
||||
|
||||
@@ -64,6 +72,41 @@ context:
|
||||
The domain layer should depend on this port, not on a concrete authorization
|
||||
client.
|
||||
|
||||
## Identity Context Shape
|
||||
|
||||
Every domain-facing identity context should be reducible to:
|
||||
|
||||
```yaml
|
||||
actor:
|
||||
issuer: string
|
||||
subject: string
|
||||
tenant: string
|
||||
principal_type: human | service | agent
|
||||
user:
|
||||
user_id: string
|
||||
account:
|
||||
account_id: string
|
||||
status: string
|
||||
identity_links:
|
||||
- issuer: string
|
||||
subject: string
|
||||
tenant: string
|
||||
application_id: string | null
|
||||
memberships:
|
||||
- scope_type: string
|
||||
scope_id: string
|
||||
kind: string
|
||||
canon_refs:
|
||||
entities: object
|
||||
relationships: object
|
||||
grant_like_refs: object
|
||||
evidence_refs: object
|
||||
gaps: [string]
|
||||
```
|
||||
|
||||
The context is a facade over NetKingdom IAM and security infrastructure. It
|
||||
does not issue tokens, decide authorization policy, or own credentials.
|
||||
|
||||
## Projection Types
|
||||
|
||||
- `self_service`: current-user view and allowed mutations.
|
||||
|
||||
@@ -14,6 +14,7 @@ projection, audit, and event behavior testable without a UI.
|
||||
| two_applications | Catalog ownership and projections prevent application data leakage. |
|
||||
| sensitive_redaction | Sensitive values are redacted in runtime and claims-enrichment projections. |
|
||||
| audit_event_replay | Mutations carry audit records, outbox events, and correlation ids. |
|
||||
| identity_canon_context | Actor, user, account, authenticated subject, authorization principal, tenant, membership, grant-like facts, and evidence references stay distinguishable. |
|
||||
|
||||
## Fixture Actors
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ from user_engine.domain.models import (
|
||||
AuthorizationDecision,
|
||||
AuthorizationEffect,
|
||||
AuthorizationRequest,
|
||||
CanonEntityReference,
|
||||
CanonRelationshipReference,
|
||||
Catalog,
|
||||
CatalogLifecycle,
|
||||
ExternalIdentity,
|
||||
@@ -41,6 +43,8 @@ __all__ = [
|
||||
"AuthorizationDecision",
|
||||
"AuthorizationEffect",
|
||||
"AuthorizationRequest",
|
||||
"CanonEntityReference",
|
||||
"CanonRelationshipReference",
|
||||
"Catalog",
|
||||
"CatalogLifecycle",
|
||||
"ExternalIdentity",
|
||||
|
||||
@@ -99,6 +99,27 @@ class AuthorizationEffect(StrEnum):
|
||||
NOT_APPLICABLE = "not_applicable"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanonEntityReference:
|
||||
concept: str
|
||||
identifier: str
|
||||
scope: str | None = None
|
||||
source_system: str = "user-engine"
|
||||
local_type: str | None = None
|
||||
evidence_id: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanonRelationshipReference:
|
||||
relationship_type: str
|
||||
source: CanonEntityReference
|
||||
target: CanonEntityReference
|
||||
scope: str | None = None
|
||||
source_system: str = "user-engine"
|
||||
evidence_id: str | None = None
|
||||
lifecycle_state: str = "active"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Actor:
|
||||
issuer: str
|
||||
|
||||
@@ -15,6 +15,7 @@ from user_engine.domain import (
|
||||
AuditRecord,
|
||||
AuthorizationDecision,
|
||||
AuthorizationRequest,
|
||||
CanonEntityReference,
|
||||
Membership,
|
||||
OutboxEvent,
|
||||
)
|
||||
@@ -76,6 +77,37 @@ class AuditWriter(Protocol):
|
||||
"""Persist an audit record."""
|
||||
|
||||
|
||||
class EvidenceReferenceExporter(Protocol):
|
||||
"""Export audit/review material as identity-canon evidence references."""
|
||||
|
||||
def export(
|
||||
self, audit_records: Iterable[AuditRecord]
|
||||
) -> tuple[CanonEntityReference, ...]:
|
||||
"""Return evidence references without owning the platform audit sink."""
|
||||
|
||||
|
||||
class PolicyControlReferenceResolver(Protocol):
|
||||
"""Resolve policy/control references for identity-domain traces."""
|
||||
|
||||
def references_for(
|
||||
self, request: AuthorizationRequest, decision: AuthorizationDecision
|
||||
) -> Mapping[str, CanonEntityReference]:
|
||||
"""Return policy, control, review, or exception references when known."""
|
||||
|
||||
|
||||
class LifecycleTaskSink(Protocol):
|
||||
"""Handoff identity-domain gaps or lifecycle work to a task system."""
|
||||
|
||||
def create_or_link(
|
||||
self,
|
||||
*,
|
||||
summary: str,
|
||||
subject: CanonEntityReference,
|
||||
evidence: Iterable[CanonEntityReference] = (),
|
||||
) -> CanonEntityReference:
|
||||
"""Return the task reference created or linked by the downstream system."""
|
||||
|
||||
|
||||
class SecretProvider(Protocol):
|
||||
"""Load runtime secret material from the active environment."""
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ from user_engine.domain import (
|
||||
AttributeDefinition,
|
||||
AuditRecord,
|
||||
AuthorizationRequest,
|
||||
CanonEntityReference,
|
||||
CanonRelationshipReference,
|
||||
Catalog,
|
||||
CatalogLifecycle,
|
||||
ExternalIdentity,
|
||||
@@ -89,6 +91,23 @@ class Projection:
|
||||
redactions: Mapping[str, str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IdentityContext:
|
||||
actor: Actor
|
||||
user: User
|
||||
account: Account
|
||||
identities: tuple[ExternalIdentity, ...]
|
||||
tenant: str
|
||||
application_id: str | None
|
||||
memberships: tuple[Membership, ...]
|
||||
entity_refs: Mapping[str, CanonEntityReference]
|
||||
relationship_refs: tuple[CanonRelationshipReference, ...]
|
||||
grant_like_refs: tuple[CanonEntityReference, ...]
|
||||
evidence_refs: tuple[CanonEntityReference, ...]
|
||||
profile: EffectiveProfile | None = None
|
||||
gaps: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TenantDiagnostics:
|
||||
tenant: str
|
||||
@@ -643,6 +662,89 @@ class UserEngineService:
|
||||
redactions=redactions,
|
||||
)
|
||||
|
||||
def identity_context(
|
||||
self,
|
||||
actor: Actor,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
tenant: str | None = None,
|
||||
application_id: str | None = None,
|
||||
include_profile: bool = False,
|
||||
correlation_id: str | None = None,
|
||||
) -> IdentityContext:
|
||||
tenant_context = self.resolve_tenant_context(actor, tenant)
|
||||
identity = self.store.find_identity(*actor.identity_key)
|
||||
if user_id is None:
|
||||
if identity is None:
|
||||
raise NotFoundError("identity link not found")
|
||||
user_id = identity.user_id
|
||||
user = self._require_user(user_id)
|
||||
account = self._require_account(user_id)
|
||||
correlation_id = correlation_id or new_id("corr")
|
||||
self._authorize(
|
||||
actor,
|
||||
action="identity_context.read",
|
||||
resource_type="user-engine:identity-context",
|
||||
resource_id=user_id,
|
||||
tenant=tenant_context.tenant,
|
||||
correlation_id=correlation_id,
|
||||
application_id=application_id,
|
||||
target_user_id=user_id,
|
||||
context={"include_profile": include_profile},
|
||||
)
|
||||
identities = tuple(
|
||||
identity
|
||||
for identity in self.store.identities.values()
|
||||
if identity.user_id == user.user_id
|
||||
)
|
||||
memberships = self.store.memberships_for_user(
|
||||
user.user_id, tenant=tenant_context.tenant
|
||||
)
|
||||
evidence_refs = self._identity_evidence_refs(
|
||||
user,
|
||||
account,
|
||||
memberships,
|
||||
tenant_context.tenant,
|
||||
)
|
||||
entity_refs = self._identity_entity_refs(
|
||||
actor,
|
||||
user,
|
||||
account,
|
||||
identities,
|
||||
memberships,
|
||||
tenant_context.tenant,
|
||||
application_id,
|
||||
)
|
||||
relationship_refs, grant_like_refs = self._identity_relationship_refs(
|
||||
entity_refs,
|
||||
memberships,
|
||||
tenant_context.tenant,
|
||||
evidence_refs[0].identifier if evidence_refs else None,
|
||||
)
|
||||
profile = (
|
||||
self._resolve_effective_profile(
|
||||
user.user_id, application_id, tenant_context.tenant
|
||||
)
|
||||
if include_profile
|
||||
else None
|
||||
)
|
||||
gaps = () if evidence_refs else ("evidence:no-audit-records",)
|
||||
return IdentityContext(
|
||||
actor=actor,
|
||||
user=user,
|
||||
account=account,
|
||||
identities=identities,
|
||||
tenant=tenant_context.tenant,
|
||||
application_id=application_id,
|
||||
memberships=memberships,
|
||||
entity_refs=entity_refs,
|
||||
relationship_refs=relationship_refs,
|
||||
grant_like_refs=grant_like_refs,
|
||||
evidence_refs=evidence_refs,
|
||||
profile=profile,
|
||||
gaps=gaps,
|
||||
)
|
||||
|
||||
def tenant_diagnostics(
|
||||
self,
|
||||
actor: Actor,
|
||||
@@ -737,6 +839,216 @@ class UserEngineService:
|
||||
context["actor_subject"] = actor.subject
|
||||
return context
|
||||
|
||||
def _identity_entity_refs(
|
||||
self,
|
||||
actor: Actor,
|
||||
user: User,
|
||||
account: Account,
|
||||
identities: tuple[ExternalIdentity, ...],
|
||||
memberships: tuple[Membership, ...],
|
||||
tenant: str,
|
||||
application_id: str | None,
|
||||
) -> Mapping[str, CanonEntityReference]:
|
||||
actor_identifier = f"{actor.issuer}:{actor.subject}"
|
||||
refs: dict[str, CanonEntityReference] = {
|
||||
"actor": CanonEntityReference(
|
||||
concept="Actor",
|
||||
identifier=actor_identifier,
|
||||
scope=actor.tenant,
|
||||
source_system=actor.issuer,
|
||||
local_type=actor.principal_type.value,
|
||||
),
|
||||
"user": CanonEntityReference(
|
||||
concept="User",
|
||||
identifier=user.user_id,
|
||||
scope=tenant,
|
||||
local_type="user-engine-user-record",
|
||||
),
|
||||
"account": CanonEntityReference(
|
||||
concept="Account",
|
||||
identifier=account.account_id,
|
||||
scope=tenant,
|
||||
local_type=account.status.value,
|
||||
),
|
||||
"authenticated_subject": CanonEntityReference(
|
||||
concept="Authenticated Subject",
|
||||
identifier=actor_identifier,
|
||||
scope=actor.tenant,
|
||||
source_system=actor.issuer,
|
||||
local_type="issuer-subject",
|
||||
),
|
||||
"authorization_principal": CanonEntityReference(
|
||||
concept="Authorization Principal",
|
||||
identifier=f"{tenant}:{actor_identifier}",
|
||||
scope=tenant,
|
||||
local_type=actor.principal_type.value,
|
||||
),
|
||||
"tenant": CanonEntityReference(
|
||||
concept="Tenant",
|
||||
identifier=tenant,
|
||||
scope=tenant,
|
||||
local_type="isolation-scope",
|
||||
),
|
||||
}
|
||||
if application_id is not None:
|
||||
refs["application_scope"] = CanonEntityReference(
|
||||
concept="Scope",
|
||||
identifier=application_id,
|
||||
scope=tenant,
|
||||
local_type="application",
|
||||
)
|
||||
for identity in identities:
|
||||
refs[f"identity_record:{identity.identity_id}"] = CanonEntityReference(
|
||||
concept="Identity Record",
|
||||
identifier=identity.identity_id,
|
||||
scope=identity.issuer,
|
||||
source_system=identity.issuer,
|
||||
local_type=identity.provider,
|
||||
)
|
||||
refs[f"identifier:{identity.identity_id}"] = CanonEntityReference(
|
||||
concept="Scoped Identifier",
|
||||
identifier=f"{identity.issuer}:{identity.subject}",
|
||||
scope=identity.issuer,
|
||||
source_system=identity.issuer,
|
||||
local_type="issuer-subject",
|
||||
)
|
||||
for membership in memberships:
|
||||
scope_key = f"{membership.scope_type}:{membership.scope_id}"
|
||||
refs.setdefault(
|
||||
scope_key,
|
||||
CanonEntityReference(
|
||||
concept=_scope_concept(membership.scope_type),
|
||||
identifier=membership.scope_id,
|
||||
scope=tenant,
|
||||
local_type=membership.scope_type,
|
||||
),
|
||||
)
|
||||
refs[f"membership:{membership.membership_id}"] = CanonEntityReference(
|
||||
concept="Membership Relationship",
|
||||
identifier=membership.membership_id,
|
||||
scope=tenant,
|
||||
local_type=membership.kind,
|
||||
)
|
||||
refs[f"role:{membership.membership_id}"] = CanonEntityReference(
|
||||
concept="Role",
|
||||
identifier=f"{membership.scope_id}:{membership.kind}",
|
||||
scope=membership.scope_id,
|
||||
local_type=membership.kind,
|
||||
)
|
||||
return refs
|
||||
|
||||
def _identity_relationship_refs(
|
||||
self,
|
||||
entity_refs: Mapping[str, CanonEntityReference],
|
||||
memberships: tuple[Membership, ...],
|
||||
tenant: str,
|
||||
evidence_id: str | None,
|
||||
) -> tuple[tuple[CanonRelationshipReference, ...], tuple[CanonEntityReference, ...]]:
|
||||
account_ref = entity_refs["account"]
|
||||
subject_ref = entity_refs["authenticated_subject"]
|
||||
principal_ref = entity_refs["authorization_principal"]
|
||||
user_ref = entity_refs["user"]
|
||||
tenant_ref = entity_refs["tenant"]
|
||||
relationships = [
|
||||
CanonRelationshipReference(
|
||||
relationship_type="belongs_to_tenant",
|
||||
source=user_ref,
|
||||
target=tenant_ref,
|
||||
scope=tenant,
|
||||
evidence_id=evidence_id,
|
||||
),
|
||||
CanonRelationshipReference(
|
||||
relationship_type="authenticates_as",
|
||||
source=account_ref,
|
||||
target=subject_ref,
|
||||
scope=tenant,
|
||||
evidence_id=evidence_id,
|
||||
),
|
||||
CanonRelationshipReference(
|
||||
relationship_type="evaluated_as",
|
||||
source=subject_ref,
|
||||
target=principal_ref,
|
||||
scope=tenant,
|
||||
evidence_id=evidence_id,
|
||||
),
|
||||
]
|
||||
grant_like_refs: list[CanonEntityReference] = []
|
||||
for key, ref in entity_refs.items():
|
||||
if key.startswith("identity_record:"):
|
||||
relationships.append(
|
||||
CanonRelationshipReference(
|
||||
relationship_type="identity_link",
|
||||
source=ref,
|
||||
target=user_ref,
|
||||
scope=ref.scope,
|
||||
evidence_id=evidence_id,
|
||||
)
|
||||
)
|
||||
for membership in memberships:
|
||||
membership_ref = entity_refs[f"membership:{membership.membership_id}"]
|
||||
role_ref = entity_refs[f"role:{membership.membership_id}"]
|
||||
scope_ref = entity_refs[f"{membership.scope_type}:{membership.scope_id}"]
|
||||
grant_ref = CanonEntityReference(
|
||||
concept="Access Grant",
|
||||
identifier=membership.membership_id,
|
||||
scope=tenant,
|
||||
local_type=membership.kind,
|
||||
evidence_id=evidence_id,
|
||||
)
|
||||
grant_like_refs.append(grant_ref)
|
||||
relationships.extend(
|
||||
(
|
||||
CanonRelationshipReference(
|
||||
relationship_type="member_of",
|
||||
source=user_ref,
|
||||
target=scope_ref,
|
||||
scope=tenant,
|
||||
evidence_id=evidence_id,
|
||||
),
|
||||
CanonRelationshipReference(
|
||||
relationship_type="role_label",
|
||||
source=membership_ref,
|
||||
target=role_ref,
|
||||
scope=tenant,
|
||||
evidence_id=evidence_id,
|
||||
),
|
||||
CanonRelationshipReference(
|
||||
relationship_type="scoped_to",
|
||||
source=grant_ref,
|
||||
target=tenant_ref,
|
||||
scope=tenant,
|
||||
evidence_id=evidence_id,
|
||||
),
|
||||
)
|
||||
)
|
||||
return tuple(relationships), tuple(grant_like_refs)
|
||||
|
||||
def _identity_evidence_refs(
|
||||
self,
|
||||
user: User,
|
||||
account: Account,
|
||||
memberships: tuple[Membership, ...],
|
||||
tenant: str,
|
||||
) -> tuple[CanonEntityReference, ...]:
|
||||
membership_ids = {membership.membership_id for membership in memberships}
|
||||
subjects = {user.user_id, account.account_id, *membership_ids}
|
||||
evidence = []
|
||||
for record in self.store.audit_records:
|
||||
if record.tenant != tenant:
|
||||
continue
|
||||
if record.subject not in subjects and record.summary != "membership.added":
|
||||
continue
|
||||
evidence.append(
|
||||
CanonEntityReference(
|
||||
concept="Evidence Source",
|
||||
identifier=record.audit_id,
|
||||
scope=tenant,
|
||||
source_system="user-engine:audit",
|
||||
local_type=record.summary or record.action,
|
||||
)
|
||||
)
|
||||
return tuple(evidence)
|
||||
|
||||
def _session(self, actor: Actor, user: User, account: Account) -> UserSession:
|
||||
identities = tuple(
|
||||
identity
|
||||
@@ -1089,6 +1401,16 @@ def _optional_claim(actor: Actor, key: str) -> str | None:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _scope_concept(scope_type: str) -> str:
|
||||
concepts = {
|
||||
"team": "Team",
|
||||
"tenant": "Tenant",
|
||||
"application": "Scope",
|
||||
"group": "Group",
|
||||
}
|
||||
return concepts.get(scope_type, "Scope")
|
||||
|
||||
|
||||
def _visible_in_projection(
|
||||
definition: AttributeDefinition, projection_type: ProjectionType
|
||||
) -> bool:
|
||||
|
||||
159
tests/test_identity_canon_alignment.py
Normal file
159
tests/test_identity_canon_alignment.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import unittest
|
||||
|
||||
from user_engine.adapters.local import InMemoryUserEngineStore, LocalAuthorizationCheckPort
|
||||
from user_engine.domain import AccountStatus, ProjectionType
|
||||
from user_engine.errors import AuthorizationDenied
|
||||
from user_engine.service import UserEngineService
|
||||
from user_engine.testing.fixtures import (
|
||||
FixtureIdentityClaimsAdapter,
|
||||
human_actor_claims,
|
||||
sample_application,
|
||||
sample_application_binding,
|
||||
sample_catalog,
|
||||
)
|
||||
from user_engine.testing.scenarios import service_claims
|
||||
|
||||
|
||||
class IdentityCanonAlignmentTests(unittest.TestCase):
|
||||
def test_identity_context_separates_user_account_subject_and_principal(self):
|
||||
service, _, _ = _service()
|
||||
session = _bootstrap(service, _claims(subject="ada-admin", tenant="tenant:acme"))
|
||||
service.add_membership(
|
||||
session.actor,
|
||||
session.user.user_id,
|
||||
tenant="tenant:acme",
|
||||
scope_type="team",
|
||||
scope_id="team:platform",
|
||||
kind="tenant-admin",
|
||||
correlation_id="corr-acme-admin",
|
||||
)
|
||||
|
||||
context = service.identity_context(
|
||||
session.actor,
|
||||
application_id="app.demo",
|
||||
include_profile=True,
|
||||
correlation_id="corr-context",
|
||||
)
|
||||
|
||||
self.assertEqual(context.entity_refs["user"].concept, "User")
|
||||
self.assertEqual(context.entity_refs["account"].concept, "Account")
|
||||
self.assertEqual(
|
||||
context.entity_refs["authenticated_subject"].concept,
|
||||
"Authenticated Subject",
|
||||
)
|
||||
self.assertEqual(
|
||||
context.entity_refs["authorization_principal"].concept,
|
||||
"Authorization Principal",
|
||||
)
|
||||
self.assertNotEqual(
|
||||
context.entity_refs["account"].identifier,
|
||||
context.entity_refs["authenticated_subject"].identifier,
|
||||
)
|
||||
self.assertEqual(context.entity_refs["team:team:platform"].concept, "Team")
|
||||
self.assertEqual(context.profile.values["demo.display_density"], "comfortable")
|
||||
self.assertFalse(context.gaps)
|
||||
|
||||
relationship_types = {
|
||||
relationship.relationship_type for relationship in context.relationship_refs
|
||||
}
|
||||
self.assertIn("authenticates_as", relationship_types)
|
||||
self.assertIn("evaluated_as", relationship_types)
|
||||
self.assertIn("member_of", relationship_types)
|
||||
self.assertIn("scoped_to", relationship_types)
|
||||
self.assertEqual(context.grant_like_refs[0].concept, "Access Grant")
|
||||
self.assertEqual(context.grant_like_refs[0].local_type, "tenant-admin")
|
||||
self.assertTrue(context.evidence_refs)
|
||||
|
||||
def test_identity_context_handles_service_account_projection(self):
|
||||
service, _, _ = _service()
|
||||
session = _bootstrap(service, service_claims())
|
||||
|
||||
context = service.identity_context(
|
||||
session.actor,
|
||||
correlation_id="corr-service-context",
|
||||
)
|
||||
|
||||
self.assertEqual(context.entity_refs["actor"].local_type, "service")
|
||||
self.assertEqual(context.entity_refs["account"].concept, "Account")
|
||||
self.assertEqual(
|
||||
context.entity_refs["authorization_principal"].local_type,
|
||||
"service",
|
||||
)
|
||||
|
||||
def test_small_saas_tenant_context_denies_globex_without_scope(self):
|
||||
service, _, _ = _service()
|
||||
session = _bootstrap(service, _claims(subject="ada-admin", tenant="tenant:acme"))
|
||||
service.set_tenant_account_status(
|
||||
session.actor,
|
||||
session.user.user_id,
|
||||
AccountStatus.ACTIVE,
|
||||
tenant="tenant:acme",
|
||||
correlation_id="corr-acme-account",
|
||||
)
|
||||
|
||||
with self.assertRaises(AuthorizationDenied):
|
||||
service.identity_context(
|
||||
session.actor,
|
||||
tenant="tenant:globex",
|
||||
correlation_id="corr-globex-context",
|
||||
)
|
||||
|
||||
def test_identity_context_can_be_read_as_claims_enrichment_input(self):
|
||||
service, _, authz = _service()
|
||||
session = _bootstrap(service, _claims(subject="claim-user", tenant="tenant:acme"))
|
||||
|
||||
projection = service.projection(
|
||||
session.actor,
|
||||
session.user.user_id,
|
||||
ProjectionType.CLAIMS_ENRICHMENT,
|
||||
application_id="app.demo",
|
||||
tenant="tenant:acme",
|
||||
correlation_id="corr-claims",
|
||||
)
|
||||
context = service.identity_context(
|
||||
session.actor,
|
||||
application_id="app.demo",
|
||||
include_profile=True,
|
||||
correlation_id="corr-claims-context",
|
||||
)
|
||||
|
||||
self.assertEqual(projection.values["demo.display_density"], "comfortable")
|
||||
self.assertEqual(context.application_id, "app.demo")
|
||||
self.assertIn("identity_context.read", [request.action for request in authz.requests])
|
||||
|
||||
|
||||
def _service():
|
||||
store = InMemoryUserEngineStore()
|
||||
authz = LocalAuthorizationCheckPort()
|
||||
service = UserEngineService(
|
||||
store=store,
|
||||
identity_adapter=FixtureIdentityClaimsAdapter(),
|
||||
authorization=authz,
|
||||
)
|
||||
return service, store, authz
|
||||
|
||||
|
||||
def _bootstrap(service: UserEngineService, claims: dict[str, object]):
|
||||
session = service.me(claims, correlation_id="corr-me")
|
||||
service.register_application(
|
||||
session.actor,
|
||||
sample_application(),
|
||||
binding=sample_application_binding(),
|
||||
correlation_id="corr-app",
|
||||
)
|
||||
service.publish_catalog(
|
||||
session.actor,
|
||||
sample_catalog(),
|
||||
correlation_id="corr-catalog",
|
||||
)
|
||||
return session
|
||||
|
||||
|
||||
def _claims(*, subject: str, tenant: str) -> dict[str, object]:
|
||||
claims = human_actor_claims(subject=subject, tenant=tenant)
|
||||
claims["roles"] = ["tenant-admin"]
|
||||
return claims
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -4,7 +4,7 @@ type: workplan
|
||||
title: "Identity Domain Canon Alignment"
|
||||
domain: netkingdom
|
||||
repo: user-engine
|
||||
status: proposed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
planning_priority: high
|
||||
@@ -59,7 +59,7 @@ exists for that context?
|
||||
|
||||
```task
|
||||
id: USER-WP-0007-T1
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "09bf2de5-0dab-4c21-845a-ff7dbde4cbd8"
|
||||
```
|
||||
@@ -73,7 +73,7 @@ work.
|
||||
|
||||
```task
|
||||
id: USER-WP-0007-T2
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8d10eaf7-12ac-4a7c-bf90-ded6fc59eeb4"
|
||||
```
|
||||
@@ -86,7 +86,7 @@ models and service operations to canon concepts and relationships including
|
||||
|
||||
```task
|
||||
id: USER-WP-0007-T3
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "c3839ae6-5d82-4cfd-819a-79b3bdf6efa6"
|
||||
```
|
||||
@@ -98,7 +98,7 @@ without requiring consumers to know IAM provider details.
|
||||
|
||||
```task
|
||||
id: USER-WP-0007-T4
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "2d29ceec-2d0b-4753-82cc-3fd87a252ba1"
|
||||
```
|
||||
@@ -110,7 +110,7 @@ Membership, and Grant or grant-like membership facts.
|
||||
|
||||
```task
|
||||
id: USER-WP-0007-T5
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "1ad927a2-eca7-4904-a666-600617cb7519"
|
||||
```
|
||||
@@ -122,7 +122,7 @@ audit reference export, policy/control references, and lifecycle task handoff.
|
||||
|
||||
```task
|
||||
id: USER-WP-0007-T6
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "cb0dbd53-fb9a-4b84-87d8-e7cc3ce4ab40"
|
||||
```
|
||||
@@ -134,7 +134,7 @@ review, approval, exception, remediation, or explicit evidence gaps.
|
||||
|
||||
```task
|
||||
id: USER-WP-0007-T7
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "973bf3c5-e407-4441-a2e6-c9c8d5e55135"
|
||||
```
|
||||
@@ -146,7 +146,7 @@ tracked work rather than silent scope drift.
|
||||
|
||||
```task
|
||||
id: USER-WP-0007-T8
|
||||
status: todo
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "e6285175-7b71-4a57-9183-dab8274f19a6"
|
||||
```
|
||||
@@ -186,3 +186,28 @@ organization responsibilities.
|
||||
- small-SaaS canon conformance tests
|
||||
- evidence-gap and lifecycle task examples
|
||||
- naming decision record
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Implemented on 2026-06-05:
|
||||
|
||||
- Added `CanonEntityReference` and `CanonRelationshipReference` domain shapes.
|
||||
- Added `UserEngineService.identity_context(...)` as the first canon-facing
|
||||
identity-domain read model.
|
||||
- Added adapter contracts for evidence export, policy/control references, and
|
||||
lifecycle task handoff.
|
||||
- Added `docs/canon-interface-card.yaml`, `docs/canon-mapping.md`,
|
||||
`docs/evidence-gap-examples.md`, and
|
||||
`docs/identity-domain-naming-decision.md`.
|
||||
- Updated scope, contracts, NetKingdom integration docs, README, and scenarios.
|
||||
- Added executable identity-canon alignment tests for distinct user/account/
|
||||
subject/principal context, service-account projection, small-SaaS tenant
|
||||
isolation, and claims-enrichment context.
|
||||
|
||||
Verification:
|
||||
|
||||
```text
|
||||
make test
|
||||
Ran 35 tests in 0.175s
|
||||
OK
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user