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>
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>
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>
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>
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>