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
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,40 @@ spec:
name: azure-account-creds
```

#### Only Active (Enabled) Accounts

By default a user is validated on existence alone, so a disabled (for example
offboarded) account still passes. Set `activeAccount: true` to additionally
require the Entra ID `accountEnabled` attribute to be true, omitting disabled
users from the result:

```yaml
queryType: UserValidation
users:
- "user1@yourdomain.com"
- "user2@yourdomain.com"
target: "status.validatedUsers"
activeAccount: true
```

Every validated user carries its `accountEnabled` value in the result,
regardless of whether `activeAccount` is set, so the account state is visible to
downstream consumers either way. A user for which Microsoft Graph returns no
`accountEnabled` value is treated as disabled: when the account state cannot be
confirmed, `activeAccount: true` excludes the user.

> Note: `activeAccount` only applies to the `UserValidation` query type, and it
> filters the result of a query. It does not retroactively purge users from a
> target that is no longer being refreshed, so combining it with
> `skipQueryWhenTargetHasData: true` on an existing XR leaves any already-stored
> disabled users in place. Clear the target once (or drop the skip for one
> reconcile) when enabling the flag on an existing deployment. `queryInterval`
> has the same effect, bounded by the interval.

> Note: consumers whose XRD constrains the target with a structural item schema
> (rather than `x-kubernetes-preserve-unknown-fields`) need to add
> `accountEnabled` to that schema, otherwise the API server silently prunes it.

### Get Group Membership

```yaml
Expand Down Expand Up @@ -276,7 +310,8 @@ spec:
| `target` | string | Required. Where to store the query results. Can be `status.<field>` or `context.<field>` |
| `skipQueryWhenTargetHasData` | bool | Optional. When true, will skip the query if the target already has data |
| `queryInterval` | string | Optional. Minimum interval between queries as a Go duration string (e.g. `10m`, `1h`, `90s`). Skips querying Microsoft Graph until the interval has elapsed since the last successful query, independent of reconcile frequency. Only effective in Composition mode with a `status.` target. |
| `FailOnEmpty` | bool | Optional. When true, the function will fail if the `users`, `groups`, or `servicePrincipals` lists are empty, or if their respective reference fields are empty lists. |
| `failOnEmpty` | bool | Optional. When true, the function will fail if the `users`, `groups`, or `servicePrincipals` lists are empty, or if their respective reference fields are empty lists. |
| `activeAccount` | bool | Optional. `UserValidation` only. When true, only users whose Entra ID `accountEnabled` attribute is true are stored at the target; disabled users, and users whose account state Graph does not report, are omitted. |
| `identity.type` | string | Optional. Type of identity credentials to use. Valid values: `AzureServicePrincipalCredentials`, `AzureWorkloadIdentityCredentials`. Default is `AzureServicePrincipalCredentials` |

## Result Targets
Expand Down Expand Up @@ -336,6 +371,7 @@ status:
displayName: "Jane Doe"
userPrincipalName: "jane@example.com"
mail: "jane@example.com"
accountEnabled: true
lastQueryTimestamps: # separate metadata, keyed by target
validatedUsers: "2026-07-16T10:00:00Z"
```
Expand Down
16 changes: 16 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,19 @@ crossplane render xr-with-last-query-time.yaml user-validation-example-query-int
> `xr-with-last-query-time.yaml` uses a far-future timestamp in `status.lastQueryTimestamps` so the skip is deterministic; set it to a real recent time to test the natural elapsed boundary. The credentials are not used on the skip path (no Graph call is made), so they need not be valid for this command.

> **macOS note:** these commands use `-r` (function results) rather than `-rc`. The `-c`/`--include-context` flag makes `crossplane render` v2.x run an internal context-extraction step over a unix socket bind-mounted into its Docker helper container, which Docker Desktop for macOS does not support (`connect: operation not supported`) — the render then hangs with no output. Since the query-interval results are written to a `status.` target, `-c` is unnecessary here. This is a known CLI bug ([crossplane/cli#161](https://github.com/crossplane/cli/issues/161)), fixed by [#163](https://github.com/crossplane/cli/pull/163) (context function now listens on TCP) but not yet in a tagged release as of CLI v2.4.0. Until then, drop `-c` on macOS, or run `crossplane render` on Linux / a CLI built from `main` if you need context output.

### 6. Active Account Filtering

Restrict `UserValidation` results to enabled accounts with `activeAccount: true`. Users whose Entra ID `accountEnabled` attribute is not true are omitted from the target. Every result carries its `accountEnabled` value regardless of the flag, so the two renders below can be compared directly (both write to a `status.` target, so `-r` is sufficient, see the macOS note above):

```shell
crossplane render xr.yaml user-validation-example-active-account.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -r
```

Disabled users are absent from `status.validatedUsers`, and every remaining entry has `accountEnabled: true`. Render the plain example for the unfiltered baseline, where a disabled user is still listed, with `accountEnabled: false`:

```shell
crossplane render xr.yaml user-validation-example.yaml functions.yaml --function-credentials=./secrets/azure-creds.yaml -r
```

> The example intentionally omits `skipQueryWhenTargetHasData`. Enabling `activeAccount` on an XR whose target already holds disabled users does not purge them while the query is being skipped, so clear the target once (or drop the skip for one reconcile) when adopting the flag on an existing deployment.
56 changes: 46 additions & 10 deletions example/e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
# queryInterval e2e (Composition mode)
# function-msgraph e2e (Composition mode)

Manifests to exercise the `queryInterval` throttling feature end-to-end on a real
cluster, where the time-based skip/refresh loop can actually be observed across
reconciles (something `crossplane render` cannot show).
Manifests to exercise features end-to-end on a real cluster, where behaviour
across reconciles can actually be observed (something `crossplane render` cannot
show).

## Files

| File | Purpose |
|------|---------|
| `function.yaml` | Installs `function-msgraph` (pin the tag you want to test) |
| `function.yaml` | Installs `function-msgraph` (pin the tag you want to test) with `--debug` enabled |
| `composition.yaml` | UserValidation → `status.validatedUsers`, `queryInterval: "2m"` |
| `xr.yaml` | A composite resource instance |
| `xr.yaml` | A composite resource instance for the `queryInterval` scenario |
| `composition-active-account.yaml` | Two UserValidation Compositions, identical except for `activeAccount: true` |
| `xr-active-account.yaml` | One composite resource instance per `activeAccount` Composition |

Reuses `../definition.yaml` (XRD) and the `azure-account-creds` secret built from
`../secrets/azure-creds.yaml` — see [Update Credentials](../README.md#update-credentials).
Populate it locally; keep real values out of commits.

## Run
## Cluster

```shell
kind create cluster --name msgraph-e2e
Expand All @@ -27,12 +29,15 @@ kubectl apply -f example/e2e/function.yaml
kubectl wait function.pkg.crossplane.io/function-msgraph --for=condition=Healthy --timeout=180s

kubectl apply -f example/definition.yaml
kubectl apply -f example/e2e/composition.yaml
kubectl apply -f example/secrets/azure-creds.yaml # populate locally; keep real values out of commits
kubectl apply -f example/e2e/xr.yaml
```

## Verify
## Scenario: queryInterval

```shell
kubectl apply -f example/e2e/composition.yaml
kubectl apply -f example/e2e/xr.yaml
```

```shell
# the result list stays a clean list of results
Expand All @@ -54,9 +59,40 @@ Force reconciles with `kubectl annotate xr msgraph-query-interval-e2e poke=$(dat
Poking repeatedly inside the 2m window leaves the query count flat; after 2m
elapses the next reconcile re-queries and `status.lastQueryTimestamps.validatedUsers` advances.

## Scenario: activeAccount

Edit `composition-active-account.yaml` and `xr-active-account.yaml` first: replace
the placeholder UPNs with one enabled and one deliberately disabled account from
your directory, otherwise the filter has nothing to exclude.

```shell
kubectl apply -f example/e2e/composition-active-account.yaml
kubectl apply -f example/e2e/xr-active-account.yaml
```

```shell
# baseline: the disabled account passes validation, reported as accountEnabled false
kubectl get xr msgraph-baseline-e2e -o jsonpath='{.status.validatedUsers}' | jq

# activeAccount: only the enabled account is stored
kubectl get xr msgraph-active-account-e2e -o jsonpath='{.status.validatedUsers}' | jq

# the exclusion is logged per user
POD=$(kubectl get pods -n crossplane-system -o name | grep msgraph)
kubectl logs -n crossplane-system "$POD" | grep 'Skipping user with disabled account'
```

Both XRs should report `SYNCED=True READY=True` with a `FunctionSuccess/Success`
condition. Poke either XR to confirm the filtering holds across reconciles rather
than being a first-pass artefact.

## Notes

- `queryInterval` is effective only in Composition mode with a `status.` target.
- `activeAccount` filters the result of a query, it does not purge a target that
is no longer being refreshed. Combining it with `skipQueryWhenTargetHasData` or
`queryInterval` leaves already-stored disabled users in place, which is why the
`activeAccount` Composition sets neither.
- The example XRD resolves to `LegacyCluster` scope under Crossplane v2, so the XR
is cluster-scoped (no namespace).
- Teardown: `kind delete cluster --name msgraph-e2e`.
65 changes: 65 additions & 0 deletions example/e2e/composition-active-account.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
# Baseline: existence check only. A disabled account still passes validation,
# which is the behaviour activeAccount exists to change. Kept alongside the
# filtered Composition so the two can be compared in the same cluster.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: user-validation-baseline-e2e
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: XR
mode: Pipeline
pipeline:
- step: validate-user
functionRef:
name: function-msgraph
input:
apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
queryType: UserValidation
# Replace with real accounts from your directory: one enabled, one
# deliberately disabled, otherwise there is nothing for the filter to do.
users:
- "user@example.onmicrosoft.com"
- "disabled-user@example.onmicrosoft.com"
target: "status.validatedUsers"
credentials:
- name: azure-creds
source: Secret
secretRef:
namespace: crossplane-system
name: azure-account-creds
---
# Same query, restricted to enabled accounts. Note that neither
# skipQueryWhenTargetHasData nor queryInterval is set: both suppress the query,
# so an already-populated target would keep its disabled entries.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: user-validation-active-account-e2e
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: XR
mode: Pipeline
pipeline:
- step: validate-user
functionRef:
name: function-msgraph
input:
apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
queryType: UserValidation
users:
- "user@example.onmicrosoft.com"
- "disabled-user@example.onmicrosoft.com"
target: "status.validatedUsers"
activeAccount: true
credentials:
- name: azure-creds
source: Secret
secretRef:
namespace: crossplane-system
name: azure-account-creds
22 changes: 21 additions & 1 deletion example/e2e/function.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
---
# Run the function with --debug so per-user decisions are visible in the pod
# logs (for example the accounts excluded by activeAccount, and the queryInterval
# skips).
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: msgraph-debug
spec:
deploymentTemplate:
spec:
selector: {}
template:
spec:
containers:
- name: package-runtime
args:
- --debug
---
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-msgraph
spec:
package: xpkg.upbound.io/upbound/function-msgraph:v0.7.0-rc4
package: xpkg.upbound.io/upbound/function-msgraph:v0.8.0-rc2
runtimeConfigRef:
name: msgraph-debug
24 changes: 24 additions & 0 deletions example/e2e/xr-active-account.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
apiVersion: example.crossplane.io/v1
kind: XR
metadata:
name: msgraph-baseline-e2e
spec:
compositionRef:
name: user-validation-baseline-e2e
userAccess:
emails:
- "user@example.onmicrosoft.com"
- "disabled-user@example.onmicrosoft.com"
---
apiVersion: example.crossplane.io/v1
kind: XR
metadata:
name: msgraph-active-account-e2e
spec:
compositionRef:
name: user-validation-active-account-e2e
userAccess:
emails:
- "user@example.onmicrosoft.com"
- "disabled-user@example.onmicrosoft.com"
40 changes: 40 additions & 0 deletions example/user-validation-example-active-account.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: user-validation-example-active-account
# Important: This function example requires an Azure AD app registration with Microsoft Graph API permissions:
# - User.Read.All
# - Directory.Read.All
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: XR
mode: Pipeline
pipeline:
- step: validate-user
functionRef:
name: function-msgraph
input:
apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
queryType: UserValidation
# Replace these with actual users in your directory. To see the filter
# take effect, include at least one account that is disabled in Entra ID.
users:
- "admin@example.onmicrosoft.com"
- "user@example.onmicrosoft.com"
- "yury@upbound.io"
target: "status.validatedUsers"
# Only users whose accountEnabled attribute is true are stored at the
# target. Disabled users, and users whose account state Microsoft Graph
# does not report, are omitted.
activeAccount: true
# Note: skipQueryWhenTargetHasData is deliberately not set here, so every
# render re-queries Graph. With the skip enabled, a target that already
# holds disabled users is never refreshed and they would persist.
credentials:
- name: azure-creds
source: Secret
secretRef:
namespace: crossplane-system
name: azure-account-creds
Loading
Loading