Skip to content

feat: add authorization and administrator relationship XRs - #34

Draft
patrickleet wants to merge 3 commits into
mainfrom
feat/v2-authorization-administrator
Draft

patrickleet wants to merge 3 commits into
mainfrom
feat/v2-authorization-administrator

Conversation

@patrickleet

@patrickleet patrickleet commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Zitadel role assignments and administrator memberships accept either a human user ID or a machine user ID. A native Crossplane reference targets one resource type, so it cannot express that choice. Consumers otherwise need to resolve remote user IDs themselves or repeat composition logic for each subject and target type.

The existing generic identity/client XRs do not provide this focused relationship API. The desired boundary is an assignment that references independently managed users, organizations, projects, and shared delegation.

Solution

Add two namespaced relationship XRs backed by native stable v2 assignments:

  • Authorization resolves a typed HumanUser or MachineUser, project, and explicit granting organization into one native authorization. It can reference an existing project grant and checks that its project and grantee match. It never creates shared delegation per user.
  • Administrator resolves the same subject choice into one native administrator assignment. Exactly one target is required: organization, project, project grant, or explicit instance scope.

Both share reference-resolution and rendering logic. Users, organizations, projects, clients, delegation, and credential exports retain their own owners.

Here “v2” identifies the underlying Zitadel Authorization and InternalPermission services; the XR Kubernetes API remains v1alpha1. The relationship-XR design addresses polymorphic references independently of the backend API migration. Existing AuthStack XRs are retained abstractions; their backend API versions depend on the managed resources they compose. Zitadel still supports its v1 API family while directing new development to resource-based v2 services; retaining an existing XR or using a v1-backed resource does not itself establish deprecation. See the Zitadel API overview.

Before / after

With the native provider alone, a consumer must resolve the human or machine resource into userId before declaring an assignment:

apiVersion: authorization.zitadel.m.crossplane.io/v1alpha1
kind: Authorization
metadata:
  name: reader
  namespace: app
spec:
  providerConfigRef:
    name: zitadel
    kind: ProviderConfig
  forProvider:
    userId: "resolved-remote-user-id"
    projectIdRef:
      name: product
    orgIdRef:
      name: app-org
    roleKeys:
    - reader

With the new XR, the manifest names the subject and the composition resolves its observed ID:

apiVersion: auth.hops.ops.com.ai/v1alpha1
kind: Authorization
metadata:
  name: reader
  namespace: app
spec:
  subjectRef:
    name: alice
    kind: HumanUser
  projectRef:
    name: product
  organizationRef:
    name: app-org
  roles:
  - reader
  providerConfigRef:
    name: zitadel
    kind: ProviderConfig

For a service account, change the subject to kind: MachineUser and its resource name. For administration, use kind: Administrator and replace the project/organization fields with an explicit target, for example:

# Fields inside Administrator.spec, alongside subjectRef/providerConfigRef:
target:
  organizationRef:
    name: app-org
roles:
- ORG_USER_MANAGER

The native example requires the provider changes linked below; these native types are not available in published provider v0.3.0. In both forms, the assignment owns the complete role set, so omitted roles are revoked.

Backward compatibility and adoption

  • Existing HumanUser, MachineUser, Grant, and OIDCClient XRDs remain packaged. This is additive and does not automatically migrate existing consumers or remove their resources.
  • References default to the XR namespace. Cross-namespace resolution requires explicit opt-in; RBAC remains the security boundary. Usage of.resourceRef.namespace identifies the protected source; by is local to the Usage namespace and omits the unsupported namespace field.
  • Subject, target, provider, and naming/import contracts are immutable. Pass-through fields cannot override resolved identities or inject selectors that redirect the assignment.
  • Missing/deleting references or changed remote IDs retain an established binding and suspend create/update instead of removing it from desired state. Readiness reports the unresolved relationship, and established Usage protections are retained.
  • Stable native names and external-name imports support deliberate adoption. Imports remain Observe-only until observed identities match. A mismatched imported identity also disables deletion; a previously verified assignment can retain configured deletion permission during temporary reference loss.
  • Migration must transfer ownership without deleting the remote assignment. These guards and render tests do not replace live adoption verification.

Validation

Passed locally:

  • Configuration package build and all 20 example/observed-resource render cases. The same 20 cases validate with no missing schemas against YAML-exported CRDs from the source-upgraded local cluster, including the new provider and Usage schemas.
  • Ten direct Crossplane-render regression tests, including subcases for both user kinds, all four administrator scopes, namespace opt-in, missing/unready/replaced references, current native observations, deletion locks, imports, mismatched imported identities, and shared-delegation matching.
  • Schema validation of both rendered XRs, both native assignments, and five deletion-protection Usages: nine successes, no missing schemas or validation failures.
  • Initial unresolved-reference rendering through up composition render, with Ready=False.

Live tests installed both provider and configuration from source on a local Kubernetes cluster running Zitadel v4.13.1. Both subject kinds and all four administrator scopes reached Ready. Checks passed for full role replacement, server-side drift correction, cross-org assignment through an existing shared delegation, cross-namespace Observe imports with working Usage locks, and mismatched imports retaining Observe-only policies without Delete. API reads confirmed removal of the test authorizations and administrator assignments during teardown.

Nine existing grants were first observed through the new XR with exact original IDs and verified user/target identities. A subsequent live cutover transferred all nine assignments to Authorization XRs: old controllers processed Observe-only policies, their Kubernetes handles were removed without Delete, and only then were writes enabled on the new owners. All nine retained remote IDs and full role sets, with 27 dependency Usages and no remaining old Grant handles. Existing users, project settings, OIDC registrations and credential-secret fingerprints were preserved. Headless sign-in, callback and authenticated-dashboard checks passed in three active local environments. The original auth installation remained Ready and the local GitOps watcher was restored. Temporary test namespaces and credentials from the earlier isolated tests were removed; the nine adopted application bindings remain running.

CI run 39474264661 passes 35/35 KCL cases, including both new relationship cases, plus instance-observation and the existing e2e job. The two retained-native example validations still fail against published provider v0.3.0, which lacks these resource schemas. The local Dory-based KCL Docker harness timed out; the direct-render regression suite and installed-cluster checks passed. Live write-ownership transfer is verified for the nine existing role assignments. OIDC registration adoption and upgrades using published containing provider/configuration releases remain separate.

Dependencies and release status

Depends on Terraform provider PR474 and Crossplane provider PR70. This remains a draft: the package dependency must identify a provider build that actually contains these native types before release. An interim downstream build can use an explicitly pinned fork package; an upstream release can replace that dependency when available. Neither provider v0.3.0 nor passing tests for the existing XRs satisfies that requirement.

Reproducible live relationship tests

Added and ran a real relationship e2e suite: test source, fixture/cleanup helpers, run instructions. make e2e now includes e2e-relationships rather than succeeding locally without a real relationship suite.

ZITADEL_E2E_CONTEXT=<test-context> \
ZITADEL_E2E_API_URL=<test-instance-or-loopback-forward> \
ZITADEL_E2E_CREDENTIALS_SECRET=<namespace>/<provider-secret> \
make e2e

PASS: test_administrators
PASS: test_authorizations
Ran 2 tests in 391.777s — OK

The run exercised XR -> composition functions -> source-built native provider -> Zitadel v4.13.1. It covers:

  • Human, machine and delegated Authorizations; complete role replacement retaining binding IDs, Observe-only imports, remote identity/role assertions and remote deletion.
  • All four Administrator targets; role replacement retaining identity, imports, direct API assertions and remote deletion.
  • A mismatched-subject import that stays unready, Observe-only and unable to delete the existing assignment.
  • Explicit cross-namespace Observe imports, correct source namespaces in Usage.of, local Usage.by references, and dependency locks for subjects/organizations/projects/shared delegation.

All remote assertions call the real Authorization/InternalPermission v2 APIs independently of Ready status. The suite creates disposable organizations, users, project, roles and one shared delegation. Cleanup waits for composed native bindings and Usages to disappear before deleting protected parents; all fixture namespaces were removed.

The initial attempts are not counted as passing: one stopped during fixture creation (fixtures now use unique usernames/emails across runs); another passed binding assertions but failed teardown when an XR disappeared before its Usage garbage collection completed. The final run above passes both lifecycle assertions and complete cleanup. No deletion protection was bypassed.

Separately, direct HTTPS API reads with CA verification matched nine existing authorization ID/user/project/organization/role tuples and an organization-scoped Administrator with ORG_USER_MANAGER. The adopted assignments and requested running example remain installed; test fixtures were removed.

CI distinction: the existing reusable Upbound e2e job had no KCL E2ETest definitions for these bindings. Its prior green result was not evidence of live coverage. This new standard-library Python suite runs against an explicitly prepared source/containing-release test cluster and is not automatically run by that reusable job. The Make target fails without explicit context/endpoint/credential-Secret opt-in; direct unittest discovery reports a skip. Existing render/security/KCL tests remain separate. This suite does not provision or validate every AuthStack Helm/database/routing variation, and containing released provider/configuration dependencies remain required before release.

Implements [[tasks/harmony-2506]]. Preserve existing APIs and remote identities while native provider support is reviewed.
@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@patrickleet

Copy link
Copy Markdown
Contributor Author

Added and ran a real relationship e2e suite: test source, fixture/cleanup helpers, run instructions. make e2e now includes e2e-relationships rather than succeeding locally without a real relationship suite.

ZITADEL_E2E_CONTEXT=<test-context> \
ZITADEL_E2E_API_URL=<test-instance-or-loopback-forward> \
ZITADEL_E2E_CREDENTIALS_SECRET=<namespace>/<provider-secret> \
make e2e

PASS: test_administrators
PASS: test_authorizations
Ran 2 tests in 391.777s — OK

The run exercised XR -> composition functions -> source-built native provider -> Zitadel v4.13.1. It covers:

  • Human, machine and delegated Authorizations; complete role replacement retaining binding IDs, Observe-only imports, remote identity/role assertions and remote deletion.
  • All four Administrator targets; role replacement retaining identity, imports, direct API assertions and remote deletion.
  • A mismatched-subject import that stays unready, Observe-only and unable to delete the existing assignment.
  • Explicit cross-namespace Observe imports, correct source namespaces in Usage.of, local Usage.by references, and dependency locks for subjects/organizations/projects/shared delegation.

All remote assertions call the real Authorization/InternalPermission v2 APIs independently of Ready status. The suite creates disposable organizations, users, project, roles and one shared delegation. Cleanup waits for composed native bindings and Usages to disappear before deleting protected parents; all fixture namespaces were removed.

The initial attempts are not counted as passing: one stopped during fixture creation (fixtures now use unique usernames/emails across runs); another passed binding assertions but failed teardown when an XR disappeared before its Usage garbage collection completed. The final run above passes both lifecycle assertions and complete cleanup. No deletion protection was bypassed.

Separately, direct HTTPS API reads with CA verification matched nine existing authorization ID/user/project/organization/role tuples and an organization-scoped Administrator with ORG_USER_MANAGER. The adopted assignments and requested running example remain installed; test fixtures were removed.

CI distinction: the existing reusable Upbound e2e job had no KCL E2ETest definitions for these bindings. Its prior green result was not evidence of live coverage. This new standard-library Python suite runs against an explicitly prepared source/containing-release test cluster and is not automatically run by that reusable job. The Make target fails without explicit context/endpoint/credential-Secret opt-in; direct unittest discovery reports a skip. Existing render/security/KCL tests remain separate. This suite does not provision or validate every AuthStack Helm/database/routing variation, and containing released provider/configuration dependencies remain required before release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant