39 Commits
Author SHA1 Message Date
james.maesandClaude Fable 5.2 537e56d744 perf(policy): one permission universe per resolution; PolicyResult.replacement
maven / build (push) Successful in 15m31s
build / maven (push) Successful in 15m31s
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>
2026-11-10 18:51:54 -06:00
james.maesandClaude Sonnet 5.5 a76843d622 docs(policy): break-glass operators, absent groups claim, claim-only mapper
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 20:31:59 -06:00
james.maesandClaude Sonnet 5.5 81fa5b53bf docs(policy): one shared session rule, void/unavailable, blueprint shape
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 19:56:26 -06:00
james.maesandClaude Sonnet 5.5 4a375848f2 docs(policy): vectors and spec for document shape, blocked-guard and floors
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>
2026-11-08 19:23:59 -06:00
james.maesandClaude Sonnet 5.5 86aa863c7e docs(policy): vectors, spec and blueprint for review fixes
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>
2026-11-08 18:54:02 -06:00
james.maesandClaude Sonnet 5.5 9c76ff389e docs(policy): vectors and spec for hyphenated names, one universe, host names, claim shape
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>
2026-11-08 18:01:26 -06:00
james.maesandClaude Sonnet 5.5 6f9ec41f35 docs(policy): specify format 1.1.0 (Deny, prefix wildcards, named permissions)
maven / build (push) Failing after 45s
build / maven (push) Failing after 45s
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>
2026-11-08 17:28:33 -06:00
james.maesandGPT-6 2e356b8117 feat(authentik): checkpoint application access and identity synchronization RC
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (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-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
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
james.maesandClaude Opus 5.5 c9e5506cdf feat(policy): host-declared permission names join the full-access expansion
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>
2026-10-14 15:24:21 -05:00
james.maesandClaude Opus 5.5 b196109d84 feat(admin): maintenance API to apply a policy through the audited Save Policy path
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>
2026-10-14 15:17:28 -05:00
james.maesandClaude Opus 5.5 4d2bd07e4b feat(policy): full-access statement (Resource "*" + Action "*")
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>
2026-10-14 15:09:34 -05:00
james.maesandClaude Opus 5.5 612307bde8 feat(policy): member preview payload for the policy editor
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>
2026-10-14 12:13:13 -05:00
james.maesandClaude Opus 5.5 3722380db0 feat(tokens): owner decision A (add_token); alarm on service-account creation; core notify SPI
- 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>
2026-10-14 07:57:19 -05:00
james.maes eb74cd12db Merge remote-tracking branch 'origin/feature/token-management' into feature/token-scopes 2026-10-14 07:21:06 -05:00
james.maes 9dfef79929 Merge remote-tracking branch 'origin/develop' into feature/token-management
# Conflicts:
#	CHANGELOG.md
2026-10-14 07:20:40 -05:00
james.maesandClaude Opus 5.5 6c56a24960 docs(spec): no client secret for the PAT exchange; async one-time hand-off
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:28:57 -05:00
james.maesandClaude Opus 5.5 7f8e742120 feat(tokens): scopes, last used, reminders and admin views (DESIGN.md section 13, phase 6c)
- 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>
2026-10-13 22:26:53 -05:00
james.maesandClaude Opus 5.5 26a449c9e6 test(tokens): contract tests on Authentik 2026.8.3 and service-account lifetime on the user
- 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>
2026-10-13 21:59:00 -05:00
james.maesandClaude Opus 5.5 de7c015081 feat(tokens): access-token management (DESIGN.md section 13, phase 6b)
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>
2026-10-13 21:58:46 -05:00
james.maes 38d9776f25 Merge remote-tracking branch 'origin/develop' into feature/arch-rc5
# Conflicts:
#	CHANGELOG.md
2026-10-13 20:18:53 -05:00
james.maes 9eece9cde5 Merge pull request #21 from QRun-IO/feature/token-spec
docs(spec): access tokens as QQQ's personal access tokens (revision 6)
2026-10-13 20:01:45 -05:00
james.maesandClaude Opus 5.5 d138c5c911 chore(deps): build on QQQ 4.1.0-RC.5
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>
2026-10-13 19:51:47 -05:00
james.maesandClaude Opus 5.5 2aaedacffd docs(spec): token lifetime cap on the brand, claim bound to profile, separate broker blueprint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:48:42 -05:00
james.maesandClaude Opus 5.5 f151a7344a feat(permissions): Authentik permissions for automated sessions
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>
2026-10-13 19:48:33 -05:00
james.maesandClaude Opus 5.5 d9431e7e6b docs(spec): access tokens as QQQ's personal access tokens (revision 6)
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>
2026-10-13 19:42:28 -05:00
james.maesandClaude Opus 5.5 d3b740785f feat(policy): grant table feature permissions (analyze, comment, ...)
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>
2026-10-13 15:30:15 -05:00
james.maesandClaude Opus 5.5 af970d32db feat(policy): safe cut-over to policies (status, compare, guard, generate)
- 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>
2026-10-13 14:39:05 -05:00
james.maesandClaude Opus 5.5 a79f2d2d4b feat(crud): phase 5c full user and group CRUD
- 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>
2026-10-13 13:56:23 -05:00
james.maesandClaude Opus 5.5 639dc38ca1 feat(policy): phase 5b policy editor and save to Authentik
- 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>
2026-10-13 13:47:23 -05:00
james.maesandClaude Opus 5.5 616e3a8135 feat(policy): phase 5a policy model, parser, generator and runtime mapper
- 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>
2026-10-13 13:34:38 -05:00
james.maesandClaude Opus 5.5 077d9abbe8 docs(spec): policies (IAM-style statements) and full user/group CRUD
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>
2026-10-13 13:21:27 -05:00
james.maesandClaude Sonnet 5.5 645f782e42 fix(security): bootstrap operators keyed on immutable ids, never email
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>
2026-10-12 17:02:09 -05:00
james.maesandClaude Sonnet 5.5 ebd56d6ca4 feat: write contract tests, docs, status; recovery email prerequisites
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 16:54:45 -05:00
james.maesandClaude Opus 5.5 338b591a7f fix(naming): rename identity.* permissions to authentik.* (owner naming rule)
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>
2026-10-12 14:29:15 -05:00
james.maesandClaude Opus 5.5 ecad3e2e5b fix(naming): snake_case authentik_ table names (owner naming rule)
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>
2026-10-12 14:16:37 -05:00
james.maesandClaude Opus 5.5 75c66a8f8d Phase 1: read-only MVP (memberships, sessions, events, effective permissions, drift report, mapper) (#4)
* 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>
2026-10-12 06:08:37 -05:00
james.maesandClaude Opus 5.5 037d736bfd Phase 0: foundations (read-only client, Authentik 2026.8.3 contract tests) (#1)
* 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>
2026-10-12 06:03:21 -05:00
james.maesandClaude Opus 5.5 6afe1b045a chore: scaffold qbit-authentik (build, CI, docs, design)
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>
2026-10-11 19:54:03 -05:00
james.maes f1e73d3bcf Initial commit 2026-10-11 19:01:09 -05:00