net-kingdom/docs/service-group-provenance.md
tegwick 4f65e41ac5
Some checks failed
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Identity provider journey acceptance / provider (push) Failing after 20s
Build and Publish identity-provisioner / build-and-push (push) Successful in 8s
fix(identity): retain group provenance and prepare sealed recovery custody
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
2026-10-17 00:28:17 +02:00

7.3 KiB

Durable service group creation provenance

This source change narrows the existing authenticated identity-provisioner /v1/groups/ensure and /v1/groups/revoke endpoints. It introduces no new principal, credential, policy decision, or rollback authorization. An endpoint's service token and a journal receipt do not establish an accepted case, caller session, owner assent, or permission to use a protected execution lane.

Request and receipt migration

Ensure keeps groups, members, idempotency_key and correlation_id. The original correlation is now part of its immutable binding: the original idempotency key with a different correlation or group/member request is refused. The response adds ensure_idempotency_key, ensure_correlation_id, created_groups (name and exact directory id) and created_memberships (name, directory id and username). The owner journal independently retains the complete prestate and expected postimage. Response inventories are advisory; caller-supplied inventories are rejected and never authorize undo.

Revoke retains the original group/member request, its own idempotency_key and correlation_id, and optional boolean delete_if_empty. It additionally requires ensure_idempotency_key and ensure_correlation_id from the original ensure. Legacy revoke requests fail before directory access. No old deployment inventory is imported as proof of creation: pre-upgrade groups and memberships cannot be rolled back automatically without independent owner reconciliation.

An ensure replay returns its original receipt and never recreates subsequently removed state. Reusing its key after rollback is refused. Revoke replays return its original outcome; changing deletion options after completion is refused. There are at most 16 groups, 256 requested membership entries, 4096 retained preexisting members per operation, and 256 journal records by default. Records have no timeout or automatic eviction. Capacity exhaustion stops new operations.

Storage policy and local fixture configuration

The implemented SQLite backend is for local development and disposable tests. It is not an admitted production storage design. Accepted ADR-0015 section3 requires production relational storage through CloudNative PG and rapp-postgres using database-per-consumer; it explicitly rejects per-workload SQLite-on-a-PVC as a production pattern. The rail-kubernetes wave-1 contract also does not supply a generic persistent-storage contract. This source does not authorize adding a SQLite PVC to identity-provisioner.

Production group operations remain unadmitted and must stay disabled until the existing owner paths admit a CNPG-backed implementation of the same journal semantics, credentials, storage/backup/restore and writer-isolation contracts, or an explicit governed exception to ADR-0015 identifies the exact scope, limits, duration, recovery and risk owner. No such backend, exception or native assent is supplied here. railiance-platform owns shared database/custody governance; rapp-postgres owns the package consumer provisioning surface. The identity-provisioner runtime package/operating assignment must also be explicitly settled rather than inferred from User Engine's dependency on it.

For local/disposable tests only, GROUP_PROVENANCE_DB selects an absolute, non-symlink SQLite path in a private test directory owned by the process UID, with mode0700 for the directory and mode0600 for the database. The local fixture explicitly initializes a genuinely new empty store with GroupProvenance(absolute_path, initialize=True); initialization refuses an existing file. Ordinary opening never recreates a missing store. The completed disposable fixture uses private container tmpfs and preserves the file across separate Python processes; that proves process-restart semantics, not production durability. /tmp, emptyDir, tmpfs and an image layer do not satisfy native retention or recovery duties.

With no configuration, ordinary directory readiness reports service_groups: disabled and group endpoints refuse work. Missing/untrusted or unavailable configured local storage prevents startup or returns a fixed group_provenance dependency failure without path/database diagnostics. service_groups: available checks capability only: the implementation does not infer or enforce production admission, policy exceptions, accepted cases or writer isolation from a filesystem path.

The existing reference Deployment remains one Recreate replica at UID/GID10001 with readOnlyRootFilesystem: true, no provenance volume and an older immutable image. Image rebuild, native owner review and credential delivery remain separate acts. Preserve its nonroot/read-only boundaries. Native retention and recovery must preserve original idempotency bindings, prestate, confirmed creations and unresolved intents; losing a journal never authorizes replacement initialization and replay. No arbitrary JSON import, receipt expiry or automatic uncertainty clearing is introduced. Owner reconciliation needs independent evidence.

Safety and remaining native acceptance

Before each directory mutation, durable intent is committed. A successful response and exact readback are required before recording a confirmed creation. A definite failure before any mutation attempt retains a refused record and releases the active fence; its original key still cannot retry. Transport loss, readback failure after an attempted mutation or a process exit leaves pending/uncertain state; it survives reopening and blocks new mutations instead of becoming retry success. Every database connection rechecks the private path and original device/inode; missing/replaced storage is refused. Every intent update must affect exactly one matching journal row before execution can continue. The journal records mutation intent and confirmed outcomes, not an atomic LDAP transaction or a complete audit emission guarantee.

Rollback validates every targeted group id and complete member postimage before its first destructive operation, and verifies each subsequent change. It removes only memberships confirmed newly added by that ensure. It deletes only groups confirmed newly created by that ensure, and only while empty. Preexisting groups, including empty groups, are never deleted. Existing members are never included in the removal inventory. Detected shared-member drift or group replacement refuses the operation before any destructive call and retains uncertainty.

LLDAP exposes no membership-generation token or conditional mutation/CAS here. Readback cannot detect a remove/re-add ABA sequence or close a read/write race with an external writer. Native rollback therefore still requires an owner-proven writer isolation/fencing contract or a provider-side versioned/CAS operation; these synthetic fixtures do not prove that boundary. A durable SQLite file is trusted local owner-held state, not a production storage admission, tamper-evident remote evidence or an authorization broker. Existing caller authentication is unchanged; case/source/principal binding, per-attempt protected session revocation, independently admitted evidence callbacks, native storage/backup recovery and native endpoint acceptance remain outstanding under NK-WP-0048-T14/T16. This source change alone does not finish those tasks.