A Crossplane composition function for querying the Microsoft Graph API.
The function-msgraph provides read-only access to Microsoft Graph API endpoints, allowing Crossplane compositions to:
- Validate Azure AD User Existence
- Get Group Membership
- Get Group Object IDs
- Get Service Principal Details
The function supports throttling mitigation with the skipQueryWhenTargetHasData flag to avoid unnecessary API calls.
Add the function to your Crossplane installation:
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
  name: function-msgraph
spec:
  package: xpkg.upbound.io/upbound/function-msgraph:v0.1.0The service principal needs the following Microsoft Graph API permissions:
- User.Read.All (for user validation)
- Group.Read.All (for group operations)
- Application.Read.All (for service principal details)
Create an Azure service principal with appropriate permissions to access Microsoft Graph API:
apiVersion: v1
kind: Secret
metadata:
  name: azure-account-creds
  namespace: crossplane-system
type: Opaque
stringData:
  credentials: |
    {
      "clientId": "your-client-id",
      "clientSecret": "your-client-secret", 
      "subscriptionId": "your-subscription-id",
      "tenantId": "your-tenant-id"
    }AKS cluster needs to have workload identity enabled. The managed identity needs to have the Federated Identity Credential created: https://azure.github.io/azure-workload-identity/docs/topics/federated-identity-credential.html.
apiVersion: v1
kind: Secret
metadata:
  name: azure-account-creds
  namespace: crossplane-system
type: Opaque
stringData:
  credentials: |
    {
      "clientId": "your-client-id", # optional
      "tenantId": "your-tenant-id", # optional
      "federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token"
    }apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
  name: upbound-function-msgraph
spec:
  package: xpkg.upbound.io/upbound/function-msgraph:v0.2.0
  runtimeConfigRef:
    apiVersion: pkg.crossplane.io/v1beta1
    kind: DeploymentRuntimeConfig
    name: upbound-function-msgraphapiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
  name: upbound-function-msgraph
spec: 
  deploymentTemplate:
    spec:
      selector:
        matchLabels:
          azure.workload.identity/use: "true"
          pkg.crossplane.io/function: "upbound-function-msgraph"
      template:
        metadata:
          labels:
            azure.workload.identity/use: "true"
            pkg.crossplane.io/function: "upbound-function-msgraph"
        spec:
          containers:
          - name: package-runtime
            volumeMounts:
            - mountPath: /var/run/secrets/azure/tokens
              name: azure-identity-token
              readOnly: true
          serviceAccountName: "upbound-function-msgraph"
          volumes:
          - name: azure-identity-token
            projected:
              sources:
              - serviceAccountToken:
                  audience: api://AzureADTokenExchange
                  expirationSeconds: 3600
                  path: azure-identity-token
  serviceAccountTemplate:
    metadata:
      annotations:
        azure.workload.identity/client-id: "your-client-id"
      name: "upbound-function-msgraph"apiVersion: example.crossplane.io/v1
kind: Composition
metadata:
  name: user-validation-example
spec:
  compositeTypeRef:
    apiVersion: example.crossplane.io/v1
    kind: XR
  pipeline:
  - step: validate-user
    functionRef:
      name: function-msgraph
    input:
      apiVersion: msgraph.fn.crossplane.io/v1alpha1
      kind: Input
      queryType: UserValidation
      users:
        - "[email protected]"
        - "[email protected]"
      target: "status.validatedUsers"
      skipQueryWhenTargetHasData: true
    credentials:
      - name: azure-creds
        source: Secret
        secretRef:
          namespace: crossplane-system
          name: azure-account-credsapiVersion: example.crossplane.io/v1
kind: Composition
metadata:
  name: group-membership-example
spec:
  compositeTypeRef:
    apiVersion: example.crossplane.io/v1
    kind: XR
  pipeline:
  - step: get-group-members
    functionRef:
      name: function-msgraph
    input:
      apiVersion: msgraph.fn.crossplane.io/v1alpha1
      kind: Input
      queryType: GroupMembership
      group: "Developers"
      # The function will automatically select standard fields:
      # - id, displayName, mail, userPrincipalName, appId, description
      target: "status.groupMembers"
      skipQueryWhenTargetHasData: true
    credentials:
      - name: azure-creds
        source: Secret
        secretRef:
          namespace: crossplane-system
          name: azure-account-credsapiVersion: example.crossplane.io/v1
kind: Composition
metadata:
  name: group-objectids-example
spec:
  compositeTypeRef:
    apiVersion: example.crossplane.io/v1
    kind: XR
  pipeline:
  - step: get-group-objectids
    functionRef:
      name: function-msgraph
    input:
      apiVersion: msgraph.fn.crossplane.io/v1alpha1
      kind: Input
      queryType: GroupObjectIDs
      groups:
        - "Developers"
        - "Operations"
        - "Security"
      target: "status.groupObjectIDs"
      skipQueryWhenTargetHasData: true
    credentials:
      - name: azure-creds
        source: Secret
        secretRef:
          namespace: crossplane-system
          name: azure-account-credsapiVersion: example.crossplane.io/v1
kind: Composition
metadata:
  name: service-principal-example
spec:
  compositeTypeRef:
    apiVersion: example.crossplane.io/v1
    kind: XR
  pipeline:
  - step: get-service-principal-details
    functionRef:
      name: function-msgraph
    input:
      apiVersion: msgraph.fn.crossplane.io/v1alpha1
      kind: Input
      queryType: ServicePrincipalDetails
      servicePrincipals:
        - "MyServiceApp"
        - "ApiConnector"
      target: "status.servicePrincipalDetails"
      skipQueryWhenTargetHasData: true
    credentials:
      - name: azure-creds
        source: Secret
        secretRef:
          namespace: crossplane-system
          name: azure-account-creds| Field | Type | Description | 
|---|---|---|
| queryType | string | Required. Type of query to perform. Valid values: UserValidation,GroupMembership,GroupObjectIDs,ServicePrincipalDetails | 
| users | []string | List of user principal names (email IDs) for user validation | 
| usersRef | string | Reference to resolve a list of user names from spec,statusorcontext(e.g.,spec.userAccess.emails) | 
| group | string | Single group name for group membership queries | 
| groupRef | string | Reference to resolve a single group name from spec,statusorcontext(e.g.,spec.groupConfig.name) | 
| groups | []string | List of group names for group object ID queries | 
| groupsRef | string | Reference to resolve a list of group names from spec,statusorcontext(e.g.,spec.groupConfig.names) | 
| servicePrincipals | []string | List of service principal names | 
| servicePrincipalsRef | string | Reference to resolve a list of service principal names from spec,statusorcontext(e.g.,spec.servicePrincipalConfig.names) | 
| target | string | Required. Where to store the query results. Can be status.<field>orcontext.<field> | 
| skipQueryWhenTargetHasData | bool | Optional. When true, will skip the query if the target already has data | 
| `identity.type | string | Optional. Type of identity credentials to use. Valid values: AzureServicePrincipalCredentials,AzureWorkloadIdentityCredentials. Default isAzureServicePrincipalCredentials | 
Results can be stored in either XR Status or Composition Context:
# Store in XR Status
target: "status.results"
# Store in nested XR Status
target: "status.nested.field.results"
# Store in Composition Context
target: "context.results"
# Store in Environment
target: "context.[apiextensions.crossplane.io/environment].results"You can reference values from XR spec, status, or context instead of hardcoding them:
apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
queryType: GroupMembership
groupRef: "spec.groupConfig.name"  # Get group name from XR spec
target: "status.groupMembers"apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
queryType: GroupObjectIDs
groupsRef: "spec.groupConfig.names"  # Get group names from XR spec
target: "status.groupObjectIDs"apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
queryType: UserValidation
usersRef: "spec.userAccess.emails"  # Get user emails from XR spec
target: "status.validatedUsers"apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
queryType: ServicePrincipalDetails
servicePrincipalsRef: "spec.servicePrincipalConfig.names"  # Get service principal names from XR spec
target: "status.servicePrincipals"apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
identity:
  type: AzureServicePrincipalCredentialsapiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: InputapiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
identity:
  type: AzureWorkloadIdentityCredentialsfunction-msgraph support every kind of operations but it only allows targeting Composite Resources Function omits the input.skipQueryWhenTargetHasData parameter when running in operation mode to enforce compability with Cron/Watch modes. CronOperations and WatchOperations are the most useful in context of graph queries, please check examples.
Important: Operations and Compositions work in conjunction to provide a self-healing mechanism:
- 
Operations Role (Drift Detection): - Query Microsoft Graph API on schedule/watch events
- Compare results with current XR status
- Set drift detection annotations (but don't update status directly)
 
- 
Compositions Role (Drift Correction): - Run when XR is reconciled (triggered by annotation changes)
- Check drift detection annotation
- If drift detected, ignore skipQueryWhenTargetHasDataflag and update status
- Reset drift annotation to "false" after successful update
 
This creates a two-phase self-healing system where Operations monitor for changes and Compositions perform the actual data updates.
function-msgraph operations result in two annotations set on the XR:
apiVersion: "example.org/v1"
kind: XR
metadata:
  name: "cool-xr"
  annotations:
    "function-msgraph/last-execution": "2025-01-01T00:00:00+01:00"
    "function-msgraph/last-execution-query-drift-detected": "false"function-msgraph/last-execution sets RFC3339 timestamp informing about last succesful Operation run. function-msgraph/last-execution-query-drift-detected sets a boolean if there's a drift between input.target field's value and query result, which is used by function-msgraph in Composition context for self-healing. skipQueryWhenTargetHasData input parameter is ommited when drift detected annotation is set which leads to XR update and after that next Operation run sets the annotation back to "false".
CronOperation may be used to forcefully update XR's status in a predefined interval. That functionality may be especially useful for XRs that are business critical and should have the data refreshed without worrying about throttling. Supports only singular resource reference.
apiVersion: ops.crossplane.io/v1alpha1
kind: CronOperation
metadata:
  name: update-user-validation-for-critical-xr
spec:
  schedule: "*/5 * * * *" # Every 5 minutes
  concurrencyPolicy: Forbid
  successfulHistoryLimit: 5
  failedHistoryLimit: 3
  operationTemplate:
    spec:
      mode: Pipeline
      pipeline:
      - step: user-validation
        functionRef:
          name: function-msgraph
        input:
          apiVersion: msgraph.fn.crossplane.io/v1alpha1
          kind: Input
          queryType: UserValidation
          # Replace these with actual users in your directory
          users:
            - "[email protected]"
            - "[email protected]"
            - "[email protected]"
          target: "status.validatedUsers"
        credentials:
          - name: azure-creds
            source: Secret
            secretRef:
              namespace: upbound-system
              name: azure-account-creds
        requirements:
          requiredResources:
          - requirementName: ops.crossplane.io/watched-resource
            apiVersion: example.crossplane.io/v1
            kind: XR
            name: business-critical-xrWatchOperation may be used to forcefully update XR's status based on match condition.
For example it may be useful to refresh status in business critical XR's that are labeled with label always-update: "true".
apiVersion: ops.crossplane.io/v1alpha1
kind: WatchOperation
metadata:
  name: update-user-validation-for-critical-xrs
spec:
  watch:
    apiVersion: example.crossplane.io/v1
    kind: XR
    matchLabels:
      always-update: "true"
  concurrencyPolicy: Allow
  operationTemplate:
    spec:
      mode: Pipeline
      pipeline:
      - step: user-validation
        functionRef:
          name: function-msgraph
        input:
          apiVersion: msgraph.fn.crossplane.io/v1alpha1
          kind: Input
          queryType: UserValidation
          # Replace these with actual users in your directory
          users:
            - "[email protected]"
            - "[email protected]"
            - "[email protected]"
          target: "status.validatedUsers"
        credentials:
          - name: azure-creds
            source: Secret
            secretRef:
              namespace: upbound-system
              name: azure-account-creds