Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/on-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
with:
examples: |
[
{ "example": "examples/authorizations/referenced.yaml", "api_path": "apis/authorizations" },
{ "example": "examples/authorizations/referenced.yaml", "api_path": "apis/authorizations", "observed_resources": "tests/test-relationships/observed/authorization.yaml" },
{ "example": "examples/administrators/referenced.yaml", "api_path": "apis/administrators" },
{ "example": "examples/administrators/referenced.yaml", "api_path": "apis/administrators", "observed_resources": "tests/test-relationships/observed/administrator.yaml" },
{ "example": "examples/authstacks/minimal.yaml", "api_path": "apis/authstacks" },
{ "example": "examples/authstacks/standard.yaml", "api_path": "apis/authstacks" },
{ "example": "examples/authstacks/with-smtp.yaml", "api_path": "apis/authstacks" },
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/on-push-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ jobs:
with:
examples: |
[
{ "example": "examples/authorizations/referenced.yaml", "api_path": "apis/authorizations" },
{ "example": "examples/authorizations/referenced.yaml", "api_path": "apis/authorizations", "observed_resources": "tests/test-relationships/observed/authorization.yaml" },
{ "example": "examples/administrators/referenced.yaml", "api_path": "apis/administrators" },
{ "example": "examples/administrators/referenced.yaml", "api_path": "apis/administrators", "observed_resources": "tests/test-relationships/observed/administrator.yaml" },
{ "example": "examples/authstacks/minimal.yaml", "api_path": "apis/authstacks" },
{ "example": "examples/authstacks/standard.yaml", "api_path": "apis/authstacks" },
{ "example": "examples/authstacks/with-smtp.yaml", "api_path": "apis/authstacks" },
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ node_modules/
apis/**/configuration.yaml
**/aws-creds
tests/**/secrets/

# Python render-test bytecode
__pycache__/
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ build:
# Format: example_path::observed_resources_path (observed_resources_path is optional)
# api_path is derived from example_path via the api-dir macro (examples/<x>/... → apis/<x>/).
EXAMPLES := \
examples/authorizations/referenced.yaml:: \
examples/authorizations/referenced.yaml::tests/test-relationships/observed/authorization.yaml \
examples/administrators/referenced.yaml:: \
examples/administrators/referenced.yaml::tests/test-relationships/observed/administrator.yaml \
examples/authstacks/minimal.yaml:: \
examples/authstacks/standard.yaml:: \
examples/authstacks/local-colima.yaml:: \
Expand Down Expand Up @@ -97,8 +101,13 @@ test:
test-security:
python3 -m unittest discover -s tests/security -v

e2e:
up test run $(E2E_TESTS) --e2e
e2e: e2e-relationships
@if [ -n "$(E2E_TESTS)" ]; then up test run $(E2E_TESTS) --e2e; fi

.PHONY: e2e-relationships
e2e-relationships:
@test -n "$(ZITADEL_E2E_CONTEXT)" -a -n "$(ZITADEL_E2E_API_URL)" -a -n "$(ZITADEL_E2E_CREDENTIALS_SECRET)" || (echo "Set ZITADEL_E2E_CONTEXT, ZITADEL_E2E_API_URL and ZITADEL_E2E_CREDENTIALS_SECRET"; exit 1)
python3 tests/live/test_relationships.py

publish:
@if [ -z "$(tag)" ]; then echo "Error: tag is not set. Usage: make publish tag=<version>"; exit 1; fi
Expand Down
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,76 @@ No localhost, plaintext transport or secret backend is defaulted by this API.
The package dependency now requires provider v0.3.0+, so upgrade that provider
before applying this configuration. Recreate only the observer when intentionally
replacing an installed instance; preserve Helm/database resources otherwise.

## Native identities and relationship XRs (release pending)

Use native `Organization`, `HumanUser`, `MachineUser`, `ProjectV2` and
`Application` resources for identities and registrations. The new
`Authorization` and `Administrator` XRs solve the provider's polymorphic
human/machine user reference limitation. They own one assignment and its
complete role set. They never create users, organizations, projects or shared
project grants. Application environments own those declarations and credential
exports; preview environments receive them through application promotion.

These additions depend on the [Crossplane provider PR70](https://github.com/crossplane-contrib/provider-upjet-zitadel/pull/70) and
[Terraform provider PR474](https://github.com/zitadel/terraform-provider-zitadel/pull/474).
**Provider v0.3.0 does not implement their native resource types.** This change
must remain a draft until the upstream provider is released, the dependency
constraint is advanced and full validation passes against that release.

Start with [Authorization](examples/authorizations/referenced.yaml) for application
role keys, or [Administrator](examples/administrators/referenced.yaml) for
administrative roles. Administrator requires exactly one explicit target:
`organizationRef`, `projectRef`, `projectGrantRef`, or `instance: true`. Instance
scope has no default. Project-grant targets resolve the project and grantee
organization from an environment-owned native Grant. Roles remain subject to
Zitadel's validation for the selected target scope.

References default to the XR namespace and accept native provider resources.
An explicit `namespace` requires `allowCrossNamespaceReferences: true`. This is
an opt-in declaration, not a security boundary: restrict XR and ProviderConfig
access using RBAC. All references and the ProviderConfig must address the same
Zitadel instance. For Authorization, `organizationRef` identifies the
organization where the authorization is granted; it is independent of the user's
home organization. A `projectGrantRef` can pin and protect an existing delegation;
its observed project and grantee organization must match the assignment.

An observed reference ID is sufficient to keep rendering during a temporary
Ready failure. A missing/deleting reference or changed remote identity retains
the existing native object and external ID, suspends Create/Update/LateInitialize,
and sets `ReferencesResolved=False` and `Ready=False`. Explicit Observe and Delete
permissions are preserved; intentional teardown can still remove the assignment
if Delete was enabled. Existing Usage locks remain during readiness loss. New
Usages wait for ready resources and protect referenced principals/targets until
the native assignment is removed. The owner of shared delegation is responsible
for protecting its own project/organization dependencies.

To adopt an existing assignment, set `spec.binding.externalName` and start with
`managementPolicies: [Observe]`. Authorization imports by its existing assignment
ID. Imported assignments automatically remain Observe-only until their observed
user/target identity matches the resolved references. A mismatch disables
delete as well, preserving the unrelated remote assignment. Administrator imports by `user:organization:org`, `user:project:project`,
`user:project_grant:project:grantee-org`, or `user:instance` (percent-encode colons
in IDs). `binding.resourceName` controls the stable native Kubernetes name.
Preserve the existing remote assignment while removing its previous declarative
owner without Delete; confirm the new observer reports the same identity and
roles before enabling writes. Never let two owners reconcile one complete role
set simultaneously. References and provider selection are immutable; identity
replacement requires a separate XR/adoption step.

The old HumanUser, MachineUser, Grant and OIDCClient XRDs remain packaged for
compatibility. Migrate existing Grant instances by importing their native
user-grant IDs into Authorization, with environment-owned delegation declared
separately. Migrate OIDCClient registrations to native client
resources using their current application IDs and Secret contracts. Do not
remove these XRDs or the terminating preview objects as part of introducing the
new APIs. PAT v2 and the Actions v2 groups-claim webhook migration are separate
provider/runtime work.

The relationship regression suite is included in `make test-security`. It checks
both subject kinds, all administrator targets, cross-namespace references,
missing/replaced references, current readiness, import and deletion locks. Local
render tests change no cluster resources. The opt-in [live relationship suite](tests/live/README.md)
(`make e2e-relationships`) exercises the running compositions/controllers and
checks Zitadel APIs after lifecycle/import/deletion operations. Live verification
against a containing provider/configuration release remains a release prerequisite.
46 changes: 46 additions & 0 deletions apis/administrators/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: administrators.auth.hops.ops.com.ai
spec:
compositeTypeRef:
apiVersion: auth.hops.ops.com.ai/v1alpha1
kind: Administrator
mode: Pipeline
pipeline:
- step: resolve-resource-references
functionRef:
name: crossplane-contrib-function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: "{{- $xr := getCompositeResource . }}\n{{- $spec := $xr.spec }}\n{{- $target := $spec }}\n{{- if eq $xr.kind\
\ \"Administrator\" }}{{- $target = $spec.target }}{{- end }}\n{{- $refs := dict \"subject\" (mergeOverwrite (dict\
\ \"kind\" \"HumanUser\" \"apiVersion\" \"user.zitadel.m.crossplane.io/v1alpha1\") (deepCopy $spec.subjectRef))\
\ }}\n{{- range $key, $defaults := dict\n \"project\" (dict \"kind\" \"ProjectV2\" \"apiVersion\" \"project.zitadel.m.crossplane.io/v1alpha1\"\
)\n \"organization\" (dict \"kind\" \"Organization\" \"apiVersion\" \"org.zitadel.m.crossplane.io/v1alpha1\")\n\
\ \"projectGrant\" (dict \"kind\" \"Grant\" \"apiVersion\" \"project.zitadel.m.crossplane.io/v1alpha1\") }}\n \
\ {{- $ref := get $target (printf \"%sRef\" $key) | default dict }}\n {{- if $ref.name }}{{- $_ := set $refs $key\
\ (mergeOverwrite $defaults (deepCopy $ref)) }}{{- end }}\n{{- end }}\napiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1\n\
kind: ExtraResources\nrequirements:\n {{- range $key, $ref := $refs }}\n {{ $key }}:\n apiVersion: {{ $ref.apiVersion\
\ | quote }}\n kind: {{ $ref.kind | quote }}\n matchName: {{ $ref.name | quote }}\n namespace: {{ $ref.namespace\
\ | default $xr.metadata.namespace | quote }}\n {{- end }}\n"
- step: relationship
functionRef:
name: hops-ops-auth-stackrelationship
- step: auto-ready
functionRef:
name: crossplane-contrib-function-auto-ready
- step: relationship-readiness
functionRef:
name: crossplane-contrib-function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: "{{- $xr := getCompositeResource . }}\n{{- $desiredStatus := .desired.composite.resource.status | default\
\ dict }}\napiVersion: {{ $xr.apiVersion }}\nkind: {{ $xr.kind }}\nmetadata:\n annotations:\n gotemplating.fn.crossplane.io/ready:\
\ {{ ternary \"True\" \"False\" ($desiredStatus.ready | default false) | quote }}\n"
Loading
Loading