Files
qbit-authentik/docs/INVITATION-ASSIGNMENTS.md
james.maesandGPT-6 2e356b8117
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow f1d46718955078331817c6206b822a94bc7d769f] (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
Shared Java Maven CI with Docker / secrets (push) Successful in 38s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 38s
qbit release candidate / cut-rc (pull_request) Failing after 18s
Release candidate / release-rc (pull_request) Failing after 18s
qbit release candidate / tag-and-release (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Failing after 15m39s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Failing after 24s
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Failing after 1h14m34s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 10s
Shared Java Maven CI with Docker / verify (push) Failing after 17m19s
Shared Java Maven CI with Docker / gate (push) Failing after 24s
qrun-release / release (push) Failing after 1h16m30s
qrun-release / postpublish (push) Skipped
feat(authentik): checkpoint application access and identity synchronization RC
Add user and group QQQ application-role access editing, bounded catalogs,
webhook inbox and SQL mirrors, refreshed session permissions, and deferred
invitation assignments. Include the isolated local review host, browser
acceptance scripts, documentation and the token-widget column mapping fix.

User approved this RC checkpoint. Full 1.0 gates remain documented, including
the schema-drift matcher and PostgreSQL integration errors from the full gate.
Companion UI: QRun-IO/qqq-frontend-next#205.

Co-Authored-By: GPT-6 <noreply@openai.com>
2026-11-05 05:29:54 -06:00

203 lines
18 KiB
Markdown

# Invitation assignment implementation
Automatic group assignment after enrollment remains a 1.0 requirement. The
invitation form and admin action now save group/QQQ-role selections when MIRROR,
WEBHOOK and MEMBERSHIP are configured. The deferred event-to-membership worker
is connected through AuthentikAdmin with live original-operator authorization,
audit-before-write and durable attempts. The native email/signup/membership
workflow now passes on a SQL host. Rendered UI acceptance and the remaining
failure/recovery scenarios are still required before release.
## Pinned Authentik behavior
Inspection of the owned Authentik 2026.8.3 image establishes that the invitation
stage emits `invitation_used` and deletes a single-use invitation before the user
write stage creates the account. The event records `context.invitation_uuid`;
its user is not necessarily the future enrollee. The later native `user_write`
event records the created user's pk and a server-set `context.created` flag.
Fixed invitation data is merged into the pending enrollment data before this
write. The contract `InvitationEnrollmentContractIT` exercises this through the
real flow executor, including its same-endpoint stage redirects.
Verified 2026-11-05: `target/release-invitation-enrollment-live-03.log` passes
419 unit tests, this real-provider contract and coverage. The contract observes
the consumed invitation returning 404 while no user exists, then verifies the
new account and matching `user_write` event. Earlier setup failures corrected
the stage endpoint (`/stages/invitation/stages/`) and client redirect handling;
no provider repair was needed.
Neither the invitation-used event's user nor an email lookup is sufficient to
identify the account that should receive a deferred grant. A correlation value
is only a lookup key for pending intent; it is not authorization by itself.
## Implemented event verification
`AuthentikInvitationEvidence` now fetches native events by UUID through the
uncached, audit-read-guarded `AuthentikActionUtil.syncEvent` method. It checks
event ID, native action, exact flow executor path, the saved invitation/reference
and the supplied intent time window. User creation requires a strict boolean
`created=true` and a positive integral user pk; impersonation/agent markers fail
closed. It returns only minimal immutable facts, never grants or raw context.
API errors, missing events and malformed data produce sanitized failures.
The verifier is connected to the SQL inbox and deferred admin writer. Expected
values and time bounds come from persisted intent. Immutable paired evidence,
protected flow validation and one-time group attempts provide the additional
binding and replay protections described below.
Verified: `target/release-invitation-evidence-live.log` passes 423 unit tests,
the real enrollment contract using the QBit's non-superuser service account for
event verification, and coverage. Focused rejection and schema checks also pass
in `target/release-invitation-evidence-unit.log`.
## Remaining implementation and verification
### Server-owned correlation boundary
Native prompt stages merge validated form fields into `prompt_data`. A configured
editable field named `qqq_invitation_reference` can therefore overwrite the value
copied from invitation fixed data. Matching that value in `user_write` alone is
not proof that the account consumed the intended invitation.
The packaged `authentik/invitation-reference-policy.py` expression restores the
reference from the invitation object retained in the server's flow plan. Bind it
to the user-write stage binding with `evaluate_on_plan=false`,
`re_evaluate_policies=true` and `policy_engine_mode=all`, after all prompts and
immediately before user-write. Set the mode explicitly: Authentik defaults to `any`.
Authentik's re-evaluation marker disables policy caching. The expression first
removes submitted correlation and only restores a canonical UUID from that
server-held invitation; it does not assign any memberships. It contains no
credentials and must not log flow data.
Verified by `target/release-invitation-policy-live-04.log`: 426 unit tests, two
real Authentik enrollment cases and coverage pass with no failures or skips.
The cases submit an editable forged reference after consuming an invitation:
the saved reference is restored when present, and a fabricated reference is
removed when no reference was saved. Both use native prompt submission and
service-account event verification. Earlier runs stopped in test-client setup
because they incorrectly required a CSRF cookie for anonymous enrollment; the
corrected client sends the anonymous POST through the native executor.
`AuthentikInvitationFlow` now validates live configuration before the enrollment
evidence helper returns a created-user fact. It requires an enrollment flow with
one invitation stage before one user-write stage, unambiguous stage ordering,
stage-time re-evaluation and exactly one policy on the write binding. The policy
must be enabled, non-negated, non-expiring, fail closed on execution errors, have
execution logging disabled and match the packaged source (allowing platform
newlines and surrounding whitespace only). Additional write-binding policies are
rejected because policy evaluation can be concurrent. Place other enrollment
validation before this protected write stage.
Configuration reads are audit-read guarded, uncached, bounded and server-filtered;
incomplete pagination and mismatched target/stage IDs fail closed. Required
service-account reads add `authentik_flows.view_flow`,
`authentik_flows.view_flowstagebinding` and
`authentik_policies_expression.view_expressionpolicy` to the existing
`authentik_policies.view_policybinding`. No provider configuration writes are
granted. The test bootstrap contains these read permissions; host deployment must
apply the corresponding role update. `target/release-invitation-flow-live-04.log`
passes 429 unit tests, both native enrollment cases and coverage with no failures
or skips. It verifies configuration through the non-superuser service account
and rejects the same enrollment event after the policy binding is disabled.
The provider uses different IDs for policy lookup and serialization: filter
policy bindings by `policybindingmodel_ptr_id`, but compare their returned
`target` with the concrete flow-stage binding `pk`. The regression fixtures keep
these IDs distinct. Also, Authentik 2026.8.3's policy-binding serializer requires
`target` and `policy` in an `enabled` PATCH; the native test supplies those fields.
The invitation action uses this validation before saving selected assignment
intent; the deferred worker also uses it before applying grants.
An event field or host declaration alone is not sufficient. Unprotected flows
remain unsupported for automatic assignments.
The SQL intent store now saves immutable operator/group intent, binds a single
enrollment identity and serializes per-group attempts across workers. A started
attempt is never automatically reset after a crash or lost response. Expiry
prevents new work; a matching existing attempt can still record its result after
expiry. Daily retention removes only COMPLETE/CANCELLED intent more than thirty
days after expiry in the current scope. Unresolved intent remains for review.
The invitation action starts its normal audit before saving intent, creates the
provider invitation with the generated reference and matching expiry, attaches
the provider ID durably, and only then sends email. Storage failure stops further
provider actions. Failure to attach after creation reports that an invitation
exists but email was not sent, for operator review before retrying. SQL access
temporarily uses the internal session and restores the original operator before
every provider write. Submitted operator identity fields are ignored.
The existing paged group/QQQ-role picker is registered when MIRROR, WEBHOOK and
MEMBERSHIP are enabled. Planning and execution require current user/membership
management authority, the existing subset/protected-group rules, an active scoped
operator with a canonical immutable UUID, at most 100 selected groups/roles and
a reason of at most 2048 characters. Configured flow UUIDs resolve server-side to
the actual slug and must match exactly.
Cancellation stops pending intent even if the provider invitation was already
consumed or expired. An in-flight attempt cannot be promised cancelled and is
refused for review; completed membership changes are not undone. The connected
event worker persists authoritative facts under inbox ownership, then reserves
each automatic grant before calling the normal audited admin path. Focused
creation/cancellation tests use actual H2 SQL and fixture provider HTTP.
Verified by `target/release-invitation-create-integration.log`: 438 unit tests,
44 integration tests (42 PostgreSQL and two native Authentik enrollment cases)
and coverage pass with no failures or skips. The public admin action tests cover
intent-before-creation, attachment-before-email, cancellation after consumption
and expiry, changed flow after preview, audit/storage/attachment failures,
session restoration and revoked live membership authority. Native contracts also
verify configured UUID-to-slug resolution. The later native gate below extends
this evidence to actual email delivery and deferred membership.
Native workflow verified by `target/release-invitation-native-grants-03.log`:
460 unit tests, two Authentik enrollment contracts and coverage pass without
failures or skips. The saved-reference case invokes the public invitation
action with the non-superuser service bearer, verifies Mailpit delivery, submits
a username plus forged correlation through the native flow, then delivers the
real enrollment event before consumption to the production host inbox processor.
It asserts the actual provider membership and a SUCCEEDED administrative audit
stored in SQL. The second case still proves that a fabricated reference is
removed when none was saved. Temporary provider permissions are restored.
This is HTTP flow-executor acceptance, not rendered browser/UI acceptance or a
test of provider notification delivery to the webhook endpoint.
1. Complete rendered UI acceptance and native invitation/email acceptance for the
new group/QQQ-role selection and persisted intent path.
2. Verify native event details through the Authentik API using the event UUID.
Associate consumption and successful new-user creation with the stored
invitation and configured enrollment flow. Handle either delivery order,
duplicate hints, missing events, expired/cancelled invitations and account
identity changes. Never take grant values or user identity from a webhook
payload alone. Correlation replay/copying between enrollments needs explicit
adversarial coverage before this can authorize writes.
3. Route each assignment through `AuthentikAdmin`, retaining audit-before-write,
current operator authority, protected-account checks, subset rules and any
required approval. Persist progress so retries do not re-add a membership
after a later intentional removal. Ambiguous remote write outcomes must be
visible for review rather than silently replayed.
4. Verify the full operator invitation → native signup → durable event handling
→ audited membership flow on the SQL host and real Authentik. Include lost,
reordered and duplicate delivery, partial completion, cancellation, revoked
operator permissions, changed groups and spoofed correlation/event data.
The native enrollment contract establishes provider behavior only. It does not
prove the authorization, persistence, UI or assignment requirements above.
## Invitation event pairing verified
Consumption and enrollment can now be persisted independently in either delivery order. First verified facts are immutable; the combined bind API uses the same transitions and cannot bypass a saved identity. Rejected pairs roll back both mutations, invalid chronology becomes REVIEW, and duplicate completed events cannot reset APPLIED progress. H2/PostgreSQL regressions cover both orders, restart, conflicting identity, rollback and replay. Missing configured invitation flow now refuses creation, including execute-time drift.
Gate target/release-invitation-pairing-postgres.log passed 441 units +37 AuthentikMirrorPostgresIT tests, zero failures/errors/skips and coverage; session78862 terminal0. Initial restricted run had nine localhost fixture errors (no assertion failures); rerun used approved localhost/Docker access. No source changes during final gate. No review restart, commit or release.
NEXT: add connection-aware event fact persistence inside inbox.withClaim (same SQL transaction, no nested independent commit); connect authoritative event lookup/verification to saved intent; implement audited membership execution under original immutable operator identity and CURRENT authority. Keep system-session write prohibition. Add simultaneous event delivery/lease-loss/cancellation/expiry coverage and native full signup-to-membership acceptance. Overall 1.0 goal remains active.
## Invitation inbox evidence worker connected
AuthentikInvitationEvents now handles invitation_used/user_write from the production AuthentikApplyChangeEvents.forHost path. It re-fetches authoritative event details, checks event ID/action, looks up only saved intent, uses the existing flow/time/reference verifier, and binds enrollment only to a live active scoped immutable user UUID. Envelope username/timestamp are not assignment authority. Consumption skips mirror refresh because it precedes account creation; enrollment retains existing mirror processing.
Store connection-aware transitions join inbox.withClaim's SQL transaction; no independent nested commit, no network work under lock. Tests prove rollback invisibility, reclaimed lease rejection, lease loss during HTTP reads, mismatched event identity/action/flow rejection, concurrent independent claims preserving both facts, and public batch pairing in either order without provider writes.
Final target/release-invitation-worker-postgres.log passes445units +41AuthentikMirrorPostgresIT tests, zero failures/errors/skips and coverage; session70988 terminal0. Focused02 passes2 tests; first focused run exposed a fixture returning a user for a different username, fixed to return an empty server-filtered result. Production scope validation retained. No Java edits during finalgate, no review restart/commit/release.
NEXT: implement AuthentikAdmin-controlled deferred grant execution from saved READY intent. Each group needs original operator pk+UUID and CURRENT live permission validation, active scoped target UUID check, flow protection, normal guard/protected/self/subset/audit-before-write, durable begin/finish and no replay after ambiguous outcomes. Do not weaken system-session write prohibition or expose generic impersonation. operator() currently resolves ordinary sessions by email; deferred path must be pinned to stored immutable identity, not reuse a later email owner. Add current-authority revocation, target/operator replacement, cancellation/expiry/lease loss and provider-write failure tests; then full native signup/email/membership and rendered UI acceptance. Automatic memberships are still not executed by this worker. Full1.0 remains active.
## Deferred invitation membership execution connected
AuthentikAdmin.applyInvitationAssignments accepts only the internal worker plus saved reference/claim. READY intent reserves each group attempt in the inbox ownership transaction. A private delegated admin pins original operator pk+UUID, reads active scoped identities uncached, derives current group/static/host permissions, requires both USERS_MANAGE and MEMBERSHIPS_MANAGE, verifies protected enrollment flow, and creates a short-lived human QSession only for normal ADD_MEMBER execution. No caller-supplied identity/action/grants enter this path. Original worker context is always restored; guard's system-session write prohibition remains unchanged. Revalidation immediately before provider write retains protected/self/subset checks and original current authority. No-change counts as settled. Errors mark REVIEW; failed outcome storage leaves IN_FLIGHT. Tickets cannot be replayed after completion/ambiguity/manual removal. Read-only or disabled infrastructure suspends new attempts without writing provider.
AuthentikInvitationEvents now calls that writer after successful fenced evidence commit. FakeAuthentik now respects server-side UUID filters (needed for production syncUser reads).
New SQL/admin tests cover audit RUNNING and IN_FLIGHT before HTTP, human attribution/context restoration, original/target UUID replacement or inactivity, revoked rights before and after preparing delegation, disabled flow, protected target/superuser/escalated group, internal-caller requirement, missing audit, read-only/cancel/lost ownership, ambiguous remote side effect and no replay after manual removal. A public invitation -> SQL inbox -> authoritative event/flow verification -> admin grant test passes with only provider HTTP simulated. Existing native enrollment tests remain separate.
Final target/release-invitation-grants-postgres.log:460units +41AuthentikMirrorPostgresIT tests, zero failures/errors/skips and coverage; session85250 terminal0. Focused56 tests passed earlier. No Java changes during finalgate. No review restart/commit/release.
NEXT: full native SQL-host invitation/email/signup/inbox/audited membership acceptance and rendered UI. Reuse or extend InvitationEnrollmentContractIT protected flow; public INVITE_USER fixed_data has email/name/reference but no username, so native flow must prompt for username. SessionSqlHost provides H2 sync tables and QQQ RECORD audit; admin action tables need configured backend as in QbitWriteContractIT. Grant only needed temporary provider permissions through stack.grantTemporaryPermissions and restore them. Add partial multi-group failure, expiry/cancellation races and native changed/revoked original authority cases. Current live review still predates changes. Broader mirror views, browser/deployed-node auth, scale, staging/rollback and release supply-chain gates remain open. Full1.0 goal active.
- Pause recovery: READY assignments in read-only mode or with MEMBERSHIP disabled retain their inbox trigger through deferral; re-enabling resumes with current authorization. Verified by both pause-mode SQL tests and release-invitation-pause-postgres.log (462 units +41 PG/coverage). MIRROR/WEBHOOK must remain enabled for the worker to be registered.