Skip to main content

AxonFlow v9.12.0 Release Notes

v9.12.0 makes the five-role RBAC model real. The owner, admin, policy_admin, developer, and viewer roles each now do something distinct and enforced — role names that used to be labels now gate real behavior. Around that core, this release adds an owner bootstrap and lifecycle (assign, revoke, last-owner protection), makes response-PII visibility permission-driven instead of role-driven, adds a first-class JumpCloud SSO/OIDC provider preset, and surfaces a tokens column on the audit-export CSV.

This is an Enterprise release for the RBAC, owner-lifecycle, PII, and JumpCloud work; the audit-export CSV column ships in the community mirror too. It carries database migrations and several deliberate behavior changes — read Behavior changes and Upgrading before you upgrade, because some sessions that could act yesterday get a 403 today, by design.

Behavior changes

These changes can alter what your deployment allows, denies, or redacts, or who can perform an action. Read them before upgrading.

1. admin no longer configures SSO, SCIM, or detection posture (Enterprise)

sso:configure is now owner-reserved, and the * wildcard no longer satisfies it. A session or token holding only admin (the wildcard) is now denied on the SSO-configuration, SCIM token and group-to-role-mapping, and detection-posture routes — these require the owner role. This is the mechanism that makes owner genuinely different from admin rather than a synonym for it. A user assigned both admin and owner keeps the capability. Your existing admins are carried across as owners by the upgrade backfill, so the capability is preserved through a real role rather than through the wildcard — but admins granted after the upgrade do not inherit it.

2. Policy editing is now RBAC-gated across every policy route (Enterprise)

Creating, updating, importing, toggling, overriding, or deleting a governance policy now requires policy:write (and policy:delete for deletes), enforced at the route layer across every policy family the portal exposes: the primary policies API, unified policies, static policies, policy overrides, and dynamic policies. Before this, any authenticated portal session — including a viewer — could edit policies, including flipping a blocking policy to allow, through the exact routes the UI calls.

A session without policy:write — a viewer, a developer, or a role-less operator — can no longer create, edit, or delete policies. admin, owner, and policy_admin can. Policy reads (list, get, effective, versions, export, and the dry-run evaluation endpoints) stay session-only so the policy screens still load for a read-only user. A route-coverage test derives the write-route list from the source on every build and fails CI if a new mutating policy route ships ungated, so a family cannot silently escape the gate again.

3. policy_admin per-user tokens now read tenant-wide (Enterprise)

Previously a policy_admin fleet token collapsed to own-rows on the read plane. It now reads the full tenant audit and decision trail, matching admin and owner — a genuine policy administrator can review what the policies it manages actually did. Review who holds policy_admin before upgrading if that visibility expansion is not what you want.

4. The member role is gone (Enterprise)

The sixth role, member, mapped to no distinct behavior and is removed. Minting a token with role: "member" now returns 400, and the fleet validator and SCIM resolver no longer recognize it. Existing member tokens and assignments normalize to least privilege (own-rows) — the scope they already had, so there is no runtime change for anyone holding one. The seeded member system role is pruned where nothing references it; a member role still referenced by an assignment or a SCIM group mapping is left in place. Audit your member assignments and move those users to one of the five roles.

5. Response PII visibility is permission-driven, never role-driven (Enterprise)

No role — including admin and owner — now sees unredacted PII in an LLM response by virtue of its name. Earlier code special-cased a caller whose role was literally admin and handed it the response completely unredacted; that role literal is removed. Allowance now derives only from explicit PII permissions (view_full_pii, view_basic_pii, view_financial, view_medical), and view_full_pii is deliberately held by no default role.

This was a latent gap, not a live leak: the role literal sat on a legacy fallback branch that runs only when the shared policy engine is absent, and a database-backed deployment installs that engine and never takes the branch — so its responses were already redacted regardless of role. On a deployment running the legacy fallback, an admin-role caller that relied on receiving unredacted responses now receives redacted ones until view_full_pii is granted. This is intentional and is the compliant default. See Permission-driven PII for the current limitation on where a portal grant takes effect.

The five-role RBAC model

Every role now has distinct, enforced behavior, and three role vocabularies plus the enforced-permission set are pinned in lockstep so they cannot silently drift again.

RoleReads (fleet plane)Portal capability
ownerfull tenanteverything admin can, plus SSO / SCIM / detection-posture configuration (sso:configure)
adminfull tenantfull portal operations except owner-reserved configuration: manage roles and user assignments, edit policies, query deployment-wide usage
policy_adminfull tenantcreate, edit, and delete policies; tenant-wide audit reads — but no role/user administration
developerown rowsrotate its own per-user token; own-rows portal console
viewerown rowsread-only, but tenant-wide portal audit console

Key relationships:

  • owner is a true superset of admin. Owner's bundle is the * wildcard plus the owner-reserved sso:configure, so owner is never below admin on any permission.
  • adminpolicy_admin. Role and user management is admin-only (via the wildcard); a policy_admin administers policies but receives 403 on every role- and user-management route.
  • developer and viewer do not rank against each other. On the fleet plane both read own-rows. On the portal plane each holds one permission the other lacks — developer can rotate its own token but has no tenant-wide audit read; viewer has the tenant-wide audit console but cannot rotate a token.
  • Anti-escalation. Creating, updating, or assigning a role that would confer an owner-reserved permission requires the actor to already hold that permission — so an admin cannot mint or self-assign owner to acquire sso:configure (returns 403).

A self-serve endpoint lets a developer rotate its own per-user token (always minted developer-scoped, never an escalation vector); a viewer lacks the permission and is denied. The full model, including the per-permission gate table and the cross-plane pitfalls, is documented in the Roles & Permissions reference.

Owner bootstrap and lifecycle

Making sso:configure owner-reserved only works if an owner exists. This release ensures one does, and gives ownership a real lifecycle.

  • On upgrade — automatic backfill. Every principal that effectively held sso:configure before the upgrade (a live assignment to a role carrying the wildcard or sso:configure — in practice your existing admins) is granted the owner role. Capability-preserving, never widening: roles that never carried sso:configure get nothing, expired assignments are skipped, and a time-limited admin's owner grant carries the same expiry.
  • New organizations and default self-hosted deployments. The organization-creation path seeds the owner assignment, and in enterprise / in-VPC modes the portal grants the deployment operator owner at every boot.
  • Break-glass. An admin API assigns an organization's first owner from outside the RBAC plane. It requires an authenticated admin key in every deployment mode — it fails closed with 401 even where admin auth is otherwise optional — is idempotent, and audits every attempt including refusals.
  • Revoke. Ownership is no longer append-only. A revoke verb removes an owner grant, audited on every branch including refusals. It is guarded by last-owner protection: the last owner can never be removed (409), with no force flag, and the check-and-delete run in one transaction so two concurrent revokes of two different owners cannot each conclude "not the last one" and leave the organization with zero. A time-boxed co-owner does not count as a survivor. The same protection guards every path that can remove or de-power an owner — the role-assignment API, SCIM group-mapping changes, role deletion cascades, and stripping owner-reserved permissions from a role.
  • A contact-email change moves ownership. The organization's contact email is the identity its login presents, so a genuine change to it now moves the owner grant — granting the new address and revoking the superseded one — through the authenticated, audited admin handler that performs the change, rather than a database trigger that would confer the top role to any writer of that column with no audit trail.

The owner grant requires a real identity

An organization with no contact email used to present an empty login identity, and the bootstrap owner grant was keyed on it — which made any session resolving to an empty identity (most plausibly SSO/OIDC with a missing or unmapped email claim) inherit owner, including the owner-reserved sso:configure. The platform now refuses to grant owner to a non-identity — the empty string and reserved synthetic identities — on every first-owner path. An organization with no contact email presents AXONFLOW_PORTAL_ADMIN_EMAIL (default [email protected]); set it to your operator's real address before upgrading so the audit trail names a person. Any empty-keyed owner grant a previous release created is deleted on upgrade and re-created on the real identity at portal boot — no action required, and no lockout.

Permission-driven PII

Response PII redaction is now permission-driven. What a caller may see is determined solely by the PII permissions on its role:

PermissionUnredacts
view_full_piieverything: SSN, credit card, bank account, email, phone, address
view_basic_piiemail, phone
view_financialcredit card, bank account
view_medicalmedical record, diagnosis

view_full_pii is deliberately held by no default role and is excluded from *-wildcard expansion, so out of the box every role receives redacted responses; you grant it deliberately, and the grant is visible and auditable in the role's permission set.

A portal view_full_pii grant does not yet reach the response plane

The response plane reads a caller's permissions off the request, and nothing yet resolves a portal role assignment into that request. Granting view_full_pii on a role in the portal therefore does not currently un-redact anything on that plane — the visibility claim there is caller-asserted. This release makes the grant explicit and role-agnostic and removes the blanket allow-all; server-side resolution of the response plane's permissions is separate, later work. Do not treat a portal view_full_pii grant as a working control over response PII until that lands.

First-class JumpCloud SSO

jumpcloud is now a supported SSO provider preset alongside Okta, Azure AD, Auth0, generic SAML, and generic OIDC. JumpCloud is a standard OIDC identity provider: the preset derives its protocol family (oidc) server-side — never client-set — pre-fills JumpCloud's well-known OIDC endpoints (issuer https://oauth.id.jumpcloud.com/ and its JWKS URI) and a default email claim mapping, and validates the same issuer/audience/JWKS inputs as a generic OIDC integration. It powers per-user fleet identity: developers authenticate with a JumpCloud-issued OIDC token that AxonFlow validates against JumpCloud's JWKS, and roles resolve from the SCIM-synced directory, not from token claims. See the JumpCloud SSO + SCIM setup guide and SSO Configuration.

tokens column on the audit-export CSV

The CSV audit export (POST /api/v1/audit/export?format=csv) now includes a tokens column, sourced from each row's token usage, positioned between response_time_ms and correlation_id. The JSON export already carried token usage, so it is unchanged. This ships in the community mirror.

Known limitation

On RDS-style managed-Postgres deployments where the migration role is a delegated superuser (such as rds_superuser) rather than a true superuser or a BYPASSRLS role, the organizations table's FORCE row-level security applies to the migration role too — which applies even to the table owner. A migration role in that position sees zero organizations, so the upgrade's owner-assignment backfill can enumerate nothing and its orphan-organization report is blind. In v9.12.0 the migration warns rather than silently no-op'ing or aborting, but the practical consequence is that a legacy organization can finish the upgrade with no owner assigned.

Symptoms and recovery:

  • An organization that upgraded without a backfilled owner cannot configure SSO/SCIM/detection posture (no owner → no sso:configure), and an admin cannot self-assign owner (anti-escalation).
  • To find affected organizations, run the detection query as a superuser or BYPASSRLS role (the migration role's own view is subject to the same FORCE RLS and will under-report).
  • Recover with the break-glass owner endpoint, which assigns an organization's first owner from outside the RBAC plane — see Owner bootstrap and lifecycle.

This is fixed in v9.12.1, which repairs the enumerations so the backfill and orphan report see every organization under a FORCE-RLS migration role.

Migrations

All migrations are idempotent, ship down migrations, and none rewrites a large table — no maintenance window is required.

MigrationWhat it does
core/148Reconciles the system-role definitions to the canonical five-tier spec for every organization and prunes the dropped member role where unreferenced
core/149Backfills the owner assignment for principals that effectively held sso:configure before the upgrade, plus the organization bootstrap grant
core/150Requires a real identity for the owner grant — refuses a blank or synthetic target and deletes any empty-keyed owner grant a prior run created
core/151Backfills policy_admin for zero-assignment organizations so their login identity retains policy editing under the new gate
core/152Drops the contact-email owner-drift trigger; the owner re-seed moves into the authenticated, audited admin handler

Upgrading

Apply the migrations above as part of a normal upgrade.

# Community
docker compose pull && docker compose up -d

# Enterprise
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml pull
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml up -d

Before upgrading, act on the behavior changes:

  1. Assign owner to whoever should configure identity. Existing admins are carried across automatically; admins granted after the upgrade are not. See admin ≠ owner.
  2. Grant policy_admin (or admin/owner) to anyone who authors policy. viewer, developer, and role-less operators lose policy-write access. On multi-organization deployments, an organization whose only login is a role-less operator has no in-product recovery — use break-glass. See policy gating.
  3. Review who holds policy_admin if the tenant-wide read expansion is not what you want. See policy_admin scope.
  4. Audit your member assignments and move those users to one of the five roles. See member dropped.
  5. Set AXONFLOW_PORTAL_ADMIN_EMAIL to your operator's real address before upgrading if your organization has no contact email, so the audit trail names a person. See The owner grant requires a real identity.
  6. Prune stale owner grants. An organization that changed its contact email in the past may still have owner on the old address. Audit owner assignments for addresses you no longer recognize and remove them through the roles API.
  7. Grant view_full_pii to the role that should hold it if you relied on admins receiving unredacted responses on a legacy-fallback deployment. See permission-driven PII.

Single-admin and Community-mode deployments are unaffected by the RBAC and owner-lifecycle changes (no fleet of distinct users or owner plane to manage). If a legacy organization on a managed-Postgres deployment finishes the upgrade with no owner, see the Known limitation.