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>
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>
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>
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>
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>
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 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>
- 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>
- 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>
AuthentikGroupPermissionMapper implements customizeAutomatedSessionForUser.
Server-minted sessions (scheduled saved reports, SSH-key users of
qbit-file-gateway) carry no token, so they got no Authentik permissions.
The email idReference must match exactly one active Authentik user; that
user's direct groups and ancestors go through the same resolver, static
fallbacks and cut-over guard as sign-in. Anything else, or an unreadable
API, grants only the static fallbacks. customizeSession is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decisions (2026-10-13) 23-30: tokens managed in the app and stored
in Authentik, shown once, self-service through Preferences, on by default
with an opt-out, guardrails (a)-(f), optional per-token scopes enforced
through core QCredentialScope (qqq#1153), reminders, last used, admin
views and bulk revoke.
Decision 30 records the owner-approved mechanism revision: Authentik
2026.8.3 always creates a token for the caller unless it is a superuser,
so people's tokens are created as that person through RFC 8693 token
exchange (token broker with a deny policy), service accounts through
service_account, set_key and PATCH. The qbit role gains change_token and
set_token_key and never holds add_token or view_token_key.
Adds docs/ACCESS-TOKENS.md ("Using access tokens").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The catalog lists each table's feature permissions, read generically from
PermissionsHelper.getAllAvailablePermissions (core
TableFeaturePermissionsMetaData), as a features array per table resource.
Policies grant them by name; the * wildcard still means the five
standard actions only, so existing policies grant nothing new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AuthentikHostPermissionSource SPI (today's access) and config
mapperEnabled, cutoverGuardGroups, mapperOverride, staticGroupPermissions.
- Enforcement status OFF/ACTIVE/OVERRIDDEN/BLOCKED: banner widget on group
and policy records and the Edit Policy step ("Policies are saved but NOT
enforced at sign-in on this instance (mapper off)"), and a row in the home's
Connection card.
- Compare Access widget (authentik.admin, read only): per active person,
today vs the mapper, added/removed/unchanged, guard result.
- Cut-over guard: while a guarded-group member would lose a permission (no
override), the mapper grants today's access plus the static fallback and
bootstrap subjects instead of the policies.
- Generate Policy From Current Access group action; operator's access today
counts for the subset rule.
- DESIGN.md 12.8 with the one-switch rollback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Edit User: username (validated, unique) and other attributes (JSON object;
qqq.io/ and goauthentik.io/ keys kept and refused; blank keeps them; needs
authentik.audit.read, which also gates showing them). Edit Group: other
attributes the same way.
- Delete User and Delete Group behind the new DELETE feature (off by default;
Authentik delete_user/delete_group): typed confirmation, never yourself, a
protected account, a superuser group or the last identity administrator; a
group needs no members and no child groups; its policy row goes with it.
- Tests on the in-memory Authentik and a delete contract test on 2026.8.3;
README, STATUS, CHANGELOG, CLAUDE.md invariant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Edit Policy record action on groups and policies (feature POLICIES,
permission authentik.policies.manage): streamed-ETL with the
AUTHENTIK_POLICY_EDITOR widget on its step; Review parses fail-closed,
regenerates canonically with the round-trip check, refuses new resources the
app does not define, applies the subset rule to added permissions and lists
the changes; Result merge-patches qqq.io/policy into the group's current
attributes (removed when empty). Optional policyTwoPersonApproval.
- PolicyCatalog: apps, tables, processes (hidden record actions included),
reports and protected widgets with the actions QQQ checks, grouped by app;
joins the permission catalog.
- authentik_policy local table (one row per group, field-level audit as the
change history), nav and child list on the group; DDL in docs/schema.sql.
- Widget authentikPolicyEditor: catalog plus the live document, flagged when
changed in Authentik since the last QQQ save.
- Tests: catalog, admin rules, process, widget, gating, approval, and a
savePolicy contract test on Authentik 2026.8.3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PolicyParser (fail closed), PolicyGenerator (canonical form, round-trip
check), PolicyFormat and config (policySpec/Version/Attribute/Claim,
policyTwoPersonApproval).
- AuthentikPolicyClaimMapper and PolicyPermissions for the token claim, with
an applySecurityKeys hook for record locks; AuthentikGroupPermissionMapper
adds the claim too.
- GroupGraph and EffectivePermissionResolver read each group's qqq.io/policy,
so effective permissions, the subset rule and the last-admin check see
policy grants; an invalid policy is listed as malformed.
- PolicyClaimMappingIT pins the scope-mapping blueprint example on Authentik
2026.8.3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN.md revision 5: owner decisions of 2026-10-13 (17-22), new section 12
(policy model stored on Authentik groups, fail-closed parsing, canonical
generator with round-trip check, claim mapping, the policy editor, delete
user/group behind the DELETE feature, authentik.policies.manage), plus
docs/POLICY-FORMAT.md and the scope-mapping blueprint example.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Users can change their own email in Authentik and emails are not unique, so
bootstrapOperators (emails) let a user become an identity operator by setting
their email to the owner's. Replaced by bootstrapOperatorSubjects: token sub
(email-looking subs ignored) or, with a verified email, the single active user
with that email whose pk or uuid is configured. Emails in config fail validation.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Permissions follow the owner's full-name rule (2026-10-12; spec #1065
amended in 8ee607610): identity.users.read -> authentik.users.read, etc.,
same suffixes; the wildcard is authentik.*.
- IdentityPermission names and WILDCARD renamed; @Deprecated
LEGACY_PREFIX, LEGACY_WILDCARD and getLegacyShortName() keep the old
names for upgrade scripts. Legacy names are never granted: the resolver
reports them as unknown and the drift report warns on them.
- UPGRADE.md: rename table, update qqq.io/permissions group attributes
(UI or blueprint) before deploying, sample SQL for host-stored grants,
sign users out afterwards.
- Code comments, drift messages, test fixtures, the test bootstrap
blueprint, README, CHANGELOG and docs/DESIGN.md use authentik.*.
The identity-admins group and the "Identity" app label are unchanged.
- Tests: IdentityPermissionTest (names, session form, legacy names not
resolved), resolver test that legacy grants are unknown and grant
nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Table names are now lowercase snake_case prefixed with the qbit name:
authentik_user, authentik_group, authentik_group_member,
authentik_session, authentik_event (were authentikUser, ...).
- AuthentikQBitConfig.applyTablePrefix(name) = snake(prefix) + "_" +
snake(name), plus a toSnakeCase helper. applyPrefix keeps the camelCase
join for apps, widgets, the backend and processes (authentikAdmin,
authentikApi), as spec #1065 says.
- @Deprecated AuthentikTables.LEGACY_*_TABLE_NAME constants hold the old
names for upgrade scripts.
- UPGRADE.md covers stored references (permissions, saved views, audit
table names, links). API-backed tables, so no DDL.
- README, CHANGELOG and docs/DESIGN.md use the new names.
- Tests: snake helper, prefix composition (default, empty, custom
camelCase prefix), producer and contract tests on the new names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: phase 0 foundations (read-only client, contract tests on Authentik 2026.8.3)
- AuthentikQBitConfig (validated, secrets resolved by the qbit, never
serialized or printed) and AuthentikQBitProducer (API backend, read-only
authentikUser / authentikGroup tables, Identity app).
- AuthentikActionUtil on qqq-backend-module-api: bearer auth, page/page_size
paging with pagination.count, key lookups by GET, server-side user scope,
AuthentikFilterMapper (rejects whatever Authentik cannot evaluate),
10 rps token-bucket throttle, 429/5xx retries, error mapping, and logging
that never carries the token, query strings, successful GET bodies or
sensitive bodies (AuthentikRedactor).
- AuthentikGuard: identity.* read checks, sensitive fields behind
identity.audit.read, all writes refused in this release, protected-account
detection.
- Pinned Authentik 2026.8.3 schema, SchemaDriftTest / SchemaDriftIT over
the API surface used, and a weekly drift workflow against new releases.
- Testcontainers harness (Authentik server + worker, Postgres, Mailpit) with
the kof22-shaped blueprints, fixture recorder, and contract tests (b), (c),
(d), (h) plus the access-check, sign-in policy, webhook and token findings.
- Owner checklist, rehearsal script and blueprint lint for the cluster-gitops
migration (KofTwentyTwo/cluster-gitops#72).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: phase 1 read-only MVP (memberships, sessions, events, effective permissions, drift report)
- authentikGroupMember: virtual membership table served from a group
(include_users), a user (groups_obj) or a bounded sweep; filters by group
pk/name, user pk/username or key; user scope checked per member.
- authentikSession and authentikEvent, behind identity.audit.read.
- GroupGraph + EffectivePermissionResolver: groups and inherited parents to
QQQ permissions; identity.* and catalog wildcards expanded, unknown names
reported and never granted. Groups show inherited superuser status and
effective permissions; users show effective permissions.
- Identity drift report widget (identity.admin): version, blueprint status,
service-account shape and self-minted tokens, break-glass, last identity
admin, qqq.io/* attribute validity, and service-account changes to
protected or superuser objects.
- Contract tests on Authentik 2026.8.3 (real session via the flow executor;
the drift report flags SuperuserFloorIT's lockout probes) and recorded
fixtures for the new unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: group permission mapper; harden phase 0/1 after code review
- AuthentikGroupPermissionMapper: token groups claim -> QQQ permissions via
the group graph and resolver; static fallbacks and bootstrapOperators;
Authentik unreachable grants only the fallbacks, never everything.
- Review fixes (general and silent-failure reviews):
- plain-text-only search, because Authentik's QLSearch could reach
sensitive fields
- thread-safe group graph
- a failed group-graph read shows "Unavailable", not empty permissions
- malformed qqq.io/* attributes are reported by the drift report
- operator filters may not widen the user scope; membership rows are
always scope-checked
- a missing pagination object is an error
- sorting by the key falls back to the default; strict booleans; ".."
keys refused
- https required except loopback / allowInsecureHttp; groupScopeNamePrefix
refused until implemented
- drift: unrunnable checks are ERROR "result unknown"; service-account
tokens filtered server-side, zero visible is a warning; deleted or
vanished targets flagged; only human users count as identity admins;
break-glass re-checked client-side
- redact error bodies before flattening; version check retried after a
failed fetch
- rehearse-phase0.sh: portable under Git Bash, waits for both blueprints,
derived is_superuser; passed on a synthetic production-like dump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat: phase 0 foundations (read-only client, contract tests on Authentik 2026.8.3)
- AuthentikQBitConfig (validated, secrets resolved by the qbit, never
serialized or printed) and AuthentikQBitProducer (API backend, read-only
authentikUser / authentikGroup tables, Identity app).
- AuthentikActionUtil on qqq-backend-module-api: bearer auth, page/page_size
paging with pagination.count, key lookups by GET, server-side user scope,
AuthentikFilterMapper (rejects whatever Authentik cannot evaluate),
10 rps token-bucket throttle, 429/5xx retries, error mapping, and logging
that never carries the token, query strings, successful GET bodies or
sensitive bodies (AuthentikRedactor).
- AuthentikGuard: identity.* read checks, sensitive fields behind
identity.audit.read, all writes refused in this release, protected-account
detection.
- Pinned Authentik 2026.8.3 schema, SchemaDriftTest / SchemaDriftIT over
the API surface used, and a weekly drift workflow against new releases.
- Testcontainers harness (Authentik server + worker, Postgres, Mailpit) with
the kof22-shaped blueprints, fixture recorder, and contract tests (b), (c),
(d), (h) plus the access-check, sign-in policy, webhook and token findings.
- Owner checklist, rehearsal script and blueprint lint for the cluster-gitops
migration (KofTwentyTwo/cluster-gitops#72).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: drop committed Python bytecode and ignore it
The lint script's __pycache__/*.pyc was committed by accident: it is
generated output and an unreviewable binary (K22-REPO-05).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Set up from qbit-template-extension with the build used by
qbit-sql-workbench and qbit-quick-search: qbit-build-parent 2.0.0 with the
qqq 4.1.0-RC.2 BOM profile, Maven wrapper, Testcontainers ITs under
failsafe, JaCoCo gate, and the qqq-orb 0.6.8 CircleCI workflows (develop
snapshots, release/* RCs, v* releases). docs/DESIGN.md is the approved spec
(QRun-IO/qqq#1065). README, CLAUDE.md and AGENTS.md describe the layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>