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

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

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

8.4 KiB

AGENTS.md / CLAUDE.md

Shared guidance for agents working in this repository. Keep AGENTS.md and CLAUDE.md in sync.

Project Overview

QBit Authentik: a QQQ QBit to manage Authentik users, groups, memberships and group-granted QQQ permissions from a QQQ app. Authentik's database is the source of truth; the qbit reads live and writes through the Authentik API (never its DB). Design: docs/DESIGN.md (approved spec, QRun-IO/qqq#1065). Current line: 0.1 = the full user lifecycle and group/permission management (phases 0-3), read-only by default.

Build Commands

./mvnw compile          # always use the wrapper: Maven 3.10 cannot read the parent POM
./mvnw test             # unit tests, offline, on recorded fixtures (src/test/resources/fixtures/2026.8.3)
./mvnw test -Dtest=ClassName
./mvnw verify -DskipITs # compile, unit tests and JaCoCo; no contract ITs
CI=true ./mvnw verify   # + contract ITs: Authentik 2026.8.3 server+worker, Postgres, Mailpit (Testcontainers), JaCoCo gate
./mvnw verify -Dit.test=QbitReadContractIT -Dtest=none -Dsurefire.failIfNoSpecifiedTests=false
./mvnw verify -Dauthentik.fixtures.record=true              # refresh unit fixtures from the contract ITs
./mvnw test -Dtest=SchemaDriftTest -Dauthentik.schema.file=/path/to/schema.yml   # drift vs another release
  • Contract ITs (*IT, package contract) share one AuthentikTestStack per JVM; first boot takes ~1.5-3 minutes. @ExtendWith(RequiresDockerCondition.class): skipped without Docker locally, failed with CI=true.
  • The stack boots with src/test/resources/blueprints/*.yaml already in /blueprints/custom: that IS contract test (h) (bootstrap on an empty instance).
  • TestApi uses HTTP/1.1 on purpose: Java's HttpClient tries an h2c upgrade on plain-http POSTs, which Authentik answers with "400 Invalid HTTP request received."
  • Tests that grant extra permissions to the service-account role must remove them again (they are additive and shared).

Architecture

  • AuthentikQBitProducer validates the config, registers an AuthentikRuntime for the API backend, then runs the component producers in metadata/.
  • AuthentikRuntime (registry by API backend name): config, TokenBucketThrottle (10 rps, burst 20 by default), AuthentikGuard, AuthentikFilterMapper, response cache, and the table-name to AuthentikTableSpec map.
  • client/AuthentikActionUtil extends BaseAPIActionUtil is the only code that talks to Authentik.
    • doQuery/doCount/doGet use page/page_size paging (max 100). Key-only filters become per-id GETs. The user scope and type filters are applied server-side and re-checked per record.
    • The generic table writes (doInsert/doUpdate/doDelete) are refused. Changes use write(WriteAuthorization, ...) or writeAs(bearer, WriteAuthorization, ...), and only AuthentikGuard.authorize(AdminAction) mints a WriteAuthorization (an architecture test pins callers to AuthentikAdmin and its AuthentikTokens delegate).
    • execute() is the single request loop: throttle, bearer header, 429/5xx retry, error mapping, logging.
    • send() is the test seam (FixtureActionUtil, QbitReadContractIT.RecordingActionUtil).
  • client/AuthentikTables maps tables to the pinned schema (users, groups, sessions, events), plus the virtual groupMember table, which AuthentikActionUtil.memberships() serves from a group, a user, or a bounded sweep. client/AuthentikApiSurface lists every operation and parameter used. SchemaDriftTest and SchemaDriftIT check that surface.
  • client/AuthentikFilterMapper maps a QQQ filter to Authentik params, or rejects it with a QUserFacingException. It never filters client-side.
  • security/AuthentikGuard handles read permission checks, write authorization, the user scope, and protected-account detection. security/IdentityPermission is the authentik.* catalog (session form <name>.hasAccess).
  • admin/AuthentikAdmin plans and performs every change (AdminAction, AdminRequest, AdminPlan): the guardrails of DESIGN.md 5.4 run against LIVE Authentik data and the operator's LIVE groups (never the session claim) at plan time and again at execute time; the audit row (AdminActionAuditor, table authentik_admin_action) is written BEFORE the call and the call is not made if that fails; then the change, then an Authentik custom_qqq_admin_action event. AuthentikSafety holds the pure rules (subset rule helpers, last-admin, fingerprints). Granting authentik.* queues an authentik_approval_request for a second person (approve/reject).
  • admin/AuthentikTokens implements DESIGN section 13: personal calls use an action-scoped exchanged bearer; service-account calls use the qbit bearer. One-time keys never enter records, process values, audit or event context. TokenKeyDelivery refuses key-producing actions when core has no response-only secret channel.
  • processes/AuthentikProcesses: every action is a standard streamed-ETL table or record action (details screen, Review = dry run, Result), never in the nav. The ETL defaults to full validation, so the Review screen already holds the dry-run lines. The preview step's record count is NOT carried to later steps (and a client count needs setDistinctCount), so steps count targets themselves.
  • app/: AuthentikHomeData/AuthentikHomeView (the single home widget, HTML with no tables) and AuthentikRelations (joins, child-list widgets and sections so sub-tables appear on their parents, not in the nav).
  • permissions/GroupGraph + EffectivePermissionResolver (pure) turn groups and their ancestors into QQQ permissions. The util caches the graph for groupCacheTtlSeconds and uses it for derived fields.
  • drift/AuthentikDriftReport holds the drift checks, using only the service account's read permissions. It is rendered by metadata/AuthentikDriftReportWidgetRenderer, a standard TABLE widget gated by authentik.admin.

Invariants (do not break)

  • Every AuthentikActionUtil entry point calls the guard first (in addition to core action permission enforcement, retain Authentik-specific rules and protection for direct util calls).
  • Never log query strings (search terms are PII), the token, successful GET bodies, or bodies of sensitive endpoints; run anything stored through AuthentikRedactor.
  • Never filter client-side; reject what Authentik cannot evaluate.
  • Multi-value params are repeated plain names (type=a&type=b). Authentik silently ignores type[]=.
  • The user detail GET takes no include_* params. Groups have no last_updated in 2026.8.
  • check_access?for_user= is useless for the service account: it is silently evaluated for the caller. Never use it for other users.
  • Only AuthentikAdmin and its AuthentikTokens delegate may call AuthentikActionUtil.write / writeAs. Every write needs a reason, passes the guard, and is audited first.
  • recovery_email needs the brand to have a recovery flow ("No recovery flow set." otherwise), the role's add_user (DRF maps POST to add) plus reset_user_password, and view_emailstage on the stage.
  • The service account must never be a superuser and never hold enable_group_superuser or impersonate permissions; never token-key READ (view_token_key); add_token and set_token_key only for service-account creation and rotation (add_token cannot mint for others); delete_user/delete_group only when the host turns the DELETE feature on.

Code Style

3-space indentation, braces on the next line, return (x);, flower-box Javadoc on every class and method, fluent withX() setters, wrapper types in config.

Dependencies

Java 21. Parent qbit-build-parent:2.1.0. The default-active qqq-snapshot profile imports qqq-bom-pom:4.1.0-RC.21 (ADR-0007, as in qbit-sql-workbench). The project uses qqq-backend-core and qqq-backend-module-api. Test dependencies: JUnit 5, AssertJ, Mockito, Testcontainers 1.21.4, and Jackson YAML (from qqq) for the schema tests.

Release

Gitea is primary. .gitea/workflows/qrun-release.yaml delegates to the shared qrun-release-v1 lifecycle; .qqq-ci.yml identifies develop as the default feature branch and enables snapshot, RC and release publication. Do not infer feature completeness from main, which still contains the older baseline.

Local review

Run ./scripts/Start-Review.ps1 on Windows with Java 21 and Docker Desktop. The Next UI is at http://localhost:18300/app/authentikAdmin; startup URLs for Authentik and Mailpit are in target/review-server.log. All application ports bind to loopback. See docs/LOCAL-REVIEW.md for scope, reset and limitations.