Tenant onboarding and service-client grants

Audience: estate operators creating tenants or enabling service access for existing tenants.

Provisioning rule

Authority owns tenant membership. POST /console/admin/tenants applies the tenant-client grant before returning success. By default every client whose is_first_party flag is true receives the new tenant in its membership allowlist (the typed authority.client_tenants links and their properties.tenants mirror, updated together by the owner store). This includes policy-engine, so Policy can authenticate its tenant-scoped Scanner read without a manual client edit.

Authority startup applies the same rule to all enabled tenants after migrations and plugin client bootstrap. This covers seeded tenants, clients declared after tenant creation, and tenants created while auto-grants were disabled. Disabled tenants are excluded from the startup pass. Grants union existing memberships, normalize and sort slugs, and preserve scopes, audiences and secrets. Standard bootstrap preserves the union on subsequent restarts.

The target set has two configuration levels:

  1. When Authority:TenantClientGrants:AutoGrantClientIds exists, its explicit list is authoritative, including an empty list that opts out. This overrides the first-party flag.
  2. When that setting is absent, Authority resolves all first-party clients from its own client store. Clients outside that set receive no automatic membership.

Authority:TenantClientGrants:Enabled=false disables both admin-create grants and startup convergence. Re-enabling it lets the next startup reconcile existing enabled tenants.

{
  "Authority": {
    "TenantClientGrants": {
      "Enabled": true
      // Omit AutoGrantClientIds for the declarative first-party rule.
      // Supply an explicit list when the estate needs a narrower target set.
    }
  }
}

Declare clients with firstParty: true in the canonical Standard descriptor when they need this tenant access. The default is a stored flag, not a fixed list of names. Existing true flags survive Standard bootstrap through its union rule; setting a descriptor’s flag to false does not revoke an already-persisted true flag or its grants. Use the explicit target list to restrict future automatic grants. It does not revoke existing memberships. There is no per-tenant auto-grant opt-out.

Create and verify a tenant

Use the estate administration operator identity with ui.admin and authority:tenants.write. The ordinary product CLI does not carry tenant administration rights. The admin token must satisfy fresh authentication. Use the provider name from Authority’s GET /ready when multiple password-capable providers are registered.

# TOKEN is an estate-admin token obtained through the configured Authority flow.
# Keep it in the operator's environment; do not print it.
TENANT="my-new-tenant"
curl -sS -X POST "$STELLA/console/admin/tenants" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-StellaOps-TenantId: default" \
  -H "Content-Type: application/json" \
  -d "{\"id\":\"$TENANT\",\"displayName\":\"My New Tenant\"}"

A successful create returns 201. Verify the service identity through a tenant-selected token request using its configured credentials and the scope it needs. For Policy’s Scanner read this is client_id=policy-engine, grant_type=client_credentials, scope=scanner:read, and tenant=<new slug> on /connect/token. Supply the secret through the estate’s existing secret pointer. A successful token must carry the requested stellaops:tenant; an unknown or ungranted tenant must be refused.

The create audit event authority.admin.tenants.create records tenant.client_grants.outcome, .extended, .unchanged, .not_found, and .failures. Use the admin client read API to inspect membership. The static tenant list in a bootstrap descriptor is not the effective allowlist; the persisted union is authoritative.

Failed onboarding and recovery

If client enumeration throws or a grant cannot be persisted, create returns 503 with a tenant-client grant problem and a warning header. The tenant row may already exist. Authority records the failure in its audit trail. Repeating create for that slug returns a conflict and does not repair the grant. Restore the owning store/configuration condition, then let Authority startup reconcile the existing enabled tenant. Startup convergence fails instead of reporting ready with unresolved grant failures or missing explicitly configured clients.

An empty explicit target list and the global disable are deliberate opt-outs, not store failures. Neither proves service onboarding. A scoped client edit through PATCH /console/admin/clients/{clientId} remains an operator option under an explicit membership policy; it is not required under the default first-party rule.

Policy exposes evidence.baselineEnrollmentFailure. authentication_rejected means the Scanner owner read could not authenticate; scan_not_found means Scanner has no attempt for the image. Re-scanning does not repair a missing membership grant. See the gateway guide.

Authority owns the tenant catalog; other services replicate it into their own databases. A new tenant can take a replication poll to become available there. Authority does not write sibling databases during onboarding.

Suspension and tenant headers

Suspension preserves client membership; token issuance refuses the disabled tenant. Resuming retains membership. There is no tenant-delete admin endpoint. Startup convergence does not revoke grants.

A tenant-selected token binds the request tenant. With the gateway override off (the default), a conflicting tenant header is stripped and the token’s tenant is forwarded; with Gateway:Auth:EnableTenantOverride=true, a header naming a tenant outside the token’s stellaops:allowed_tenants is refused as tenant_override_forbidden. A global administrator’s X-StellaOps-TenantId selects the tenant in both modes (ADR-023). Mint for the tenant being called, or omit a redundant header where the gateway derives it from the token. Membership does not bypass tenant binding or resource scopes.

Source and verification

PCD-340 behavior is source-verified against 76be17ca49 (2026-10-05). Re-verify through tools/scripts/test-targeted-xunit.ps1 on Authority’s test project, selecting TenantClientGrantServiceTests and the real-host test below. Shared-estate acceptance waits for the recorded Authority/Policy release window.