Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/on-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
{ "example": "examples/machineusers/minimal.yaml", "api_path": "apis/machineusers" },
{ "example": "examples/machineusers/with-pat.yaml", "api_path": "apis/machineusers" },
{ "example": "examples/machineusers/with-pat-push.yaml","api_path": "apis/machineusers" },
{ "example": "examples/humanusers/explicit-org.yaml", "api_path": "apis/humanusers" },
{ "example": "examples/humanusers/with-org-ref.yaml", "api_path": "apis/humanusers" },
{ "example": "examples/grants/same-org.yaml", "api_path": "apis/grants" },
{ "example": "examples/grants/cross-org.yaml", "api_path": "apis/grants" }
]
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/on-push-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
{ "example": "examples/machineusers/minimal.yaml", "api_path": "apis/machineusers" },
{ "example": "examples/machineusers/with-pat.yaml", "api_path": "apis/machineusers" },
{ "example": "examples/machineusers/with-pat-push.yaml","api_path": "apis/machineusers" },
{ "example": "examples/humanusers/explicit-org.yaml", "api_path": "apis/humanusers" },
{ "example": "examples/humanusers/with-org-ref.yaml", "api_path": "apis/humanusers" },
{ "example": "examples/grants/same-org.yaml", "api_path": "apis/grants" },
{ "example": "examples/grants/cross-org.yaml", "api_path": "apis/grants" }
]
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
_output/
.up/
.venv/
.tmp/
node_modules/
.DS_Store
apis/**/configuration.yaml
**/aws-creds
tests/**/secrets/
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ EXAMPLES := \
examples/machineusers/minimal.yaml:: \
examples/machineusers/with-pat.yaml:: \
examples/machineusers/with-pat-push.yaml:: \
examples/humanusers/explicit-org.yaml:: \
examples/humanusers/with-org-ref.yaml:: \
examples/grants/referenced-same-org.yaml:: \
examples/grants/same-org.yaml:: \
examples/grants/cross-org.yaml:: \
Expand Down
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,34 @@ status:

## Auth-group primitives

Per [[specs/identity-architecture]], the auth-group primitive XRDs that have substantive composition value-add — `MachineUser`, `Grant` — live in this repo alongside `AuthStack` under the `auth.hops.ops.com.ai` group.
Per [[specs/identity-architecture]], the auth-group primitive XRDs that have substantive composition value-add — `HumanUser`, `MachineUser`, `Grant` — live in this repo alongside `AuthStack` under the `auth.hops.ops.com.ai` group.

Status:

| Kind | Plural | Composes | Status |
|---|---|---|---|
| `HumanUser` | `humanusers` | One provider `HumanUser` with organization-ID reference resolution | ✓ |
| `MachineUser` | `machineusers` | `MachineUser` + opt-in `AccessToken` + opt-in AWS SM `Secret` + ESO `PushSecret` (provider-kubernetes Object) | ✓ |
| `Grant` | `grants` | `user.zitadel.../Grant` (same-Org) or `project.zitadel.../Grant + user.zitadel.../Grant` with `projectGrantId` (cross-Org) | ✓ |

Single-resource wrappers we deliberately didn't make: `HumanUser`, `IDP`, `OrganizationSsoConfig` (and the previously-attempted `Organization`, `Project`). Operators apply raw Zitadel / OpenPanel MRs directly for those.
Single-resource wrappers we deliberately didn't make: `IDP`, `OrganizationSsoConfig` (and the previously-attempted `Organization`, `Project`). Operators apply raw Zitadel / OpenPanel MRs directly for those.

### `HumanUser`

Declarative Zitadel human identity with organization-reference resolution. The
upstream provider documents `orgId` as optional, but its HumanUser v2 create path
sends an invalid empty organization when it is omitted. `HumanUser` resolves a
concrete organization UUID from a stable local resource such as a Zitadel
Project's `status.atProvider.orgId`, then renders the raw provider HumanUser.

Use `spec.orgIdRef` for GitOps so generated UUIDs do not enter the repository, or
use explicit `spec.orgId` for adoption and external integrations. Initial
passwords are accepted only by namespaced Secret reference. Typed status exposes
`userId`, `orgId`, and `loginName` for `Grant` and other consumers. The composed
resource remains rendered from its own observed `orgId` if the reference lookup
temporarily disappears.

See `examples/humanusers/{with-org-ref,explicit-org}.yaml`.

### `MachineUser`

Expand All @@ -105,7 +123,7 @@ See `examples/machineusers/{minimal,with-pat,with-pat-push}.yaml`.

### `Grant`

First-class membership relationship that ties a Zitadel User to a Project + Roles. For GitOps, prefer local references: `userIdRef` points to a HumanUser or MachineUser MR and `projectIdRef` points to a Project MR in the Grant namespace. The composition resolves IDs and Org IDs from each resource's `status.atProvider`, so no live Zitadel UUIDs need to be committed. Explicit `userId + userOrgId + projectId + projectOrgId` inputs remain available for adoption and cross-stack cases.
First-class membership relationship that ties a Zitadel User to a Project + Roles. For GitOps, prefer local references: `userIdRef` points to a raw HumanUser/MachineUser MR or the Hops `HumanUser` XR, and `projectIdRef` points to a Project MR in the Grant namespace. The composition resolves the Hops XR's typed status or raw resources' `status.atProvider`, so no live Zitadel UUIDs need to be committed. Explicit `userId + userOrgId + projectId + projectOrgId` inputs remain available for adoption and cross-stack cases.

Polymorphic dispatch then picks the right Zitadel mechanism:

Expand Down
33 changes: 33 additions & 0 deletions apis/grants/composition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@ spec:
kind: Grant
mode: Pipeline
pipeline:
- functionRef:
name: crossplane-contrib-function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: |
{{- $xr := getCompositeResource . }}
{{- $spec := $xr.spec | default dict }}
{{- $userRef := $spec.userIdRef | default dict }}
{{- $projectRef := $spec.projectIdRef | default dict }}
{{- $namespace := $xr.metadata.namespace | default "default" }}
{{- if or $userRef.name $projectRef.name }}
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: ExtraResources
requirements:
{{- if $userRef.name }}
referenced-user:
apiVersion: {{ $userRef.apiVersion | default "user.zitadel.m.crossplane.io/v1alpha1" | quote }}
kind: {{ $userRef.kind | default "HumanUser" | quote }}
matchName: {{ $userRef.name | quote }}
namespace: {{ $namespace | quote }}
{{- end }}
{{- if $projectRef.name }}
referenced-project:
apiVersion: {{ $projectRef.apiVersion | default "project.zitadel.m.crossplane.io/v1alpha1" | quote }}
kind: {{ $projectRef.kind | default "Project" | quote }}
matchName: {{ $projectRef.name | quote }}
namespace: {{ $namespace | quote }}
{{- end }}
{{- end }}
step: resolve-resource-references
- functionRef:
name: hops-ops-auth-stackgrant
step: grant
Expand Down
7 changes: 4 additions & 3 deletions apis/grants/definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ spec:
type: string
userIdRef:
description: |
Reference to a HumanUser or MachineUser managed resource in
the Grant namespace. The composition resolves userId and
userOrgId from status.atProvider.
Reference to a raw provider HumanUser/MachineUser managed
resource or an auth.hops.ops.com.ai HumanUser XR in the Grant
namespace. The composition resolves userId and userOrgId from
the reference's typed status or status.atProvider.
Mutually exclusive with userId + userOrgId.
type: object
properties:
Expand Down
38 changes: 38 additions & 0 deletions apis/humanusers/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: humanusers.auth.hops.ops.com.ai
spec:
compositeTypeRef:
apiVersion: auth.hops.ops.com.ai/v1alpha1
kind: HumanUser
mode: Pipeline
pipeline:
- functionRef:
name: crossplane-contrib-function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: |
{{- $xr := getCompositeResource . }}
{{- $spec := $xr.spec | default dict }}
{{- $ref := $spec.orgIdRef | default dict }}
{{- if $ref.name }}
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: ExtraResources
requirements:
organization-id-source:
apiVersion: {{ $ref.apiVersion | default "project.zitadel.m.crossplane.io/v1alpha1" | quote }}
kind: {{ $ref.kind | default "Project" | quote }}
matchName: {{ $ref.name | quote }}
namespace: {{ $ref.namespace | default ($xr.metadata.namespace | default "default") | quote }}
{{- end }}
step: resolve-organization-reference
- functionRef:
name: hops-ops-auth-stackhumanuser
step: humanuser
- functionRef:
name: crossplane-contrib-function-auto-ready
step: crossplane-contrib-function-auto-ready
177 changes: 177 additions & 0 deletions apis/humanusers/definition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: humanusers.auth.hops.ops.com.ai
spec:
group: auth.hops.ops.com.ai
names:
kind: HumanUser
plural: humanusers
scope: Namespaced
versions:
- name: v1alpha1
referenceable: true
served: true
schema:
openAPIV3Schema:
description: |
HumanUser creates a Zitadel human identity and resolves its
organization ID from a stable local managed-resource reference.
This keeps generated Zitadel UUIDs out of consumer GitOps while
working around provider-upjet-zitadel's HumanUser create path,
which requires a concrete orgId even when the provider credential
already belongs to that organization.
type: object
properties:
spec:
description: HumanUserSpec defines the desired human identity.
type: object
properties:
orgId:
description: |
Explicit Zitadel organization UUID. Mutually exclusive with
orgIdRef; prefer orgIdRef for GitOps-created organizations.
type: string
minLength: 1
orgIdRef:
description: |
Reference to a namespaced managed resource whose
status.atProvider.orgId supplies the Zitadel organization
UUID. Defaults to a provider-upjet-zitadel Project.
This is an ID source, not an ownership relationship.
type: object
properties:
name:
type: string
minLength: 1
namespace:
description: Defaults to the HumanUser namespace.
type: string
minLength: 1
apiVersion:
type: string
default: project.zitadel.m.crossplane.io/v1alpha1
kind:
type: string
default: Project
required:
- name
humanUserId:
description: |
Existing Zitadel user UUID to adopt. Propagates as the raw
managed resource's crossplane.io/external-name annotation.
type: string
minLength: 1
userName:
description: Zitadel username. Defaults to metadata.name.
type: string
minLength: 1
firstName:
description: Human user's first name.
type: string
minLength: 1
lastName:
description: Human user's last name.
type: string
minLength: 1
displayName:
description: Optional display name.
type: string
nickName:
description: Optional nickname.
type: string
email:
description: Human user's email address.
type: string
minLength: 1
phone:
description: Optional phone number.
type: string
preferredLanguage:
description: Optional preferred language, such as en.
type: string
gender:
description: Optional Zitadel gender enum value.
type: string
enum:
- GENDER_UNSPECIFIED
- GENDER_FEMALE
- GENDER_MALE
- GENDER_DIVERSE
isEmailVerified:
description: Whether Zitadel should mark the email verified.
type: boolean
isPhoneVerified:
description: Whether Zitadel should mark the phone verified.
type: boolean
initialSkipPasswordChange:
description: Whether the initial password may be kept on first login.
type: boolean
initialPasswordSecretRef:
description: |
Secret key containing the initial password. The Secret must
exist in the HumanUser namespace; password bytes never enter
the XR or composition function input.
type: object
properties:
name:
type: string
minLength: 1
key:
type: string
minLength: 1
required:
- name
- key
providerConfigRef:
description: ProviderConfig for provider-upjet-zitadel.
type: object
properties:
name:
type: string
minLength: 1
kind:
type: string
default: ProviderConfig
enum:
- ProviderConfig
- ClusterProviderConfig
required:
- name
managementPolicies:
description: Crossplane managementPolicies for the composed HumanUser.
type: array
items:
type: string
default: ["*"]
labels:
description: |
Custom labels merged with Hops defaults and applied to the
composed managed resource.
type: object
additionalProperties:
type: string
x-kubernetes-preserve-unknown-fields: true
x-kubernetes-validations:
- rule: '(has(self.orgIdRef) && !has(self.orgId)) || (!has(self.orgIdRef) && has(self.orgId))'
message: "provide exactly one organization source: orgIdRef or orgId"
required:
- firstName
- lastName
- email
- providerConfigRef
status:
description: Observed identity values for downstream references.
type: object
properties:
userId:
description: Observed Zitadel user UUID.
type: string
orgId:
description: Observed Zitadel organization UUID.
type: string
loginName:
description: Observed preferred Zitadel login name.
type: string
required:
- spec
13 changes: 13 additions & 0 deletions examples/humanusers/explicit-org.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: auth.hops.ops.com.ai/v1alpha1
kind: HumanUser
metadata:
name: bob
namespace: default
spec:
orgId: "373268222482392664"
firstName: Bob
lastName: Example
email: bob@example.com
providerConfigRef:
name: default
kind: ProviderConfig
20 changes: 20 additions & 0 deletions examples/humanusers/with-org-ref.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: auth.hops.ops.com.ai/v1alpha1
kind: HumanUser
metadata:
name: alice
namespace: default
spec:
orgIdRef:
name: platform
firstName: Alice
lastName: Example
displayName: Alice
email: alice@example.com
isEmailVerified: true
initialSkipPasswordChange: true
initialPasswordSecretRef:
name: human-passwords
key: alice
providerConfigRef:
name: default
kind: ClusterProviderConfig
Loading