A host's authentication customizer runs on every request of a session. Policy
resolution read the live permission universe once for every Allow term, and each
reading recomputed the PolicyCatalog fingerprint, walked every catalog resource
and called every host-name function. A role of nine named permissions therefore
built nine universes per request; a full-access document built one.
- PolicyRules.allowed, resolve and withheld read the universe they are given once
per call (PolicyUniverse.once), so resultFromClaims, fromClaims, both claims
mappers, the access editor and AuthentikAdmin cost one universe per resolution
whatever the policy holds. PolicyParser.parseForSave reads it once per document.
- PolicyCatalog.of compares the instance with the kept fingerprint piece by piece
instead of building the fingerprint text on every call; the same content is
compared, so every in-place metadata change still gives a new catalog on the
next call. The catalog keeps its permission names and its part of the universe.
- Nothing is cached across resolutions: no universe, no decision. The catalog
cache is per JVM and derived only from that JVM's instance, as before.
- PolicyResult.replacement(): the grant alone (Allow minus Deny), for a host whose
sessions take their permissions only from the token. apply() is unchanged and
remains a union with what the host passes.
Measured on a 1,400-object instance (212 tables, 985 processes, 199 widgets), one
resolution: 9 named permissions 12.1 ms and 7.9 MB to 0.33 ms and 0.12 MB; 30
named 28.7 ms and 25.3 MB to 0.32 ms and 0.14 MB; full access 1.19 ms and 1.37 MB
to 0.77 ms and 0.20 MB.
Tests: PolicyResolutionCostTest counts universe builds (1 for 1, 9 and 30 names,
0 for a void or absent claim), pins that one reading grants exactly what a reading
per term grants, and holds an allocation budget; PolicyCatalogTest changes every
fingerprinted aspect in place. The golden vectors are untouched and pass.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
With AUTHENTIK_QBIT_TOKENS off the check could only ever report UNKNOWN
"not configured", and core has no way to remove a registered check, so the
health page showed a permanent UNKNOWN row. Register it only when
getTokensEnabled() is true; the other checks are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The shared CI runner image has no python3, so the 'fail without python' rule made
every build red at the unit stage. The python run now skips unless
BLUEPRINT_PYTHON_REQUIRED=true; a pure-Java test keeps the blueprint's safety
shape checked everywhere.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Bootstrap identity permissions are added to an operator after the Deny filter, so
a void or denying group cannot lock them out of repairing policies. A token with
no groups claim and no policy claim is unavailable (an empty array stays known).
The mirror restores a policy attribute verbatim only when it parses. Docs mark the
resolve/resolveResult/apply helpers as not for sign-in assembly. More mapper tests
(void claim alone, bootstrap under void, guard error at sign-in, specific
QAuthenticationException on refused mirror snapshots).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
SessionPermissionRules.compute serves sign-in, automated sessions and the
refreshing mapper: void leaves break-glass only even while the guard blocks;
unavailable leaves only configured outage fallbacks; known applies every Deny to
policy grants, mirror permissions, static and host floors and earlier session
permissions. The refreshing mapper reads Deny rules from the mirrored group rows
on the same connection (checkpoint-consistent, bounded). Automated sessions void
on an unknown group pk and no longer keep earlier permissions when unresolved.
The guard keeps only a real last state (never OFF, original time, 15 min bound).
The mirror stores the policy document unredacted. Blueprint rejects extra
top-level keys and a missing Statement.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The cut-over guard's blocked branch applies valid Deny rules (claim and graph) to
floor, host source and existing permissions; a guard error keeps the last known
state. Static and host floors are subject to Deny everywhere (bootstrap exempt),
including the refreshing mapper, which now reads Deny rules from the mirror
without an API call. PolicyResult gains UNAVAILABLE; a failed graph read with no
claim voids earlier permissions; resolveResult reports the full rules. Missing
parents void. Documents with unknown top-level keys or no Statement are
rejected. The group fingerprint covers the configured policy attribute. Stronger
tests for release, grandparent Deny, and the blueprint (fails without python).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Documents with an unknown top-level key or no Statement are rejected (new
rawDocuments vectors); a Deny subtracts static and host floors, also while the
guard blocks; unreadable and void states; fingerprint covers the policy attribute.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Operator credit never includes a name their own policies deny; the configured
policy attribute is reserved against attribute edits; un-voiding an invalid
policy is a release; a deliberate Deny no longer trips the cut-over guard and
void is shown; a group missing from the graph voids; PolicyResult typed API
with apply(); Deny of a prefix covers dotted host names; empty arrays and bad
host names reject; Save Policy requires expectedFingerprint; the catalog cache
key covers all rule sources and the runtime shares the cached catalog.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Deny of a prefix or named resource covers its dotted host names; empty
Resource/Action/Permission arrays reject; the blueprint voids on any document
under the attribute that is not exactly our spec (skipping other specs is an
opt-in) and on duplicate keys. Spec: what a Deny subtracts, void is not empty,
operators, host customizer API, blueprint known differences.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PolicyParser returns typed PolicyRules (Allow and Deny terms, or void) instead
of permission names, so a Deny or prefix cannot be read as a grant. Result =
union(Allow) minus union(Deny) across all of a user's groups; one invalid
policy voids every group of the user. Allow expands against one live universe;
Deny is a predicate. Canonicalization never drops a Deny. Editor: save-time
zero-match checks, effective-permission subset rule, release of Deny on
removal, last-admin check. Host names register without an Authentik runtime;
PolicyCatalog is cached per QInstance. Default PolicyVersion is 1.1.0.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Permission names are dot-separated segments of [A-Za-z0-9_-] (bare names
allowed); Resource stays [A-Za-z0-9_]+ and utils_* prefixes. Every Allow
expands against one universe, host names are not features, a standard action
a resource lacks is dropped, and the claim is one merged document.
New vectors: hyphenated and bare Permission names, invalid names, hyphenated
Resource, host name as Action, dropped action, not-in-universe name.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spec and shared golden vectors only; no parser change yet.
- Effect Allow|Deny; result = union(Allow) minus union(Deny) across all of
a user's groups, Deny wins, no ordering, expanded against the live
instance so a Deny also covers future resources.
- Prefix resources (utils_*): at least two characters, literal part ends in
an underscore, wildcard last. Deny with Action * means every action
including features; Allow with Action * stays the five standard actions.
- Permission[] key for exact dotted/host permission names under Allow and
Deny, so Deny can remove what full access includes.
- Zero-match prefix, feature or named permission is rejected at save for
both effects. Unknown statement keys (Condition) reject the document.
- One invalid document among a user's groups voids all of them: dropping a
bad group could drop a Deny and escalate access.
- Canonicalization never drops a Deny.
- policy-vectors-1.1.0.json: 27 vectors over a 16-name universe, to be run
by this parser, the authentik-cd claim validator and Forge's tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With twoPersonApproval=true, the native Create Group action persisted a
new group carrying authentik.* permissions (for example
authentik.users.read) immediately, while Edit Group queued an
authentik_approval_request for a second person. Apply the same
identity-permission expansion and second-person condition at creation
time: the plan becomes NEEDS_APPROVAL and nothing is written until a
distinct operator holding authentik.approve approves it; the stored
request keeps the native requester/approver and live revalidation
checks.
Regression test: creating a group with authentik.users.read queues one
approval request and writes nothing, self-approval is refused, a second
operator with approve + groups.manage completes the creation, the
persisted group carries exactly the requested permission, the request is
APPROVED, and a repeat approval is refused.
Carried locally by GG Forge as qbit-authentik-rc8-create-approval.patch
since 0.1.0-RC.8.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Replace the platform-ci release callers with one caller of
KofTwentyTwo/ci-workflows maven.yaml (qqq-orb port).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A degraded authentik.mapper check said only "Mapper drift detected" with
findingCodes=group_attributes.warning, so nobody could tell which group
was wrong or why without opening the drift report.
Group-attribute findings now carry a short, safe reason
(DriftFinding.reason): "grants nothing: <permission>", "malformed
<attribute>" or "superuser group carries qqq.io/permissions". The health
message and the new flaggedGroups and summary details name each flagged
group with its reasons, within the 500-character limits. Typed attribute
values (malformed entries, unprintable permission names) and
service-account activity subjects (people) stay out of the outcome.
The "grants nothing" drift message also notes that a permission an app
checks only in code, not in its metadata, is not in the catalog.
A group with no qqq.io/* attributes (a group relying on the host's
static floor) is not drift; a test pins that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Maintenance (itself a principal) refuses groups named service:... or under
qqq-service-accounts: only the owner changes principal grants (security
review H3).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop the self-made MaintenanceSession that granted itself
policies.manage and the document's grants (rejected under OAuth2). Run
as the host's declared maintenance principal: its Authentik service
account's grants decide; the subset rule still applies. The audit
operator is service:{principal} and the job name prefixes the reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The broker provider allows only token exchange, so the client_credentials
probe always got 400 (Invalid grant_type for provider). Probe with token
exchange and no subject. Authentik checks grant type, then secret, then
subject, so invalid_request means the credentials passed. invalid_client
and invalid_grant are DOWN. Refusals carry their OAuth2 error code
(BrokerRefusedException).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QRun-IO/.github#2 adds a tag-and-release job that signs vX with the
QRUN_TAG_SIGNING_KEY secret after Maven Central confirms the artifact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On release branches the bundle's version and the producer's differed, so the
bundle test that asserts they match failed, and released bundles recorded
SNAPSHOT as their provenance. help/*.yaml is now filtered:
version: ${project.version}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Host-neutral help (deployment-specific facts removed), registered with
HelpBundles, gated by HelpBundleAssertions. QQQ 4.1.0-RC.7.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add help/qbit-authentik.yaml (QQQ help-bundle format) for the Identity app and
all 15 tables, applied by AuthentikHelp (STAND-IN for core #1198). API-backed
tables say rows are read live and what an empty list means. Add
AuthentikTablesGuardrailTest (used at runtime, explained, documented).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Custom-checker and named permissions (for example cdn.purge) are not listed
by PermissionsHelper.getAllAvailablePermissions, so full access missed them.
AuthentikQBitConfig.withFullAccessHostPermissions lets the host declare them;
they are computed at use time and only for the instance holding the runtime.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AuthentikMaintenance.applyPolicy(backend, groupName, policyJson,
operatorName, reason) lets reviewed host code (a run-once job) set a
group's policy as a named maintenance operator (run-once:/maintenance:),
never as a system session. It runs the same Save Policy plan and
execute: audit row before the Authentik call, custom event, guardrails
(protected and superuser groups, scope, readOnly, POLICIES), stale check,
optional two-person approval. An equivalent policy is a NO_CHANGE no-op.
Plain Java API only; nothing is exposed over HTTP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner request (2026-10-14): a "*" access policy for full access. The
statement {"Effect":"Allow","Action":["*"],"Resource":["*"]} grants
every permission the instance defines, expanded when permissions are
computed (sign-in, resolver, access comparison, member preview), so new
tables, processes, apps, reports, widgets and features are covered with
no edit. Resource "*" with any other action rejects the document. The
canonical form is the single statement; saving needs the operator to
hold the full set. Editor payload: fullAccess.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Authentik QBit produced" line logged the configured feature set, so
TOKENS (on by default whenever writes are enabled) never appeared.
getEffectiveFeatures() reports what hasFeature() says is on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor widget now lists the group's members (direct and through
descendant groups, active and in scope, capped at 200) with each
person's sign-in permissions WITHOUT this group's policy
(basePermissions), plus mapperEnabled. The Next widget computes the
live effective permissions for a chosen member and what the edit adds
or removes. EffectivePermissionResolver gains an exclude-one-policy
resolution; loading requires authentik.policies.manage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves qqq-orb@0.6.8 to qqq-orb@0.6.9, which passes --update-snapshots so cached SNAPSHOT dependencies are re-checked on every build (QRun-IO/qqq-orb#22).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QQQ 4.1.0-RC.6 (now the qbit's baseline) ships the notification SPI
(qqq#1125), so the reflection stand-in is gone: guard incidents use
QAlerts.raise, owner reminders QAlerts.notify.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- DESIGN.md decision 31 (owner, 2026-10-14): the qbit role holds
add_token (needed by service_account and by set_key); view_token_key
stays not granted.
- TokenContractIT pins 403 without add_token and 200/204 with it, and
that the alarm notifies identity-admins when the qbit's account creates
a service account (fixture = cluster-gitops#103 blueprint, verbatim).
- Reminders go through core QAlerts.notify (qqq#1125) by reflection,
never another qbit's classes (G1); guard alerts through core QAlerts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move to the released 4.1.0-RC.6, which includes core
TableFeaturePermissionsMetaData (QRun-IO/qqq#1136). The policy catalog
discovers features through core only; the test-only injection overload
is removed. The policy feature tests register real metadata instead of
the synthetic catalog, so the expired STAND-IN marker is gone.
Adopt qbit-build-parent 2.1.0 with the guardrails enforced and strict
raw-route mode.
Closes#20 (architecture review QRun-IO/qqq#1150, finding F19).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TokenContractIT now exchanges app passwords exactly like the generated
usage text (no client_secret); green on Authentik 2026.8.3. Also logs the
cause when a token scope cannot be verified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lookup runs while a bearer session is still being built (no user), so
the guard refused the Authentik read and the scope failed closed for
every token. Read Authentik and the scope row as QSystemUserSession, then
restore the caller's context. Found by the kof22 adoption test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host whose RDBMS backend maps camelCase to snake_case got
last_reminder7d, which does not match docs/schema.sql (found by the
kof22 adoption's changeset test).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Scopes: Create Token takes read only, apps and tables when
tokenScopesEnabled (validated against the instance and the operator's
own access). AuthentikTokenScopes reads the signed qqq_token claim in
both mappers (and AuthentikTokenScopes.apply for hosts with their own
customizer): no row = unscoped, deleted token = no permissions, any
verification failure = no permissions. STAND-IN for core
QCredentialScope (qqq#1168, RC.7): permissions are intersected; the
core scope is attached by reflection when present (empty dimension =
null = unrestricted).
- Last used from Authentik login events (method token, 90 days, cached
5 minutes); createdAt and scopeSummary from the scope row.
- Daily expiry reminders (7 days, 1 day) through the optional Alerts
producer, deduplicated on the scope row; guard-bypass alert.
- Admin widget on the app home (authentik.tokens.manage): by owner, by
age, unused for tokenUnusedDays, deactivated owners; bulk Revoke Token
on the list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- TokenContractIT (9 tests) against the production token blueprint
(verbatim copy of cluster-gitops kof22-qqq-tokens.yaml): caller-owned
create, PATCH defaults, expiry cap (brand, groups, user), token
exchange as the user via the broker, the deny policy for protected
users, the qqq_token claim (profile only), service_account + set_key +
PATCH, the detective alarm, and the qbit lifecycle with the one-time
key absent from audit rows and events.
- Findings now in DESIGN.md: service_account and set_key need add_token
(safe: a non-superuser create is always the caller's); set_key events
use the model reference shape.
- Service-account lifetime cap is a user attribute
(serviceAccountTokenLifetime, default days=365), not a group.
- Test fixtures carry the production role; SuperuserFloorIT accepts the
400 from /recovery/ with reset_user_password (the qbit never calls it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TOKENS feature, on by default with writes (AUTHENTIK_QBIT_TOKENS=false
opts out). Create, rotate, revoke and extend tokens, and create service
accounts, as record and table actions with the standard Review/Result.
- People's tokens are created as that person through the token broker
(RFC 8693 exchange); service accounts through service_account, set_key
and PATCH (always intent, user and expires, with a race guard).
- Guardrails: protected owners (superusers, authentik Admins,
identity-admins, qqq.io/protected, the qbit account, configurable
groups), self-only for people, required capped expiry, optional
two-person approval for service-account tokens.
- One-time key via core one-time process output (stand-in until
qqq#1162 ships; refuses at Review when unavailable), never in tables,
audit, events, logs or process state; usage text from config.
- authentik_token_scope table; authentik.tokens.manage permission.
- In-memory Authentik models the 2026.8.3 token semantics.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the core baseline from 4.1.0-RC.2 to the released 4.1.0-RC.5. No
source changes were needed. Comments now reflect core's opt-in permission
enforcement in 4.1. The synthetic feature-permission catalog in tests is
tagged STAND-IN(core=QRun-IO/qqq#1136, removeBy=4.1.0-RC.6), because
TableFeaturePermissionsMetaData is on develop but not in RC.5.
Refs #20 (architecture review QRun-IO/qqq#1150, finding F19).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>