zitadel-rbac-mapper

module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 12, 2026 License: MIT

README

Zitadel RBAC Mapper

Groups-to-grants mapping webhook for Zitadel Actions V2. Resolves user's Google Workspace group memberships, maps groups to project roles, syncs UserGrants, and enriches tokens with a groups claim — all in a single synchronous function call during the OIDC flow.

What it does

On every login (OIDC token issuance), Zitadel calls this webhook via preuserinfo/preaccesstoken function executions. The webhook:

  1. Resolves groups — calls google-group-sync (localhost:9090) to get the user's Google Workspace group emails
  2. Syncs grants — maps groups to project roles via configured rules, then performs idempotent add/update/remove of UserGrants in Zitadel via gRPC
  3. Enriches token — returns append_claims with a groups claim containing the group email list

The grant sync is idempotent — if grants are already correct, no Zitadel API calls are made.

Architecture

[User logs in] → [Zitadel OIDC flow]
                        │
                        ├─ function/preuserinfo  ──→ [restCall target] ──→ POST /webhook
                        └─ function/preaccesstoken → [restCall target] ──→ POST /webhook
                                                                                │
                                                        [zitadel-rbac-mapper Lambda]
                                                                │
                                                                ├─ google-group-sync extension (localhost:9090)
                                                                ├─ resolve groups → map to grants → sync via gRPC
                                                                └─ return {"append_claims": [{"key":"groups","value":[...]}]}

Why Functions, Not Events

Grant sync runs in the preuserinfo function (not as an async event handler) because:

  • Timing: Grants must exist before the token is issued. Async events arrive after the fact — too late.
  • Reliability: Functions are synchronous and inline. Events have no delivery guarantees and can be delayed/dropped on Zitadel Cloud.
  • Idempotency: The sync is diff-based. Running on every login is safe — no-op if nothing changed.
  • Simplicity: One target, two executions. No event infrastructure needed.

Zitadel Configuration

1. Create a REST Call target
curl -L -X POST 'https://<ZITADEL_DOMAIN>/v2/actions/targets' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <PAT>' \
--data-raw '{
  "name": "rbac-mapper",
  "restCall": {
    "interruptOnError": false
  },
  "endpoint": "https://<LAMBDA_FUNCTION_URL>/webhook",
  "timeout": "30s",
  "payloadType": "PAYLOAD_TYPE_JWT"
}'

Important:

  • Target type must be REST Call (not Webhook) — Zitadel reads the response body to apply append_claims
  • Payload type JWT — the body is signed with the instance key, verified via JWKS
  • interruptOnError: false — token issuance continues even if the webhook fails
2. Create function executions
# Adds "groups" claim to userinfo endpoint
curl -L -X PUT 'https://<ZITADEL_DOMAIN>/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <PAT>' \
--data-raw '{"condition":{"function":{"name":"preuserinfo"}},"targets":["<TARGET_ID>"]}'

# Adds "groups" claim to access token
curl -L -X PUT 'https://<ZITADEL_DOMAIN>/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <PAT>' \
--data-raw '{"condition":{"function":{"name":"preaccesstoken"}},"targets":["<TARGET_ID>"]}'
Final configuration
Target Type Payload Timeout
rbac-mapper REST Call JWT 30s
Execution Condition Effect
preuserinfo function/preuserinfo Groups claim in userinfo + grant sync
preaccesstoken function/preaccesstoken Groups claim in access token + grant sync

No event executions needed. No webhook targets needed.

Environment Variables

Variable Required Default Description
ZITADEL_DOMAIN Yes Zitadel instance domain (for gRPC + JWKS)
ZITADEL_PORT No 443 Zitadel gRPC port
ZITADEL_KEY_FILE Mutual excl. Path to JWT key JSON file
ZITADEL_KEY_JSON Mutual excl. Raw JWT key JSON content
ZITADEL_KEY_SECRET_NAME No AWS Secrets Manager secret (Lambda only)
GROUPS_RESOLVER_URL Yes Groups resolver URL (e.g., http://localhost:9090/groups)
RULES_FILE Mutual excl. Path to rules YAML file
RULES_JSON Mutual excl. Inline rules JSON
PORT No 8080 HTTP server port
HEALTH_PORT No 7070 Health probe port
LOG_LEVEL No info Log level: debug, info, warn, error
LOG_FORMAT No json Log format: json, text

Rules Format

Rules map Google Workspace groups to Zitadel project roles:

[
  {
    "group": "engineering-admin@example.com",
    "grants": [
      {"project": "<project-id>", "roles": ["admin"]},
      {"project": "<project-id-2>", "roles": ["admin"]}
    ]
  },
  {
    "group": "engineering-devops@example.com",
    "grants": [
      {"project": "<project-id>", "roles": ["devops", "engineer"]}
    ]
  }
]

The project field can be a project name (resolved at startup) or a project ID directly (when generated by Pulumi).

Deployment

AWS Lambda (with google-group-sync extension)
[zitadel-rbac-mapper Lambda]
  ├── LWA layer (event → HTTP on :8080)
  ├── google-group-sync extension layer (HTTP on :9090)
  │     └── reads GGS_SA_KEY_SECRET_NAME from Secrets Manager
  └── rbac-mapper binary
        ├── POST /webhook — function payload handler
        ├── POST /sync — direct sync API (programmatic use)
        └── GET /health — health check
Kubernetes (Helm chart)
helm install zitadel-rbac-mapper oci://ghcr.io/truvity/charts/zitadel-rbac-mapper \
  --set zitadelKey.secretName=zitadel-sa-key \
  --set sidecar.enabled=true \
  --set sidecar.saKey.secretName=google-sa-key

Development

devbox shell          # activates dev environment (GOEXPERIMENT=jsonv2)
just build            # build binaries
just test             # run unit tests
just lint             # run linter
just test-integration # run integration tests (requires real Zitadel)
just check            # build + test + lint + vuln

License

MIT

Directories

Path Synopsis
cmd
zitadel-rbac-mapper command
Package main is the entry point for zitadel-rbac-mapper.
Package main is the entry point for zitadel-rbac-mapper.
zitadel-rbac-mapper-lambda command
Package main is the Lambda entry point for zitadel-rbac-mapper.
Package main is the Lambda entry point for zitadel-rbac-mapper.
pkg
app
Package app wires all components and runs the service.
Package app wires all components and runs the service.
config
Package config provides environment-based configuration for zitadel-rbac-mapper.
Package config provides environment-based configuration for zitadel-rbac-mapper.
grantsync
Package grantsync provides idempotent Zitadel UserGrant synchronization.
Package grantsync provides idempotent Zitadel UserGrant synchronization.
mapper
Package mapper provides the rules engine that maps groups to desired Zitadel UserGrants.
Package mapper provides the rules engine that maps groups to desired Zitadel UserGrants.
resolver
Package resolver provides an HTTP client for the groups resolver service (google-group-sync).
Package resolver provides an HTTP client for the groups resolver service (google-group-sync).
server
Package server provides the HTTP server for zitadel-rbac-mapper.
Package server provides the HTTP server for zitadel-rbac-mapper.
zitadeljwt
Package zitadeljwt verifies JWT-signed payloads from Zitadel Actions V2 webhooks.
Package zitadeljwt verifies JWT-signed payloads from Zitadel Actions V2 webhooks.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL