Add JWT-based authorization support for Temporal server with Keycloak integration
- Create QUICK_REFERENCE.md for a concise guide on setting up temporal authorization. - Add README_AUTHORIZATION.md detailing the implementation steps and common issues. - Introduce REVERSE_PROXY_APPROACH.md as an alternative method for authorization using a reverse proxy. - Implement Dockerfile for building a custom Temporal server with authorization features. - Add main.go to initialize the custom Temporal server with JWT authorization. - Create example-keycloak-mapper.json for mapping Keycloak groups to Temporal permissions. - Add development.yaml for configuring the Temporal server with JWT settings. - Implement test-authorization.sh script to verify JWT token claims and Temporal server access. - Include go.mod for managing Go dependencies in the custom server. - Document troubleshooting steps and customization options in README.md.
This commit is contained in:
400
AUTHORIZATION_GUIDE.md
Normal file
400
AUTHORIZATION_GUIDE.md
Normal file
@ -0,0 +1,400 @@
|
||||
# Temporal Authorization Implementation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to implement fine-grained authorization for your self-hosted Temporal deployment with OIDC/Keycloak integration.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
|
||||
│ Browser │─────▶│ Keycloak │─────▶│ Temporal UI │
|
||||
│ │◀─────│ (OIDC) │◀─────│ │
|
||||
└─────────────┘ └──────────────┘ └─────────────────┘
|
||||
│ │
|
||||
│ JWT Token │ API Calls
|
||||
│ with claims │ (with JWT)
|
||||
▼ ▼
|
||||
┌──────────────────────────────────┐
|
||||
│ Temporal Server │
|
||||
│ ┌────────────────────────┐ │
|
||||
│ │ ClaimMapper │ │
|
||||
│ │ (Extracts JWT claims) │ │
|
||||
│ └────────────┬───────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────▼───────────┐ │
|
||||
│ │ Authorizer │ │
|
||||
│ │ (Allow/Deny decision) │ │
|
||||
│ └────────────────────────┘ │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### 1. ClaimMapper
|
||||
- Extracts claims from JWT tokens
|
||||
- Translates OIDC/Keycloak groups/roles into Temporal permissions
|
||||
- Temporal provides a default JWT ClaimMapper that you can use
|
||||
|
||||
### 2. Authorizer
|
||||
- Makes authorization decisions for each API call
|
||||
- Checks if the caller's claims allow the requested operation
|
||||
- Temporal provides a default Authorizer
|
||||
|
||||
### 3. JWT Token Format
|
||||
|
||||
The default JWT ClaimMapper expects a `permissions` array in your JWT:
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "user@example.com",
|
||||
"permissions": [
|
||||
"temporal-system:admin",
|
||||
"production:read",
|
||||
"development:write"
|
||||
],
|
||||
"aud": "temporal-ui",
|
||||
"iss": "https://keycloak.example.com/realms/myrealm"
|
||||
}
|
||||
```
|
||||
|
||||
Permission format: `<namespace>:<role>`
|
||||
|
||||
Available roles:
|
||||
- `read` - Read-only access
|
||||
- `write` - Read and write access
|
||||
- `worker` - Worker permissions
|
||||
- `admin` - Full admin access
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Configure Keycloak Claims
|
||||
|
||||
You need to map Keycloak groups/roles to the `permissions` claim format that Temporal expects.
|
||||
|
||||
#### Option A: Using Keycloak Groups
|
||||
|
||||
1. Create Keycloak groups for your teams:
|
||||
- `temporal-admins`
|
||||
- `dev-team`
|
||||
- `ops-team`
|
||||
- `viewers`
|
||||
|
||||
2. Add a **Protocol Mapper** to your Temporal UI client:
|
||||
- Mapper Type: `Group Membership`
|
||||
- Token Claim Name: `groups`
|
||||
- Full group path: OFF
|
||||
- Add to ID token: YES
|
||||
- Add to access token: YES
|
||||
- Add to userinfo: YES
|
||||
|
||||
3. Add a **Script Mapper** to transform groups to permissions:
|
||||
- Mapper Type: `Script Mapper`
|
||||
- Token Claim Name: `permissions`
|
||||
- Script:
|
||||
```javascript
|
||||
var permissions = [];
|
||||
var groups = user.getGroupIds();
|
||||
|
||||
for each (var group in groups) {
|
||||
var groupName = group.getName();
|
||||
if (groupName === 'temporal-admins') {
|
||||
permissions.push('temporal-system:admin');
|
||||
} else if (groupName === 'dev-team') {
|
||||
permissions.push('development:write');
|
||||
permissions.push('staging:write');
|
||||
} else if (groupName === 'ops-team') {
|
||||
permissions.push('production:write');
|
||||
} else if (groupName === 'viewers') {
|
||||
permissions.push('production:read');
|
||||
permissions.push('development:read');
|
||||
}
|
||||
}
|
||||
|
||||
permissions;
|
||||
```
|
||||
|
||||
#### Option B: Using Keycloak Roles
|
||||
|
||||
1. Create Keycloak roles:
|
||||
- `temporal-system-admin`
|
||||
- `namespace-dev-writer`
|
||||
- `namespace-prod-reader`
|
||||
|
||||
2. Add a **User Realm Role Mapper**:
|
||||
- Mapper Type: `User Realm Role`
|
||||
- Token Claim Name: `roles`
|
||||
- Add to ID token: YES
|
||||
|
||||
3. Add a **Script Mapper** to convert roles to permissions format
|
||||
|
||||
#### Option C: Custom Attribute Mapper
|
||||
|
||||
You can also add custom user attributes in Keycloak and map them directly:
|
||||
|
||||
1. In User → Attributes, add:
|
||||
- `temporal-permissions` = `["production:read", "development:write"]`
|
||||
|
||||
2. Create an attribute mapper that extracts this as the `permissions` claim
|
||||
|
||||
### Step 2: Update Your Temporal Server Configuration
|
||||
|
||||
Add JWT configuration to your Temporal config:
|
||||
|
||||
```yaml
|
||||
# config/development.yaml
|
||||
global:
|
||||
authorization:
|
||||
jwtKeyProvider:
|
||||
# JWKS endpoint from Keycloak
|
||||
keySourceURIs:
|
||||
- "https://your-keycloak.com/realms/your-realm/protocol/openid-connect/certs"
|
||||
refreshInterval: "1h"
|
||||
|
||||
# Claim name containing permissions (default: "permissions")
|
||||
permissionsClaimName: "permissions"
|
||||
```
|
||||
|
||||
### Step 3: Build Custom Temporal Server
|
||||
|
||||
See `custom-server/main.go` for the implementation. Key points:
|
||||
|
||||
```go
|
||||
temporal.WithClaimMapper(func(cfg *config.Config) authorization.ClaimMapper {
|
||||
return authorization.NewDefaultJWTClaimMapper(
|
||||
authorization.NewDefaultTokenKeyProvider(cfg, logger),
|
||||
cfg,
|
||||
logger,
|
||||
)
|
||||
})
|
||||
|
||||
temporal.WithAuthorizer(authorization.NewDefaultAuthorizer())
|
||||
```
|
||||
|
||||
### Step 4: Update Docker Compose
|
||||
|
||||
Replace the standard Temporal image with your custom build:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
temporal:
|
||||
build:
|
||||
context: ./custom-server
|
||||
dockerfile: Dockerfile
|
||||
# ... rest of config
|
||||
environment:
|
||||
- TEMPORAL_AUTH_ISSUER_URL=${TEMPORAL_AUTH_ISSUER_URL}
|
||||
# ... other env vars
|
||||
```
|
||||
|
||||
### Step 5: Configure Temporal UI to Pass JWT
|
||||
|
||||
The Temporal UI needs to forward the JWT token to the Temporal Server. This happens automatically when you configure the UI with OIDC - the UI will include the token in gRPC metadata under the `authorization` header.
|
||||
|
||||
Your existing UI configuration should work:
|
||||
```yaml
|
||||
environment:
|
||||
- TEMPORAL_AUTH_ENABLED=true
|
||||
- TEMPORAL_AUTH_PROVIDER_URL=${TEMPORAL_AUTH_PROVIDER_URL}
|
||||
- TEMPORAL_AUTH_ISSUER_URL=${TEMPORAL_AUTH_ISSUER_URL}
|
||||
- TEMPORAL_AUTH_CLIENT_ID=${TEMPORAL_AUTH_CLIENT_ID}
|
||||
- TEMPORAL_AUTH_CLIENT_SECRET=${TEMPORAL_AUTH_CLIENT_SECRET}
|
||||
- TEMPORAL_AUTH_CALLBACK_URL=${TEMPORAL_AUTH_CALLBACK_URL}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Test JWT Token
|
||||
|
||||
First, verify your Keycloak is issuing the right claims:
|
||||
|
||||
```bash
|
||||
# Get a token
|
||||
TOKEN=$(curl -X POST "https://your-keycloak.com/realms/your-realm/protocol/openid-connect/token" \
|
||||
-d "client_id=temporal-ui" \
|
||||
-d "client_secret=YOUR_SECRET" \
|
||||
-d "grant_type=password" \
|
||||
-d "username=testuser" \
|
||||
-d "password=testpass" | jq -r '.access_token')
|
||||
|
||||
# Decode and inspect
|
||||
echo $TOKEN | cut -d'.' -f2 | base64 -d | jq .
|
||||
```
|
||||
|
||||
Look for the `permissions` array in the output.
|
||||
|
||||
### 2. Test Authorization
|
||||
|
||||
1. Create a user in Keycloak with limited permissions (e.g., only `development:read`)
|
||||
2. Log in to Temporal UI with that user
|
||||
3. Try to:
|
||||
- View workflows in `development` namespace ✓ (should work)
|
||||
- Start a workflow in `development` namespace ✗ (should fail)
|
||||
- Access `production` namespace ✗ (should fail)
|
||||
|
||||
### 3. Check Logs
|
||||
|
||||
Enable debug logging to see authorization decisions:
|
||||
|
||||
```yaml
|
||||
# In dynamicconfig/development-sql.yaml
|
||||
frontend.enableDebugMode:
|
||||
- value: true
|
||||
```
|
||||
|
||||
## Common Permission Patterns
|
||||
|
||||
### Admin User
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"temporal-system:admin"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Developer
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"development:write",
|
||||
"staging:write",
|
||||
"production:read"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Production Operator
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"production:write"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Read-Only Viewer
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"development:read",
|
||||
"staging:read",
|
||||
"production:read"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Worker Service Account
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"production:worker",
|
||||
"development:worker"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced: Custom ClaimMapper
|
||||
|
||||
If you need more complex logic than the default JWT ClaimMapper provides (e.g., mapping based on multiple claims, using group hierarchies, etc.), you can implement a custom ClaimMapper:
|
||||
|
||||
```go
|
||||
type customClaimMapper struct {
|
||||
defaultMapper authorization.ClaimMapper
|
||||
}
|
||||
|
||||
func (c *customClaimMapper) GetClaims(authInfo *authorization.AuthInfo) (*authorization.Claims, error) {
|
||||
claims := &authorization.Claims{}
|
||||
|
||||
// Parse the JWT token
|
||||
token, err := jwt.Parse(authInfo.AuthToken, /* key function */)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract custom claims
|
||||
groups := token.Claims["groups"].([]interface{})
|
||||
email := token.Claims["email"].(string)
|
||||
|
||||
// Custom logic to map groups/email to Temporal roles
|
||||
claims.Namespaces = make(map[string]authorization.Role)
|
||||
|
||||
for _, group := range groups {
|
||||
switch group.(string) {
|
||||
case "engineering":
|
||||
claims.Namespaces["development"] = authorization.RoleWriter
|
||||
case "ops":
|
||||
claims.Namespaces["production"] = authorization.RoleWriter
|
||||
case "management":
|
||||
claims.System = authorization.RoleReader
|
||||
}
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "PermissionDenied" for all operations
|
||||
|
||||
**Solution**: Check that:
|
||||
1. JWT token contains the `permissions` claim
|
||||
2. JWKS URL is correct and accessible from Temporal server
|
||||
3. Token signature is valid
|
||||
4. ClaimMapper and Authorizer are both configured
|
||||
|
||||
### Issue: UI can authenticate but server rejects calls
|
||||
|
||||
**Solution**: Ensure the UI is passing the JWT token to the server:
|
||||
- The token must be in the `authorization` header as `Bearer <token>`
|
||||
- Check UI logs to see if token is being sent
|
||||
|
||||
### Issue: Permissions not being extracted
|
||||
|
||||
**Solution**:
|
||||
1. Verify the `permissionsClaimName` matches your JWT claim name
|
||||
2. Check that permissions are in the correct format: `"namespace:role"`
|
||||
3. Enable debug logging to see what claims are being extracted
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Use HTTPS**: Always use TLS for Keycloak and Temporal
|
||||
2. **Short Token Lifetime**: Set JWT expiration to 15-60 minutes
|
||||
3. **Rotate Keys**: Regularly rotate signing keys in Keycloak
|
||||
4. **Principle of Least Privilege**: Grant minimum necessary permissions
|
||||
5. **Audit Logging**: Enable Temporal's audit logging to track access
|
||||
6. **Service Accounts**: Use dedicated service accounts for workers with `worker` role only
|
||||
|
||||
## Alternative Approaches
|
||||
|
||||
### Reverse Proxy Authorization
|
||||
|
||||
If building a custom Temporal server is too complex, you can implement authorization at the reverse proxy level:
|
||||
|
||||
1. Use a proxy (e.g., Nginx, Traefik, Envoy) in front of Temporal
|
||||
2. Validate JWT and extract claims in the proxy
|
||||
3. Block requests based on claims before they reach Temporal
|
||||
4. More limited but easier to implement
|
||||
|
||||
### Custom Auth Service
|
||||
|
||||
Create a separate auth service that:
|
||||
1. Validates tokens
|
||||
2. Checks permissions
|
||||
3. Issues short-lived Temporal-specific tokens
|
||||
4. Acts as a gateway between UI and Temporal server
|
||||
|
||||
## References
|
||||
|
||||
- [Temporal Security Documentation](https://docs.temporal.io/self-hosted-guide/security)
|
||||
- [Authorizer Sample](https://github.com/temporalio/samples-server/tree/main/extensibility/authorizer)
|
||||
- [Keycloak Protocol Mappers](https://www.keycloak.org/docs/latest/server_admin/#_protocol-mappers)
|
||||
- [JWT.io](https://jwt.io) - Tool for inspecting JWT tokens
|
||||
|
||||
## Need Help?
|
||||
|
||||
Common issues and where to get help:
|
||||
- Temporal Community Forum: https://community.temporal.io/
|
||||
- Temporal Slack: https://temporal.io/slack
|
||||
- GitHub Issues: https://github.com/temporalio/temporal/issues
|
||||
413
DIAGRAMS.md
Normal file
413
DIAGRAMS.md
Normal file
@ -0,0 +1,413 @@
|
||||
# Temporal Authorization Flow Diagrams
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Your Organization │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Users │────────▶│ Keycloak │───────▶│ Temporal UI │ │
|
||||
│ │ │ Login │ (OIDC) │ JWT │ │ │
|
||||
│ └──────────┘ └──────────────┘ └──────┬───────┘ │
|
||||
│ │ │ │
|
||||
│ │ JWT │ │
|
||||
│ │ with │ API │
|
||||
│ │ permissions │ Calls │
|
||||
│ ▼ │ │
|
||||
│ ┌─────────────┐ │ │
|
||||
│ │ JWKS │ │ │
|
||||
│ │ Public Keys │ │ │
|
||||
│ └─────────────┘ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ Temporal Server│ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────┐ │ │
|
||||
│ │ │ ClaimMapper│ │ │
|
||||
│ │ │ (extracts) │ │ │
|
||||
│ │ └─────┬──────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌─────▼──────┐ │ │
|
||||
│ │ │ Authorizer │ │ │
|
||||
│ │ │ (allow/ │ │ │
|
||||
│ │ │ deny) │ │ │
|
||||
│ │ └────────────┘ │ │
|
||||
│ └────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Detailed Request Flow
|
||||
|
||||
### Step 1: User Authentication
|
||||
|
||||
```
|
||||
┌────────┐ ┌──────────┐
|
||||
│ User │ │ Keycloak │
|
||||
└───┬────┘ └────┬─────┘
|
||||
│ │
|
||||
│ 1. Navigate to Temporal UI │
|
||||
│─────────────────────────────────────────▶ │
|
||||
│ │
|
||||
│ 2. Redirect to Keycloak login │
|
||||
│◀───────────────────────────────────────── │
|
||||
│ │
|
||||
│ 3. Enter credentials │
|
||||
│─────────────────────────────────────────▶ │
|
||||
│ │
|
||||
│ 4. Check user groups │
|
||||
│ (e.g., dev-team) │
|
||||
│ │
|
||||
│ 5. Generate JWT with permissions │
|
||||
│ { │
|
||||
│ "groups": ["dev-team"], │
|
||||
│ "permissions": [ │
|
||||
│ "development:write", │
|
||||
│ "staging:write", │
|
||||
│ "production:read" │
|
||||
│ ] │
|
||||
│ } │
|
||||
│◀───────────────────────────────────────── │
|
||||
│ │
|
||||
│ 6. Redirect back to UI with JWT │
|
||||
│─────────────────────────────────────────▶ │
|
||||
│ │
|
||||
```
|
||||
|
||||
### Step 2: API Request Authorization
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────────┐ ┌────────────┐
|
||||
│ Temporal UI │ │ Temporal Server │ │ Database │
|
||||
└──────┬──────┘ └────────┬────────┘ └─────┬──────┘
|
||||
│ │ │
|
||||
│ 1. API Request │ │
|
||||
│ Authorization: │ │
|
||||
│ Bearer <JWT> │ │
|
||||
│─────────────────────────▶│ │
|
||||
│ │ │
|
||||
│ │ 2. Extract JWT │
|
||||
│ │ from header │
|
||||
│ │ │
|
||||
│ │ 3. Verify JWT │
|
||||
│ │ signature │
|
||||
│ │ (using JWKS) │
|
||||
│ │ │
|
||||
│ │ 4. ClaimMapper │
|
||||
│ │ extracts │
|
||||
│ │ permissions │
|
||||
│ │ │
|
||||
│ │ 5. Authorizer │
|
||||
│ │ checks if │
|
||||
│ │ operation │
|
||||
│ │ allowed │
|
||||
│ │ │
|
||||
│ │ Target: │
|
||||
│ │ - Namespace: dev │
|
||||
│ │ - API: StartWF │
|
||||
│ │ │
|
||||
│ │ Claims: │
|
||||
│ │ - dev:write ✓ │
|
||||
│ │ │
|
||||
│ │ 6. Execute request │
|
||||
│ │────────────────────────▶
|
||||
│ │ │
|
||||
│ │ 7. Return data │
|
||||
│ │◀────────────────────────
|
||||
│ │ │
|
||||
│ 8. Return response │ │
|
||||
│◀─────────────────────────│ │
|
||||
│ │ │
|
||||
```
|
||||
|
||||
### Step 3: Authorization Decision Tree
|
||||
|
||||
```
|
||||
Incoming API Request
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Extract JWT Token │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Verify Signature │
|
||||
│ (using JWKS) │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌──────────▼───────────┐
|
||||
│ Valid Signature? │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌──────────┴───────────┐
|
||||
│ │
|
||||
❌ NO ✅ YES
|
||||
│ │
|
||||
▼ ▼
|
||||
Return 401 ┌────────────────┐
|
||||
Unauthorized │ ClaimMapper │
|
||||
│ Extract perms │
|
||||
└────────┬───────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Get target: │
|
||||
│ - Namespace │
|
||||
│ - Operation │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────────────┐
|
||||
│ Authorizer: │
|
||||
│ Check if claims allow │
|
||||
│ operation on namespace │
|
||||
└────────┬────────────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Authorized? │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
│ │
|
||||
❌ NO ✅ YES
|
||||
│ │
|
||||
▼ ▼
|
||||
Return 403 Execute API
|
||||
Permission Denied Return Result
|
||||
```
|
||||
|
||||
## Permission Matching Examples
|
||||
|
||||
### Example 1: Developer accessing Development namespace
|
||||
|
||||
```
|
||||
User JWT:
|
||||
{
|
||||
"permissions": [
|
||||
"development:write",
|
||||
"staging:write",
|
||||
"production:read"
|
||||
]
|
||||
}
|
||||
|
||||
API Request:
|
||||
- Namespace: "development"
|
||||
- Operation: "StartWorkflowExecution"
|
||||
- Required: write permission
|
||||
|
||||
Authorization Check:
|
||||
✅ "development:write" in permissions
|
||||
✅ write >= write required
|
||||
✅ ALLOW
|
||||
```
|
||||
|
||||
### Example 2: Developer accessing Production namespace
|
||||
|
||||
```
|
||||
User JWT:
|
||||
{
|
||||
"permissions": [
|
||||
"development:write",
|
||||
"staging:write",
|
||||
"production:read"
|
||||
]
|
||||
}
|
||||
|
||||
API Request:
|
||||
- Namespace: "production"
|
||||
- Operation: "StartWorkflowExecution"
|
||||
- Required: write permission
|
||||
|
||||
Authorization Check:
|
||||
✅ "production:read" in permissions
|
||||
❌ read < write required
|
||||
❌ DENY - PermissionDenied
|
||||
```
|
||||
|
||||
### Example 3: Viewer accessing any namespace
|
||||
|
||||
```
|
||||
User JWT:
|
||||
{
|
||||
"permissions": [
|
||||
"development:read",
|
||||
"staging:read",
|
||||
"production:read"
|
||||
]
|
||||
}
|
||||
|
||||
API Request:
|
||||
- Namespace: "production"
|
||||
- Operation: "DescribeWorkflowExecution"
|
||||
- Required: read permission
|
||||
|
||||
Authorization Check:
|
||||
✅ "production:read" in permissions
|
||||
✅ read >= read required
|
||||
✅ ALLOW
|
||||
```
|
||||
|
||||
### Example 4: Admin with system access
|
||||
|
||||
```
|
||||
User JWT:
|
||||
{
|
||||
"permissions": [
|
||||
"temporal-system:admin"
|
||||
]
|
||||
}
|
||||
|
||||
API Request:
|
||||
- Namespace: "any-namespace"
|
||||
- Operation: "any-operation"
|
||||
|
||||
Authorization Check:
|
||||
✅ "temporal-system:admin" grants all access
|
||||
✅ ALLOW
|
||||
```
|
||||
|
||||
## Group to Permission Mapping Flow
|
||||
|
||||
```
|
||||
Keycloak Groups Script Mapper JWT Permissions
|
||||
┌──────────────┐ ┌─────────────┐ ┌──────────────────┐
|
||||
│ temporal- │───────▶│ if group │──────────▶│ temporal-system │
|
||||
│ admins │ │ == admins │ │ :admin │
|
||||
└──────────────┘ └─────────────┘ └──────────────────┘
|
||||
|
||||
┌──────────────┐ ┌─────────────┐ ┌──────────────────┐
|
||||
│ dev-team │───────▶│ if group │──────────▶│ development:write│
|
||||
│ │ │ == dev │ │ staging:write │
|
||||
└──────────────┘ └─────────────┘ │ production:read │
|
||||
└──────────────────┘
|
||||
|
||||
┌──────────────┐ ┌─────────────┐ ┌──────────────────┐
|
||||
│ ops-team │───────▶│ if group │──────────▶│ production:admin │
|
||||
│ │ │ == ops │ │ staging:admin │
|
||||
└──────────────┘ └─────────────┘ └──────────────────┘
|
||||
|
||||
┌──────────────┐ ┌─────────────┐ ┌──────────────────┐
|
||||
│ viewers │───────▶│ if group │──────────▶│ development:read │
|
||||
│ │ │ == viewers │ │ staging:read │
|
||||
└──────────────┘ └─────────────┘ │ production:read │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## System Components
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Temporal Server │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ Frontend Service │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │
|
||||
│ │ │ ClaimMapper │ │ Authorizer │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ Input: │ │ Input: │ │ │
|
||||
│ │ │ - JWT token │ │ - Claims │ │ │
|
||||
│ │ │ │ │ - CallTarget │ │ │
|
||||
│ │ │ Process: │ │ │ │ │
|
||||
│ │ │ 1. Verify signature │ │ Process: │ │ │
|
||||
│ │ │ 2. Extract claims │ │ 1. Check namespace │ │ │
|
||||
│ │ │ 3. Parse perms │ │ 2. Check operation │ │ │
|
||||
│ │ │ │ │ 3. Compare with │ │ │
|
||||
│ │ │ Output: │ │ claims │ │ │
|
||||
│ │ │ - Claims object │──▶ │ │ │
|
||||
│ │ │ │ │ Output: │ │ │
|
||||
│ │ │ │ │ - Allow/Deny │ │ │
|
||||
│ │ └──────────────────────┘ └──────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ └──────────────────────────────────────┼─────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────────────────────▼─────────────────┐ │
|
||||
│ │ API Handler │ │
|
||||
│ │ (executes if allowed) │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
```
|
||||
Internet
|
||||
│
|
||||
│ HTTPS
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ Caddy Proxy │
|
||||
│ (TLS Term) │
|
||||
└────────┬───────┘
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────┐ ┌─────────────────┐
|
||||
│ Temporal UI │ │ Keycloak │
|
||||
│ (port 8080) │◀─────────▶│ (OIDC) │
|
||||
└───────┬───────┘ JWT └─────────────────┘
|
||||
│ │
|
||||
│ gRPC │ JWKS
|
||||
│ with JWT │
|
||||
▼ ▼
|
||||
┌───────────────────────┐ ┌────────────────┐
|
||||
│ Temporal Server │───│ Public Keys │
|
||||
│ (port 7233) │ └────────────────┘
|
||||
│ │
|
||||
│ - ClaimMapper │
|
||||
│ - Authorizer │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
│ SQL
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ PostgreSQL │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Data Flow Timeline
|
||||
|
||||
```
|
||||
Time User Keycloak Temporal UI Temporal Server
|
||||
──┼──────┼──────────────┼──────────────┼──────────────┼───────────
|
||||
0s│ Login │ │
|
||||
│──────────────────▶ │ │
|
||||
│ │ │
|
||||
1s│ Authenticate │ │
|
||||
│ │ │ │
|
||||
│ Verify │ │
|
||||
│ Generate JWT │ │
|
||||
│◀───────────────── │ │
|
||||
│ JWT Token │ │
|
||||
│ │ │
|
||||
2s│ Access UI │ │
|
||||
│───────────────────────────────────▶│ │
|
||||
│ │ │
|
||||
3s│ │ List Workflows
|
||||
│ │ (with JWT) │
|
||||
│ │─────────────▶│
|
||||
│ │ Verify JWT
|
||||
│ │ Extract perms
|
||||
│ │ Authorize
|
||||
│ │ Query DB
|
||||
│ │◀─────────────│
|
||||
│ │ Workflows │
|
||||
│◀───────────────────────────────────│ │
|
||||
│ Display │ │
|
||||
│ │ │
|
||||
4s│ Start Workflow │ │
|
||||
│───────────────────────────────────▶│ │
|
||||
│ │ StartWF │
|
||||
│ │ (with JWT) │
|
||||
│ │─────────────▶│
|
||||
│ │ Verify JWT
|
||||
│ │ Extract perms
|
||||
│ │ Check: write?
|
||||
│ │ ✓ Authorized
|
||||
│ │ Start WF
|
||||
│ │◀─────────────│
|
||||
│ │ Success │
|
||||
│◀───────────────────────────────────│ │
|
||||
│ Confirmation │ │
|
||||
```
|
||||
443
IMPLEMENTATION_CHECKLIST.md
Normal file
443
IMPLEMENTATION_CHECKLIST.md
Normal file
@ -0,0 +1,443 @@
|
||||
# Temporal Authorization Implementation Checklist
|
||||
|
||||
Use this checklist to implement authorization step-by-step.
|
||||
|
||||
## ✅ Phase 1: Understanding (15 mins)
|
||||
|
||||
- [ ] Read `README_AUTHORIZATION.md` (high-level overview)
|
||||
- [ ] Review `DIAGRAMS.md` (understand the flow)
|
||||
- [ ] Read `QUICK_REFERENCE.md` (understand permission format)
|
||||
- [ ] Understand the concept: Groups → Permissions → Allow/Deny
|
||||
|
||||
**Key Concept**: Users in Keycloak groups get permissions like `"namespace:role"` in their JWT, which Temporal uses to decide what they can do.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 2: Verify Current Setup (15 mins)
|
||||
|
||||
- [ ] Verify Temporal UI authentication is working
|
||||
- [ ] Test that you can login via Keycloak
|
||||
- [ ] Confirm `TEMPORAL_AUTH_ISSUER_URL` environment variable is set
|
||||
- [ ] Run test script: `./test-authorization.sh`
|
||||
- [ ] Note: JWT probably does NOT have `permissions` claim yet (that's OK)
|
||||
|
||||
**Expected Result**: You can login, but JWT doesn't have permissions yet.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 3: Configure Keycloak (30 mins)
|
||||
|
||||
### 3.1: Create Groups
|
||||
|
||||
Login to Keycloak Admin Console:
|
||||
|
||||
- [ ] Navigate to: Groups
|
||||
- [ ] Create group: `temporal-admins`
|
||||
- [ ] Create group: `dev-team`
|
||||
- [ ] Create group: `ops-team`
|
||||
- [ ] Create group: `viewers`
|
||||
|
||||
### 3.2: Add Protocol Mapper
|
||||
|
||||
- [ ] Navigate to: Clients → [your-temporal-ui-client]
|
||||
- [ ] Go to: Client Scopes tab
|
||||
- [ ] Select the client scope (usually named same as client)
|
||||
- [ ] Click: Mappers tab
|
||||
- [ ] Click: Add mapper → By configuration
|
||||
- [ ] Select: Script Mapper
|
||||
- [ ] Configure:
|
||||
- Name: `temporal-permissions`
|
||||
- Mapper Type: `Script Mapper`
|
||||
- Token Claim Name: `permissions`
|
||||
- Claim JSON Type: `JSON`
|
||||
- Add to ID token: `ON`
|
||||
- Add to access token: `ON`
|
||||
- Add to userinfo: `ON`
|
||||
- Multivalued: `ON`
|
||||
- Script: Copy from `custom-server/example-keycloak-mapper.json`
|
||||
- [ ] Click: Save
|
||||
|
||||
### 3.3: Customize Mapper (Optional)
|
||||
|
||||
Edit the script in the mapper to match your organization:
|
||||
|
||||
```javascript
|
||||
// Example: Map your groups to permissions
|
||||
if (groupName === 'your-admin-group') {
|
||||
permissions.add('temporal-system:admin');
|
||||
}
|
||||
else if (groupName === 'your-dev-group') {
|
||||
permissions.add('development:write');
|
||||
permissions.add('staging:write');
|
||||
}
|
||||
// ... etc
|
||||
```
|
||||
|
||||
- [ ] Customize group-to-permission mappings if needed
|
||||
- [ ] Save changes
|
||||
|
||||
### 3.4: Assign Users to Groups
|
||||
|
||||
- [ ] Navigate to: Users
|
||||
- [ ] Select a test user
|
||||
- [ ] Go to: Groups tab
|
||||
- [ ] Click: Join Group
|
||||
- [ ] Add user to `dev-team` (or another group)
|
||||
- [ ] Repeat for other test users
|
||||
|
||||
### 3.5: Verify JWT
|
||||
|
||||
- [ ] Run test script again: `./test-authorization.sh`
|
||||
- [ ] Verify JWT now contains `permissions` array
|
||||
- [ ] Verify permissions match expected format: `"namespace:role"`
|
||||
|
||||
**Expected Result**: JWT contains permissions like `["development:write", "staging:write"]`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 4: Prepare Custom Server (15 mins)
|
||||
|
||||
### 4.1: Review Configuration
|
||||
|
||||
- [ ] Review `custom-server/config/development.yaml`
|
||||
- [ ] Verify JWKS URL format: `https://your-keycloak.com/realms/your-realm/protocol/openid-connect/certs`
|
||||
- [ ] Update if your realm name is different
|
||||
|
||||
### 4.2: Review Server Code
|
||||
|
||||
- [ ] Review `custom-server/main.go`
|
||||
- [ ] Understand: It uses Temporal's default ClaimMapper and Authorizer
|
||||
- [ ] No code changes needed for basic setup
|
||||
|
||||
### 4.3: Create go.sum
|
||||
|
||||
```bash
|
||||
cd /srv/temporal/custom-server
|
||||
go mod download
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
- [ ] Run commands above
|
||||
- [ ] Verify `go.sum` is created
|
||||
- [ ] Check for any errors
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 5: Update Docker Compose (10 mins)
|
||||
|
||||
### 5.1: Backup Current Config
|
||||
|
||||
```bash
|
||||
cp compose.yaml compose.yaml.backup
|
||||
```
|
||||
|
||||
- [ ] Create backup
|
||||
|
||||
### 5.2: Update Temporal Service
|
||||
|
||||
Edit `compose.yaml`:
|
||||
|
||||
```yaml
|
||||
temporal:
|
||||
# OLD: image: temporalio/auto-setup:1.29.0
|
||||
# NEW:
|
||||
build:
|
||||
context: ./custom-server
|
||||
dockerfile: Dockerfile
|
||||
# ... rest stays the same
|
||||
```
|
||||
|
||||
- [ ] Change from `image:` to `build:`
|
||||
- [ ] Point to `./custom-server` directory
|
||||
|
||||
### 5.3: Add Environment Variable
|
||||
|
||||
Make sure this exists in temporal service:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
# ... existing vars
|
||||
- TEMPORAL_AUTH_ISSUER_URL=${TEMPORAL_AUTH_ISSUER_URL}
|
||||
```
|
||||
|
||||
- [ ] Verify `TEMPORAL_AUTH_ISSUER_URL` is in environment
|
||||
- [ ] Verify it's defined in your `.env` file
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 6: Build and Deploy (15 mins)
|
||||
|
||||
### 6.1: Build Custom Server
|
||||
|
||||
```bash
|
||||
cd /srv/temporal
|
||||
docker-compose build temporal
|
||||
```
|
||||
|
||||
- [ ] Run build command
|
||||
- [ ] Wait for build to complete (may take 5-10 mins first time)
|
||||
- [ ] Check for errors
|
||||
|
||||
### 6.2: Deploy
|
||||
|
||||
#### If using Docker Compose:
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### If using Docker Swarm:
|
||||
|
||||
```bash
|
||||
docker stack deploy temporal --detach=true -c compose.yaml
|
||||
```
|
||||
|
||||
- [ ] Deploy updated stack
|
||||
- [ ] Wait for services to start
|
||||
|
||||
### 6.3: Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
docker-compose logs temporal
|
||||
# OR
|
||||
docker service logs temporal_temporal
|
||||
|
||||
# Look for:
|
||||
# "Starting Temporal Server with JWT Authorization..."
|
||||
# "All services are started"
|
||||
```
|
||||
|
||||
- [ ] Check logs for startup
|
||||
- [ ] Verify no errors
|
||||
- [ ] Confirm server is running
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 7: Create Namespaces (5 mins)
|
||||
|
||||
Create namespaces that match your permission names:
|
||||
|
||||
```bash
|
||||
# As admin user (in temporal-admins group)
|
||||
docker exec -it <temporal-container> tctl namespace register development
|
||||
docker exec -it <temporal-container> tctl namespace register staging
|
||||
docker exec -it <temporal-container> tctl namespace register production
|
||||
```
|
||||
|
||||
- [ ] Create `development` namespace
|
||||
- [ ] Create `staging` namespace
|
||||
- [ ] Create `production` namespace
|
||||
- [ ] Create any other namespaces you need
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 8: Test Authorization (30 mins)
|
||||
|
||||
### 8.1: Test Admin User
|
||||
|
||||
- [ ] Login as user in `temporal-admins` group
|
||||
- [ ] Verify can access all namespaces
|
||||
- [ ] Verify can start workflows
|
||||
- [ ] Verify can terminate workflows
|
||||
- [ ] Verify can see all operations
|
||||
|
||||
### 8.2: Test Developer User
|
||||
|
||||
- [ ] Login as user in `dev-team` group
|
||||
- [ ] Verify can access `development` namespace
|
||||
- [ ] Verify can start workflow in `development` ✓
|
||||
- [ ] Verify can access `production` namespace (read-only)
|
||||
- [ ] Try to start workflow in `production` - should FAIL ✗
|
||||
- [ ] Verify error message: "PermissionDenied"
|
||||
|
||||
### 8.3: Test Viewer User
|
||||
|
||||
- [ ] Login as user in `viewers` group
|
||||
- [ ] Verify can view workflows in all namespaces
|
||||
- [ ] Try to start any workflow - should FAIL ✗
|
||||
- [ ] Try to terminate workflow - should FAIL ✗
|
||||
- [ ] Verify can only perform read operations
|
||||
|
||||
### 8.4: Test No-Access User
|
||||
|
||||
- [ ] Login as user NOT in any Temporal groups
|
||||
- [ ] Try to access any namespace
|
||||
- [ ] Should see: PermissionDenied or no data
|
||||
|
||||
### 8.5: Verify Logs
|
||||
|
||||
```bash
|
||||
# Check authorization decisions in logs
|
||||
docker-compose logs temporal | grep -i "authoriz"
|
||||
```
|
||||
|
||||
- [ ] Check logs show authorization checks
|
||||
- [ ] Look for Allow/Deny decisions
|
||||
- [ ] Verify permissions are being extracted from JWT
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 9: Production Hardening (Optional)
|
||||
|
||||
### 9.1: Security
|
||||
|
||||
- [ ] Use HTTPS for Keycloak (if not already)
|
||||
- [ ] Use HTTPS for Temporal UI (if not already)
|
||||
- [ ] Verify JWT tokens have short expiration (15-60 mins)
|
||||
- [ ] Set up key rotation in Keycloak
|
||||
- [ ] Review Temporal audit logging
|
||||
|
||||
### 9.2: Monitoring
|
||||
|
||||
- [ ] Add authorization metrics to monitoring
|
||||
- [ ] Set up alerts for frequent PermissionDenied errors
|
||||
- [ ] Monitor JWT validation failures
|
||||
|
||||
### 9.3: Documentation
|
||||
|
||||
- [ ] Document your permission scheme for team
|
||||
- [ ] Create onboarding guide for new users
|
||||
- [ ] Document which groups map to which permissions
|
||||
- [ ] Create troubleshooting guide
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 10: Maintenance
|
||||
|
||||
### 10.1: Adding New Users
|
||||
|
||||
- [ ] Add user to Keycloak
|
||||
- [ ] Assign to appropriate group
|
||||
- [ ] User can immediately login with correct permissions
|
||||
|
||||
### 10.2: Changing Permissions
|
||||
|
||||
- [ ] Move user to different group in Keycloak
|
||||
- [ ] User gets new permissions on next login
|
||||
- [ ] (JWT refresh may take up to token expiration time)
|
||||
|
||||
### 10.3: Adding New Namespaces
|
||||
|
||||
- [ ] Create namespace in Temporal
|
||||
- [ ] Update Keycloak mapper script to include new namespace
|
||||
- [ ] Users in appropriate groups get access
|
||||
|
||||
---
|
||||
|
||||
## ❌ Troubleshooting
|
||||
|
||||
If things go wrong, check:
|
||||
|
||||
### Issue: "PermissionDenied" for all users
|
||||
|
||||
**Check**:
|
||||
- [ ] JWT contains `permissions` claim (run `./test-authorization.sh`)
|
||||
- [ ] Permissions format is correct: `"namespace:role"`
|
||||
- [ ] JWKS URL is accessible from Temporal server
|
||||
- [ ] Temporal server logs show JWT validation success
|
||||
|
||||
**Fix**:
|
||||
- Review Keycloak mapper configuration
|
||||
- Verify `permissionsClaimName` in config matches JWT claim
|
||||
- Check network connectivity to Keycloak
|
||||
|
||||
### Issue: Server won't start
|
||||
|
||||
**Check**:
|
||||
- [ ] Docker build completed successfully
|
||||
- [ ] `go.mod` and `go.sum` exist
|
||||
- [ ] Configuration YAML is valid
|
||||
- [ ] Environment variables are set
|
||||
|
||||
**Fix**:
|
||||
- Check server logs: `docker logs <container>`
|
||||
- Verify YAML syntax: `yamllint custom-server/config/development.yaml`
|
||||
- Rebuild: `docker-compose build --no-cache temporal`
|
||||
|
||||
### Issue: JWT signature validation fails
|
||||
|
||||
**Check**:
|
||||
- [ ] JWKS endpoint is correct
|
||||
- [ ] JWKS endpoint is accessible from container
|
||||
- [ ] Token hasn't expired
|
||||
- [ ] Issuer matches expected value
|
||||
|
||||
**Fix**:
|
||||
- Test JWKS: `curl https://keycloak.com/realms/realm/protocol/openid-connect/certs`
|
||||
- Verify `TEMPORAL_AUTH_ISSUER_URL` environment variable
|
||||
- Check Temporal can reach Keycloak (network/firewall)
|
||||
|
||||
### Issue: UI can't connect to server
|
||||
|
||||
**Check**:
|
||||
- [ ] Temporal server is running
|
||||
- [ ] No errors in server logs
|
||||
- [ ] UI can reach server (network)
|
||||
- [ ] JWT is being passed from UI to server
|
||||
|
||||
**Fix**:
|
||||
- Restart UI: `docker-compose restart ui`
|
||||
- Check UI logs for auth errors
|
||||
- Verify UI environment variables are correct
|
||||
|
||||
---
|
||||
|
||||
## 📊 Success Criteria
|
||||
|
||||
You'll know it's working when:
|
||||
|
||||
1. ✅ Different users see different things in Temporal UI
|
||||
2. ✅ Users in `dev-team` can manage dev namespace but not production
|
||||
3. ✅ Users in `viewers` can only view, not modify
|
||||
4. ✅ Users in `temporal-admins` can do everything
|
||||
5. ✅ Users not in any group are denied access
|
||||
6. ✅ Server logs show authorization checks happening
|
||||
7. ✅ "PermissionDenied" errors appear for unauthorized actions
|
||||
|
||||
---
|
||||
|
||||
## 📚 Reference
|
||||
|
||||
- **Full Guide**: `AUTHORIZATION_GUIDE.md`
|
||||
- **Quick Reference**: `QUICK_REFERENCE.md`
|
||||
- **Flow Diagrams**: `DIAGRAMS.md`
|
||||
- **Test Script**: `./test-authorization.sh`
|
||||
- **Alternative Approach**: `REVERSE_PROXY_APPROACH.md`
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ Time Tracking
|
||||
|
||||
- [ ] Phase 1: Understanding (15 mins)
|
||||
- [ ] Phase 2: Verify Setup (15 mins)
|
||||
- [ ] Phase 3: Configure Keycloak (30 mins)
|
||||
- [ ] Phase 4: Prepare Server (15 mins)
|
||||
- [ ] Phase 5: Update Compose (10 mins)
|
||||
- [ ] Phase 6: Build & Deploy (15 mins)
|
||||
- [ ] Phase 7: Create Namespaces (5 mins)
|
||||
- [ ] Phase 8: Test (30 mins)
|
||||
|
||||
**Total**: ~2.5 hours
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Current Status
|
||||
|
||||
Track your progress:
|
||||
|
||||
- [ ] Started implementation
|
||||
- [ ] Keycloak configured
|
||||
- [ ] Server built
|
||||
- [ ] Server deployed
|
||||
- [ ] Basic testing done
|
||||
- [ ] All test cases passing
|
||||
- [ ] Production ready
|
||||
- [ ] Documentation complete
|
||||
|
||||
Last updated: _______________
|
||||
|
||||
---
|
||||
|
||||
Good luck with your implementation! 🚀
|
||||
183
QUICK_REFERENCE.md
Normal file
183
QUICK_REFERENCE.md
Normal file
@ -0,0 +1,183 @@
|
||||
# Temporal Authorization Quick Reference
|
||||
|
||||
## TL;DR
|
||||
|
||||
You need to:
|
||||
1. Configure Keycloak to add a `permissions` claim to JWT tokens
|
||||
2. Build a custom Temporal server with ClaimMapper and Authorizer
|
||||
3. Deploy the custom server instead of the standard image
|
||||
|
||||
## Permission Format
|
||||
|
||||
```
|
||||
<namespace>:<role>
|
||||
```
|
||||
|
||||
Example: `"production:read"` = read-only access to production namespace
|
||||
|
||||
## Available Roles
|
||||
|
||||
| Role | Can View | Can Start/Signal | Can Terminate | Can Administer |
|
||||
|------|----------|------------------|---------------|----------------|
|
||||
| read | ✅ | ❌ | ❌ | ❌ |
|
||||
| write | ✅ | ✅ | ✅ | ❌ |
|
||||
| worker | ✅ | ✅ (as worker) | ❌ | ❌ |
|
||||
| admin | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
## Special Namespace
|
||||
|
||||
`temporal-system:admin` = Full cluster administrator access
|
||||
|
||||
## Keycloak Setup (One-Time)
|
||||
|
||||
### 1. Create Groups
|
||||
```
|
||||
temporal-admins → temporal-system:admin
|
||||
dev-team → development:write, staging:write, production:read
|
||||
ops-team → production:admin, staging:admin
|
||||
viewers → *:read
|
||||
```
|
||||
|
||||
### 2. Add Protocol Mapper
|
||||
|
||||
**Type**: Script Mapper
|
||||
**Claim Name**: `permissions`
|
||||
**Script**: See `custom-server/example-keycloak-mapper.json`
|
||||
|
||||
### 3. Assign Users to Groups
|
||||
|
||||
Users → [user] → Groups → Join
|
||||
|
||||
## Temporal Server Setup (One-Time)
|
||||
|
||||
### 1. Add JWT Configuration
|
||||
|
||||
Edit `custom-server/config/development.yaml`:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
authorization:
|
||||
jwtKeyProvider:
|
||||
keySourceURIs:
|
||||
- "https://your-keycloak.com/realms/your-realm/protocol/openid-connect/certs"
|
||||
refreshInterval: "1h"
|
||||
permissionsClaimName: "permissions"
|
||||
```
|
||||
|
||||
### 2. Build Custom Server
|
||||
|
||||
```bash
|
||||
cd /srv/temporal
|
||||
docker-compose build temporal
|
||||
docker-compose up -d temporal
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
### Test JWT Token
|
||||
```bash
|
||||
./test-authorization.sh
|
||||
```
|
||||
|
||||
Look for `permissions` array in output.
|
||||
|
||||
### Test Access
|
||||
1. Login as user in `dev-team`
|
||||
2. Access development namespace ✅
|
||||
3. Start workflow in development ✅
|
||||
4. Access production namespace ✅ (read-only)
|
||||
5. Start workflow in production ❌ (should fail)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Admin User
|
||||
```json
|
||||
{"permissions": ["temporal-system:admin"]}
|
||||
```
|
||||
|
||||
### Developer
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"development:write",
|
||||
"staging:write",
|
||||
"production:read"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Read-Only Viewer
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"development:read",
|
||||
"staging:read",
|
||||
"production:read"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Worker Service Account
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"production:worker",
|
||||
"staging:worker"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "PermissionDenied" Error
|
||||
1. Check JWT contains `permissions` claim
|
||||
2. Verify format: `"namespace:role"`
|
||||
3. Check JWKS URL is accessible
|
||||
4. Ensure namespace name matches exactly
|
||||
|
||||
### No `permissions` in JWT
|
||||
1. Check Keycloak mapper is configured
|
||||
2. Verify user is in a group
|
||||
3. Check mapper script logic
|
||||
4. Test token: `./test-authorization.sh`
|
||||
|
||||
### Server Won't Start
|
||||
1. Check logs: `docker logs temporal_temporal_1`
|
||||
2. Verify JWKS URL in config
|
||||
3. Run `go mod tidy` in custom-server directory
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `custom-server/main.go` | Custom server with auth |
|
||||
| `custom-server/config/development.yaml` | Temporal config with JWT |
|
||||
| `custom-server/example-keycloak-mapper.json` | Keycloak mapper config |
|
||||
| `test-authorization.sh` | Test JWT tokens |
|
||||
| `AUTHORIZATION_GUIDE.md` | Full documentation |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User → Keycloak → JWT with permissions → Temporal UI → Temporal Server
|
||||
↓
|
||||
ClaimMapper (extract)
|
||||
↓
|
||||
Authorizer (allow/deny)
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Configure Keycloak mapper
|
||||
2. ✅ Test JWT token format
|
||||
3. ✅ Build custom Temporal server
|
||||
4. ✅ Deploy and test
|
||||
5. ✅ Create namespaces matching permissions
|
||||
6. ✅ Test with different user groups
|
||||
|
||||
## Support
|
||||
|
||||
- Documentation: `/srv/temporal/AUTHORIZATION_GUIDE.md`
|
||||
- Temporal Docs: https://docs.temporal.io/self-hosted-guide/security
|
||||
- Sample Code: https://github.com/temporalio/samples-server/tree/main/extensibility/authorizer
|
||||
- Forum: https://community.temporal.io/
|
||||
232
README_AUTHORIZATION.md
Normal file
232
README_AUTHORIZATION.md
Normal file
@ -0,0 +1,232 @@
|
||||
# Implementation Summary
|
||||
|
||||
## What You Asked For
|
||||
|
||||
You want to restrict Temporal UI access so that only certain users (based on Keycloak groups/roles) can log in or view workflows, instead of everyone who can authenticate having full access.
|
||||
|
||||
## The Solution
|
||||
|
||||
Temporal provides an **Authorizer Plugin** system that works with your existing OIDC setup. Here's what you need to do:
|
||||
|
||||
### 1. Configure Keycloak (5 minutes)
|
||||
|
||||
Add a protocol mapper that transforms user groups into a `permissions` claim in the JWT token.
|
||||
|
||||
**File**: `custom-server/example-keycloak-mapper.json`
|
||||
|
||||
This mapper converts:
|
||||
- Keycloak group `temporal-admins` → Permission `temporal-system:admin`
|
||||
- Keycloak group `dev-team` → Permissions `development:write`, `staging:write`, `production:read`
|
||||
- etc.
|
||||
|
||||
### 2. Build Custom Temporal Server (10 minutes)
|
||||
|
||||
Replace the standard Temporal Docker image with a custom build that includes the Authorizer and ClaimMapper.
|
||||
|
||||
**Files**:
|
||||
- `custom-server/main.go` - Server with authorization
|
||||
- `custom-server/Dockerfile` - Docker build
|
||||
- `custom-server/config/development.yaml` - Configuration
|
||||
|
||||
The custom server:
|
||||
- Uses Temporal's built-in JWT ClaimMapper to extract permissions from tokens
|
||||
- Uses Temporal's default Authorizer to make allow/deny decisions
|
||||
- Validates JWT signatures using Keycloak's public keys (JWKS)
|
||||
|
||||
### 3. Deploy (5 minutes)
|
||||
|
||||
Update your Docker Compose to build and use the custom server, then redeploy.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
User logs in → Keycloak issues JWT → JWT includes permissions →
|
||||
UI sends JWT with API calls → Server validates JWT →
|
||||
Server extracts permissions → Server checks if operation allowed →
|
||||
Allow or Deny
|
||||
```
|
||||
|
||||
## Key Files Created
|
||||
|
||||
1. **`AUTHORIZATION_GUIDE.md`** - Complete implementation guide
|
||||
2. **`QUICK_REFERENCE.md`** - Quick lookup for common tasks
|
||||
3. **`custom-server/`** - Custom Temporal server code
|
||||
4. **`test-authorization.sh`** - Script to test your JWT tokens
|
||||
5. **`DIAGRAMS.md`** - Visual flow diagrams
|
||||
6. **`REVERSE_PROXY_APPROACH.md`** - Alternative simpler approach
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Phase 1: Test Your Current Setup (15 minutes)
|
||||
|
||||
```bash
|
||||
# Test your JWT tokens contain the right claims
|
||||
./test-authorization.sh
|
||||
```
|
||||
|
||||
This will show you what's currently in your JWT tokens from Keycloak.
|
||||
|
||||
### Phase 2: Configure Keycloak (15 minutes)
|
||||
|
||||
1. Create groups in Keycloak (e.g., `temporal-admins`, `dev-team`, `ops-team`)
|
||||
2. Add the protocol mapper from `custom-server/example-keycloak-mapper.json`
|
||||
3. Assign users to groups
|
||||
4. Re-run the test script to verify permissions appear in JWT
|
||||
|
||||
### Phase 3: Deploy Custom Server (30 minutes)
|
||||
|
||||
```bash
|
||||
# Build the custom server
|
||||
cd /srv/temporal
|
||||
docker-compose build temporal
|
||||
|
||||
# Deploy
|
||||
docker stack deploy temporal --detach=true -c compose.yaml
|
||||
# OR
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Phase 4: Test Authorization (15 minutes)
|
||||
|
||||
1. Login as user in `dev-team` group
|
||||
2. Create namespace: `development`
|
||||
3. Try to start workflow - should work
|
||||
4. Login as user in `viewers` group
|
||||
5. Try to start workflow - should fail with PermissionDenied
|
||||
|
||||
## Permission Examples
|
||||
|
||||
### Full Admin
|
||||
```json
|
||||
{"permissions": ["temporal-system:admin"]}
|
||||
```
|
||||
Can do everything in all namespaces.
|
||||
|
||||
### Developer
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"development:write",
|
||||
"staging:write",
|
||||
"production:read"
|
||||
]
|
||||
}
|
||||
```
|
||||
Can manage dev/staging, but only view production.
|
||||
|
||||
### Viewer
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"development:read",
|
||||
"production:read"
|
||||
]
|
||||
}
|
||||
```
|
||||
Can only view workflows, cannot start or modify.
|
||||
|
||||
### Worker Service Account
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"production:worker"
|
||||
]
|
||||
}
|
||||
```
|
||||
Can execute workflows but not manage them via UI.
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Issue: "PermissionDenied" for everyone
|
||||
**Solution**: JWT doesn't contain `permissions` claim. Run `./test-authorization.sh` to verify.
|
||||
|
||||
### Issue: Server won't start
|
||||
**Solution**: Check JWKS URL is accessible from container. Verify config syntax.
|
||||
|
||||
### Issue: Permissions not working
|
||||
**Solution**: Ensure namespace names in permissions match your actual namespace names exactly.
|
||||
|
||||
## Architecture Comparison
|
||||
|
||||
### Before (Current)
|
||||
```
|
||||
User → Keycloak → Temporal UI → Temporal Server
|
||||
(auth only) (no authorization)
|
||||
↓
|
||||
Everyone has full access
|
||||
```
|
||||
|
||||
### After (With Authorization)
|
||||
```
|
||||
User → Keycloak → JWT with permissions → Temporal UI → Temporal Server
|
||||
↓
|
||||
ClaimMapper
|
||||
↓
|
||||
Authorizer
|
||||
↓
|
||||
Allow/Deny based on
|
||||
permissions in JWT
|
||||
```
|
||||
|
||||
## Customization Options
|
||||
|
||||
### Option 1: Use Default JWT ClaimMapper (Recommended)
|
||||
- Simplest approach
|
||||
- Keycloak maps groups to permissions format
|
||||
- Temporal extracts and validates
|
||||
- **Best for**: Standard use cases
|
||||
|
||||
### Option 2: Custom ClaimMapper
|
||||
- Write Go code to extract claims
|
||||
- More flexible logic (e.g., email domain-based)
|
||||
- **Best for**: Complex authorization rules
|
||||
|
||||
### Option 3: Reverse Proxy
|
||||
- Validate JWT at proxy level (Caddy/Nginx)
|
||||
- No custom Temporal build
|
||||
- Less granular control
|
||||
- **Best for**: Simple "can access or not" logic
|
||||
|
||||
## Production Checklist
|
||||
|
||||
- [ ] Configure Keycloak groups and protocol mapper
|
||||
- [ ] Test JWT tokens contain correct permissions
|
||||
- [ ] Build and test custom Temporal server locally
|
||||
- [ ] Create Temporal namespaces matching permission names
|
||||
- [ ] Update deployment configuration
|
||||
- [ ] Deploy to production
|
||||
- [ ] Test with different user groups
|
||||
- [ ] Monitor authorization logs
|
||||
- [ ] Document your permission scheme for team
|
||||
|
||||
## Support Resources
|
||||
|
||||
- **Full Guide**: `AUTHORIZATION_GUIDE.md` (comprehensive documentation)
|
||||
- **Quick Ref**: `QUICK_REFERENCE.md` (cheat sheet)
|
||||
- **Test Script**: `./test-authorization.sh` (verify JWT)
|
||||
- **Diagrams**: `DIAGRAMS.md` (visual flows)
|
||||
|
||||
- **Temporal Docs**: https://docs.temporal.io/self-hosted-guide/security
|
||||
- **Sample Code**: https://github.com/temporalio/samples-server/tree/main/extensibility/authorizer
|
||||
- **Forum**: https://community.temporal.io/
|
||||
|
||||
## Estimated Time
|
||||
|
||||
- **Reading/Understanding**: 30 minutes
|
||||
- **Keycloak Configuration**: 15 minutes
|
||||
- **Building Custom Server**: 15 minutes
|
||||
- **Deployment**: 15 minutes
|
||||
- **Testing**: 15 minutes
|
||||
|
||||
**Total**: ~90 minutes for full implementation
|
||||
|
||||
## Questions?
|
||||
|
||||
All the details are in `AUTHORIZATION_GUIDE.md`. Start there if you need step-by-step instructions.
|
||||
|
||||
The key insight: Temporal's authorization system is designed for exactly your use case - restricting access based on OIDC groups/roles. You just need to:
|
||||
1. Map groups to permissions in Keycloak
|
||||
2. Build a server that validates those permissions
|
||||
3. Deploy it
|
||||
|
||||
Good luck! 🚀
|
||||
225
REVERSE_PROXY_APPROACH.md
Normal file
225
REVERSE_PROXY_APPROACH.md
Normal file
@ -0,0 +1,225 @@
|
||||
# Alternative Approach: Reverse Proxy Authorization
|
||||
|
||||
If building a custom Temporal server seems too complex, you can implement authorization at the reverse proxy level using Caddy, which you're already using.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Browser → Keycloak → Temporal UI → Caddy (validates JWT) → Temporal Server
|
||||
↓
|
||||
(blocks if unauthorized)
|
||||
```
|
||||
|
||||
## Pros and Cons
|
||||
|
||||
### Pros ✅
|
||||
- Simpler to implement - no custom Temporal server build
|
||||
- Centralized authorization logic in one place
|
||||
- Can protect any backend service, not just Temporal
|
||||
- Easier to debug and modify
|
||||
|
||||
### Cons ❌
|
||||
- Less granular control (can't enforce per-namespace/per-operation)
|
||||
- All-or-nothing access (either user can access Temporal, or they can't)
|
||||
- Harder to implement complex authorization logic
|
||||
- Can't leverage Temporal's built-in authorization features
|
||||
|
||||
## Implementation with Caddy
|
||||
|
||||
### Option 1: JWT Validation Plugin
|
||||
|
||||
Use Caddy with the JWT plugin to validate tokens:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
order jwtauth before basicauth
|
||||
}
|
||||
|
||||
# Temporal UI endpoint
|
||||
{$DOMAIN} {
|
||||
# Validate JWT on all Temporal UI requests
|
||||
jwtauth {
|
||||
# Your Keycloak JWKS endpoint
|
||||
jwks_url https://your-keycloak.com/realms/your-realm/protocol/openid-connect/certs
|
||||
|
||||
# Required claims
|
||||
claim_check sub exists
|
||||
claim_check groups contains temporal-admins temporal-users
|
||||
|
||||
# Token location
|
||||
token_source header Authorization
|
||||
strip_prefix Bearer
|
||||
}
|
||||
|
||||
reverse_proxy temporal-ui:8080
|
||||
}
|
||||
|
||||
# Temporal Server gRPC endpoint (if exposed)
|
||||
{$DOMAIN}:7233 {
|
||||
jwtauth {
|
||||
jwks_url https://your-keycloak.com/realms/your-realm/protocol/openid-connect/certs
|
||||
claim_check groups contains temporal-admins temporal-users temporal-workers
|
||||
token_source metadata authorization
|
||||
}
|
||||
|
||||
reverse_proxy h2c://temporal:7233
|
||||
}
|
||||
```
|
||||
|
||||
Install the JWT plugin:
|
||||
```bash
|
||||
caddy add-package github.com/ggicci/caddy-jwt
|
||||
```
|
||||
|
||||
### Option 2: Forward Auth to External Service
|
||||
|
||||
Create a simple auth service that validates the JWT and checks groups:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yaml
|
||||
services:
|
||||
auth-service:
|
||||
image: your-auth-service
|
||||
environment:
|
||||
- JWKS_URL=https://your-keycloak.com/realms/your-realm/protocol/openid-connect/certs
|
||||
- ALLOWED_GROUPS=temporal-admins,temporal-users
|
||||
networks:
|
||||
- proxy
|
||||
```
|
||||
|
||||
```caddyfile
|
||||
{$DOMAIN} {
|
||||
forward_auth auth-service:8080 {
|
||||
uri /verify
|
||||
copy_headers Authorization
|
||||
}
|
||||
|
||||
reverse_proxy temporal-ui:8080
|
||||
}
|
||||
```
|
||||
|
||||
Auth service code (Python example):
|
||||
|
||||
```python
|
||||
from flask import Flask, request, jsonify
|
||||
import jwt
|
||||
import requests
|
||||
from functools import lru_cache
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_jwks():
|
||||
response = requests.get(os.getenv('JWKS_URL'))
|
||||
return response.json()
|
||||
|
||||
@app.route('/verify', methods=['GET', 'POST'])
|
||||
def verify():
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return jsonify({'error': 'Missing token'}), 401
|
||||
|
||||
token = auth_header.split(' ')[1]
|
||||
|
||||
try:
|
||||
# Verify token
|
||||
jwks = get_jwks()
|
||||
# ... JWT verification logic
|
||||
|
||||
# Check groups
|
||||
groups = decoded_token.get('groups', [])
|
||||
allowed_groups = os.getenv('ALLOWED_GROUPS', '').split(',')
|
||||
|
||||
if not any(g in allowed_groups for g in groups):
|
||||
return jsonify({'error': 'Insufficient permissions'}), 403
|
||||
|
||||
return jsonify({'status': 'ok'}), 200
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({'error': 'Invalid token'}), 401
|
||||
```
|
||||
|
||||
### Option 3: Nginx with JWT Module
|
||||
|
||||
If you prefer Nginx, use the nginx-jwt module:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name temporal.example.com;
|
||||
|
||||
# JWT validation
|
||||
set $jwt_auth_key "https://your-keycloak.com/realms/your-realm/protocol/openid-connect/certs";
|
||||
set $jwt_auth_validate_groups "temporal-admins,temporal-users";
|
||||
|
||||
location / {
|
||||
auth_jwt "Temporal";
|
||||
auth_jwt_key_file $jwt_auth_key;
|
||||
auth_jwt_require_claim groups ~ "temporal-admins|temporal-users";
|
||||
|
||||
proxy_pass http://temporal-ui:8080;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
With the reverse proxy approach:
|
||||
|
||||
1. **No per-namespace control** - You can't easily restrict users to specific namespaces
|
||||
2. **No per-operation control** - Can't allow reads but deny writes
|
||||
3. **UI-focused** - Harder to protect direct gRPC access to Temporal server
|
||||
4. **Group-based only** - Can't implement complex logic based on multiple claims
|
||||
|
||||
## When to Use This Approach
|
||||
|
||||
Use reverse proxy authorization when:
|
||||
- You only need to control who can access Temporal UI (yes/no)
|
||||
- You don't need granular per-namespace permissions
|
||||
- You want a simpler setup
|
||||
- Your authorization logic is straightforward (group membership)
|
||||
|
||||
Use the custom Temporal server approach when:
|
||||
- You need per-namespace access control
|
||||
- You want to restrict specific operations (e.g., read-only users)
|
||||
- You need complex authorization logic
|
||||
- You want to leverage Temporal's built-in authorization framework
|
||||
|
||||
## Hybrid Approach
|
||||
|
||||
You can also combine both approaches:
|
||||
|
||||
1. Use reverse proxy for initial access control (who can reach Temporal UI)
|
||||
2. Use Temporal's built-in authorization for granular namespace/operation control
|
||||
|
||||
This provides defense in depth and is recommended for production.
|
||||
|
||||
## Example: Simple Caddy Configuration
|
||||
|
||||
```caddyfile
|
||||
# Allow only users in specific groups to access Temporal UI
|
||||
{$DOMAIN} {
|
||||
@has_auth header Authorization *
|
||||
@no_auth not header Authorization *
|
||||
|
||||
# Redirect to Keycloak if no auth
|
||||
handle @no_auth {
|
||||
redir https://your-keycloak.com/realms/your-realm/protocol/openid-connect/auth?client_id=temporal-ui&redirect_uri=https://{$DOMAIN}/auth/sso/callback&response_type=code 302
|
||||
}
|
||||
|
||||
# Forward authenticated requests
|
||||
handle @has_auth {
|
||||
reverse_proxy temporal-ui:8080
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is the simplest approach but provides minimal security as it relies entirely on the Temporal UI's OIDC authentication without validating the JWT at the proxy level.
|
||||
|
||||
## Recommendation
|
||||
|
||||
For your use case (restricting access based on Keycloak groups), I recommend:
|
||||
|
||||
**Start with**: Custom Temporal Server approach (more complex but more powerful)
|
||||
**Fall back to**: Reverse proxy approach if the custom server is too difficult
|
||||
|
||||
The custom server approach gives you the most flexibility and aligns with Temporal's recommended security architecture.
|
||||
28
custom-server/Dockerfile
Normal file
28
custom-server/Dockerfile
Normal file
@ -0,0 +1,28 @@
|
||||
# Multi-stage build for custom Temporal server with authorization
|
||||
FROM golang:1.21 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum* ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY main.go ./
|
||||
|
||||
# Build the server
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o temporal-server main.go
|
||||
|
||||
# Final stage
|
||||
FROM temporalio/auto-setup:1.29.0
|
||||
|
||||
# Copy the custom server binary
|
||||
COPY --from=builder /app/temporal-server /usr/local/bin/temporal-server
|
||||
|
||||
# Copy configuration
|
||||
COPY config/ /etc/temporal/config/
|
||||
|
||||
# The auto-setup image's entrypoint will handle initialization
|
||||
# We'll override the command to use our custom binary
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["temporal-server", "start"]
|
||||
271
custom-server/README.md
Normal file
271
custom-server/README.md
Normal file
@ -0,0 +1,271 @@
|
||||
# Custom Temporal Server with OIDC Authorization
|
||||
|
||||
This directory contains the code to build a custom Temporal server with JWT-based authorization support for your Keycloak/OIDC setup.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Update your Docker Compose
|
||||
|
||||
Edit your `compose.yaml` to build and use the custom server:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
temporal:
|
||||
build:
|
||||
context: ./custom-server
|
||||
dockerfile: Dockerfile
|
||||
depends_on:
|
||||
- db
|
||||
configs:
|
||||
- source: entrypoint
|
||||
target: /entrypoint.sh
|
||||
mode: 0555
|
||||
- source: dynamicconfig
|
||||
target: /etc/temporal/config/dynamicconfig/development-sql.yaml
|
||||
entrypoint: /entrypoint.sh
|
||||
command: "autosetup"
|
||||
environment:
|
||||
- DB=postgres12
|
||||
- DB_PORT=5432
|
||||
- POSTGRES_USER=temporal
|
||||
- POSTGRES_PWD_FILE=/run/secrets/db_password
|
||||
- POSTGRES_SEEDS=db
|
||||
- DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml
|
||||
# Add this new environment variable for JWKS endpoint
|
||||
- TEMPORAL_AUTH_ISSUER_URL=${TEMPORAL_AUTH_ISSUER_URL}
|
||||
networks:
|
||||
- internal
|
||||
secrets:
|
||||
- db_password
|
||||
```
|
||||
|
||||
### 2. Set Environment Variables
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```bash
|
||||
# Your Keycloak realm issuer URL (without .well-known/openid-configuration)
|
||||
TEMPORAL_AUTH_ISSUER_URL=https://your-keycloak.com/realms/your-realm
|
||||
```
|
||||
|
||||
### 3. Configure Keycloak
|
||||
|
||||
#### Import the Protocol Mapper
|
||||
|
||||
1. Go to Keycloak Admin Console
|
||||
2. Navigate to: Clients → [your temporal-ui client] → Client scopes → [client-scope] → Mappers
|
||||
3. Click "Add mapper" → "By configuration" → "Script Mapper"
|
||||
4. Import settings from `example-keycloak-mapper.json` or create manually
|
||||
|
||||
#### Create Groups
|
||||
|
||||
Create the following groups in Keycloak (or customize the mapper script):
|
||||
|
||||
- `temporal-admins` - Full system access
|
||||
- `dev-team` - Development namespace access
|
||||
- `ops-team` - Production namespace access
|
||||
- `qa-team` - Read-only access
|
||||
- `temporal-workers` - Worker service accounts
|
||||
|
||||
#### Assign Users to Groups
|
||||
|
||||
1. Go to Users → [user] → Groups
|
||||
2. Add users to appropriate groups
|
||||
|
||||
### 4. Build and Deploy
|
||||
|
||||
```bash
|
||||
# Build the custom server
|
||||
docker-compose build temporal
|
||||
|
||||
# Deploy
|
||||
docker stack deploy temporal --detach=true -c compose.yaml
|
||||
|
||||
# Or with docker-compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 5. Test
|
||||
|
||||
1. Log in to Temporal UI as a user in the `dev-team` group
|
||||
2. Try to access the `development` namespace - should work
|
||||
3. Try to start a workflow - should work (write access)
|
||||
4. Try to access `production` namespace - should only be able to view (read access)
|
||||
|
||||
Log in as a user in the `qa-team` group:
|
||||
1. Should only be able to view workflows
|
||||
2. Should not be able to start or terminate workflows
|
||||
|
||||
## Customization
|
||||
|
||||
### Modify Permission Mappings
|
||||
|
||||
Edit the script in `example-keycloak-mapper.json` to match your organization's structure. The format is:
|
||||
|
||||
```javascript
|
||||
permissions.add('<namespace>:<role>');
|
||||
```
|
||||
|
||||
Where:
|
||||
- `<namespace>` is your Temporal namespace name
|
||||
- `<role>` is one of: `read`, `write`, `worker`, `admin`
|
||||
|
||||
Examples:
|
||||
```javascript
|
||||
permissions.add('production:admin'); // Full access to production
|
||||
permissions.add('development:write'); // Read and write to development
|
||||
permissions.add('staging:read'); // Read-only access to staging
|
||||
permissions.add('temporal-system:admin'); // Cluster admin
|
||||
```
|
||||
|
||||
### Use Different Claim Names
|
||||
|
||||
If you want to use a different claim name instead of `permissions`, edit `config/development.yaml`:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
authorization:
|
||||
permissionsClaimName: "temporal_perms" # Use this claim from JWT
|
||||
```
|
||||
|
||||
### Advanced: Custom ClaimMapper
|
||||
|
||||
If you need more complex logic (like mapping based on email domains, user attributes, or multiple claims), you can implement a custom ClaimMapper.
|
||||
|
||||
Create a new file `custom-server/custom_claim_mapper.go`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"go.temporal.io/server/common/authorization"
|
||||
)
|
||||
|
||||
type customClaimMapper struct{}
|
||||
|
||||
func newCustomClaimMapper() authorization.ClaimMapper {
|
||||
return &customClaimMapper{}
|
||||
}
|
||||
|
||||
func (c *customClaimMapper) GetClaims(authInfo *authorization.AuthInfo) (*authorization.Claims, error) {
|
||||
claims := &authorization.Claims{}
|
||||
|
||||
if authInfo.AuthToken == "" {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// Parse JWT - simple example, use a proper JWT library in production
|
||||
parts := strings.Split(authInfo.AuthToken, ".")
|
||||
if len(parts) != 3 {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// Decode payload (you should use a proper JWT library)
|
||||
// This is just for illustration
|
||||
var payload map[string]interface{}
|
||||
// ... decode base64 and unmarshal JSON
|
||||
|
||||
// Extract groups from JWT
|
||||
groups, ok := payload["groups"].([]interface{})
|
||||
if !ok {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// Map groups to Temporal roles
|
||||
claims.Namespaces = make(map[string]authorization.Role)
|
||||
|
||||
for _, g := range groups {
|
||||
group := g.(string)
|
||||
switch group {
|
||||
case "temporal-admins":
|
||||
claims.System = authorization.RoleAdmin
|
||||
case "dev-team":
|
||||
claims.Namespaces["development"] = authorization.RoleWriter
|
||||
claims.Namespaces["staging"] = authorization.RoleWriter
|
||||
claims.Namespaces["production"] = authorization.RoleReader
|
||||
case "ops-team":
|
||||
claims.Namespaces["production"] = authorization.RoleAdmin
|
||||
}
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
```
|
||||
|
||||
Then update `main.go` to use it:
|
||||
|
||||
```go
|
||||
temporal.WithClaimMapper(func(cfg *config.Config) authorization.ClaimMapper {
|
||||
return newCustomClaimMapper()
|
||||
})
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
Add to your environment variables:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
### Check JWT Token
|
||||
|
||||
Use this to decode and inspect your JWT token:
|
||||
|
||||
```bash
|
||||
# Get token from browser (F12 → Network → find request with Authorization header)
|
||||
TOKEN="your.jwt.token"
|
||||
|
||||
# Decode (install jq first: apt-get install jq)
|
||||
echo $TOKEN | cut -d'.' -f2 | base64 -d | jq .
|
||||
```
|
||||
|
||||
Look for the `permissions` array.
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Error: "PermissionDenied"**
|
||||
- Check that the JWT contains the `permissions` claim
|
||||
- Verify the permissions are in the correct format: `"namespace:role"`
|
||||
- Ensure the JWKS URL is accessible from the Temporal server container
|
||||
|
||||
**Error: "InvalidToken" or "TokenExpired"**
|
||||
- Check that `TEMPORAL_AUTH_ISSUER_URL` is correct
|
||||
- Verify the JWKS endpoint is reachable: `https://your-keycloak.com/realms/your-realm/protocol/openid-connect/certs`
|
||||
- Ensure token hasn't expired (check `exp` claim)
|
||||
|
||||
**Server won't start**
|
||||
- Check server logs: `docker logs temporal_temporal_1`
|
||||
- Verify go.mod dependencies: `cd custom-server && go mod tidy`
|
||||
- Ensure config file syntax is correct: `yamllint config/development.yaml`
|
||||
|
||||
### View Server Logs
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker-compose logs -f temporal
|
||||
|
||||
# Docker Swarm
|
||||
docker service logs -f temporal_temporal
|
||||
```
|
||||
|
||||
## Files in This Directory
|
||||
|
||||
- `main.go` - Custom Temporal server entrypoint with authorization
|
||||
- `go.mod` - Go module dependencies
|
||||
- `config/development.yaml` - Temporal configuration with JWT settings
|
||||
- `Dockerfile` - Multi-stage build for custom server
|
||||
- `example-keycloak-mapper.json` - Keycloak protocol mapper configuration
|
||||
- `README.md` - This file
|
||||
|
||||
## References
|
||||
|
||||
- [Temporal Authorization Documentation](https://docs.temporal.io/self-hosted-guide/security#authorizer-plugin)
|
||||
- [Authorization Sample](https://github.com/temporalio/samples-server/tree/main/extensibility/authorizer)
|
||||
- [Keycloak Script Mappers](https://www.keycloak.org/docs/latest/server_development/#_script_providers)
|
||||
99
custom-server/config/development.yaml
Normal file
99
custom-server/config/development.yaml
Normal file
@ -0,0 +1,99 @@
|
||||
# Temporal Server Configuration with JWT Authorization
|
||||
|
||||
global:
|
||||
authorization:
|
||||
# JWT token validation settings
|
||||
jwtKeyProvider:
|
||||
# JWKS endpoint from your Keycloak - this provides the public keys
|
||||
# Format: https://your-keycloak.com/realms/yourRealm/protocol/openid-connect/certs
|
||||
keySourceURIs:
|
||||
- ${TEMPORAL_AUTH_ISSUER_URL}/.well-known/jwks.json
|
||||
# How often to refresh the keys (in Go duration format)
|
||||
refreshInterval: 1h
|
||||
|
||||
# The claim name in the JWT that contains permissions
|
||||
# Default is "permissions" but you can customize this based on your Keycloak setup
|
||||
permissionsClaimName: "permissions"
|
||||
|
||||
# Database configuration
|
||||
persistence:
|
||||
defaultStore: default
|
||||
visibilityStore: visibility
|
||||
numHistoryShards: 4
|
||||
datastores:
|
||||
default:
|
||||
sql:
|
||||
pluginName: "postgres12"
|
||||
databaseName: "temporal"
|
||||
connectAddr: "db:5432"
|
||||
connectProtocol: "tcp"
|
||||
user: "temporal"
|
||||
password: "${POSTGRES_PWD}"
|
||||
maxConns: 20
|
||||
maxIdleConns: 20
|
||||
maxConnLifetime: "1h"
|
||||
visibility:
|
||||
sql:
|
||||
pluginName: "postgres12"
|
||||
databaseName: "temporal_visibility"
|
||||
connectAddr: "db:5432"
|
||||
connectProtocol: "tcp"
|
||||
user: "temporal"
|
||||
password: "${POSTGRES_PWD}"
|
||||
maxConns: 10
|
||||
maxIdleConns: 10
|
||||
maxConnLifetime: "1h"
|
||||
|
||||
# Services configuration
|
||||
services:
|
||||
frontend:
|
||||
rpc:
|
||||
grpcPort: 7233
|
||||
membershipPort: 6933
|
||||
bindOnIP: "0.0.0.0"
|
||||
|
||||
matching:
|
||||
rpc:
|
||||
grpcPort: 7235
|
||||
membershipPort: 6935
|
||||
bindOnIP: "0.0.0.0"
|
||||
|
||||
history:
|
||||
rpc:
|
||||
grpcPort: 7234
|
||||
membershipPort: 6934
|
||||
bindOnIP: "0.0.0.0"
|
||||
|
||||
worker:
|
||||
rpc:
|
||||
grpcPort: 7239
|
||||
membershipPort: 6939
|
||||
bindOnIP: "0.0.0.0"
|
||||
|
||||
clusterMetadata:
|
||||
enableGlobalNamespace: false
|
||||
failoverVersionIncrement: 10
|
||||
masterClusterName: "active"
|
||||
currentClusterName: "active"
|
||||
clusterInformation:
|
||||
active:
|
||||
enabled: true
|
||||
initialFailoverVersion: 1
|
||||
rpcName: "frontend"
|
||||
rpcAddress: "127.0.0.1:7233"
|
||||
|
||||
dcRedirectionPolicy:
|
||||
policy: "noop"
|
||||
|
||||
archival:
|
||||
history:
|
||||
state: "disabled"
|
||||
visibility:
|
||||
state: "disabled"
|
||||
|
||||
publicClient:
|
||||
hostPort: "127.0.0.1:7233"
|
||||
|
||||
dynamicConfigClient:
|
||||
filepath: "/etc/temporal/config/dynamicconfig/development-sql.yaml"
|
||||
pollInterval: "10s"
|
||||
13
custom-server/example-keycloak-mapper.json
Normal file
13
custom-server/example-keycloak-mapper.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "temporal-permissions-mapper",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-script-based-protocol-mapper",
|
||||
"config": {
|
||||
"userinfo.token.claim": "true",
|
||||
"multivalued": "true",
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "permissions",
|
||||
"script": "/**\n * This script maps Keycloak groups to Temporal permissions.\n * \n * Temporal expects permissions in the format: 'namespace:role'\n * where role can be: read, write, worker, admin\n * \n * Customize the mapping below based on your Keycloak groups.\n */\n\nvar ArrayList = Java.type('java.util.ArrayList');\nvar permissions = new ArrayList();\n\n// Get user's groups\nvar groups = user.getGroups();\n\n// Iterate through groups and map to Temporal permissions\nfor each (var group in groups) {\n var groupName = group.getName();\n \n // System administrators - full access to everything\n if (groupName === 'temporal-admins' || groupName === '/temporal-admins') {\n permissions.add('temporal-system:admin');\n }\n \n // Development team - write access to dev/staging, read to production\n else if (groupName === 'dev-team' || groupName === '/dev-team') {\n permissions.add('development:write');\n permissions.add('staging:write');\n permissions.add('production:read');\n }\n \n // Operations team - full access to production\n else if (groupName === 'ops-team' || groupName === '/ops-team') {\n permissions.add('production:admin');\n permissions.add('staging:write');\n permissions.add('development:read');\n }\n \n // QA team - read access everywhere\n else if (groupName === 'qa-team' || groupName === '/qa-team') {\n permissions.add('development:read');\n permissions.add('staging:read');\n permissions.add('production:read');\n }\n \n // Service accounts for workers\n else if (groupName === 'temporal-workers' || groupName === '/temporal-workers') {\n permissions.add('production:worker');\n permissions.add('staging:worker');\n permissions.add('development:worker');\n }\n \n // Support team - read only access\n else if (groupName === 'support-team' || groupName === '/support-team') {\n permissions.add('production:read');\n }\n}\n\n// Return the permissions array\npermissions;"
|
||||
}
|
||||
}
|
||||
7
custom-server/go.mod
Normal file
7
custom-server/go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module custom-temporal-server
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
go.temporal.io/server v1.29.0
|
||||
)
|
||||
51
custom-server/main.go
Normal file
51
custom-server/main.go
Normal file
@ -0,0 +1,51 @@
|
||||
// Custom Temporal Server with JWT Authorization
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"go.temporal.io/server/common/authorization"
|
||||
"go.temporal.io/server/common/config"
|
||||
"go.temporal.io/server/temporal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load Temporal configuration
|
||||
cfg, err := config.LoadConfig("development", "./config", "")
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load config:", err)
|
||||
}
|
||||
|
||||
// Create Temporal server with authorization
|
||||
s, err := temporal.NewServer(
|
||||
temporal.ForServices(temporal.DefaultServices),
|
||||
temporal.WithConfig(cfg),
|
||||
temporal.InterruptOn(temporal.InterruptCh()),
|
||||
|
||||
// Configure JWT ClaimMapper
|
||||
temporal.WithClaimMapper(func(cfg *config.Config) authorization.ClaimMapper {
|
||||
return authorization.NewDefaultJWTClaimMapper(
|
||||
// Token key provider - fetches public keys from your OIDC provider
|
||||
authorization.NewDefaultTokenKeyProvider(cfg, log.Default()),
|
||||
cfg,
|
||||
log.Default(),
|
||||
)
|
||||
}),
|
||||
|
||||
// Configure Authorizer
|
||||
temporal.WithAuthorizer(authorization.NewDefaultAuthorizer()),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create server:", err)
|
||||
}
|
||||
|
||||
// Start the server
|
||||
log.Println("Starting Temporal Server with JWT Authorization...")
|
||||
err = s.Start()
|
||||
if err != nil {
|
||||
log.Fatal("Server failed:", err)
|
||||
}
|
||||
|
||||
log.Println("Server stopped.")
|
||||
}
|
||||
244
test-authorization.sh
Executable file
244
test-authorization.sh
Executable file
@ -0,0 +1,244 @@
|
||||
#!/bin/bash
|
||||
# Test script to verify Temporal authorization setup
|
||||
|
||||
set -e
|
||||
|
||||
echo "=================================="
|
||||
echo "Temporal Authorization Test Script"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo "ℹ $1"
|
||||
}
|
||||
|
||||
# Check if required tools are installed
|
||||
echo "Checking prerequisites..."
|
||||
command -v jq >/dev/null 2>&1 || { print_error "jq is not installed. Install with: apt-get install jq"; exit 1; }
|
||||
command -v curl >/dev/null 2>&1 || { print_error "curl is not installed."; exit 1; }
|
||||
print_success "Prerequisites OK"
|
||||
echo ""
|
||||
|
||||
# Configuration
|
||||
read -p "Enter Keycloak URL (e.g., https://keycloak.example.com): " KEYCLOAK_URL
|
||||
read -p "Enter Realm name: " REALM
|
||||
read -p "Enter Client ID (e.g., temporal-ui): " CLIENT_ID
|
||||
read -sp "Enter Client Secret: " CLIENT_SECRET
|
||||
echo ""
|
||||
read -p "Enter test username: " USERNAME
|
||||
read -sp "Enter test password: " PASSWORD
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
# Get token from Keycloak
|
||||
print_info "Requesting token from Keycloak..."
|
||||
TOKEN_ENDPOINT="${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token"
|
||||
|
||||
RESPONSE=$(curl -s -X POST "$TOKEN_ENDPOINT" \
|
||||
-d "client_id=${CLIENT_ID}" \
|
||||
-d "client_secret=${CLIENT_SECRET}" \
|
||||
-d "grant_type=password" \
|
||||
-d "username=${USERNAME}" \
|
||||
-d "password=${PASSWORD}")
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
print_error "Failed to connect to Keycloak"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for errors
|
||||
if echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
|
||||
print_error "Authentication failed:"
|
||||
echo "$RESPONSE" | jq '.error_description'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract access token
|
||||
ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token')
|
||||
|
||||
if [ "$ACCESS_TOKEN" == "null" ] || [ -z "$ACCESS_TOKEN" ]; then
|
||||
print_error "Failed to get access token"
|
||||
echo "$RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Successfully obtained access token"
|
||||
echo ""
|
||||
|
||||
# Decode and display JWT
|
||||
print_info "Decoding JWT token..."
|
||||
echo ""
|
||||
|
||||
# Split token into header, payload, signature
|
||||
IFS='.' read -ra TOKEN_PARTS <<< "$ACCESS_TOKEN"
|
||||
|
||||
# Decode header
|
||||
HEADER=$(echo "${TOKEN_PARTS[0]}" | base64 -d 2>/dev/null || echo "${TOKEN_PARTS[0]}" | base64 -d -i 2>/dev/null)
|
||||
echo "JWT Header:"
|
||||
echo "$HEADER" | jq '.' 2>/dev/null || echo "$HEADER"
|
||||
echo ""
|
||||
|
||||
# Decode payload
|
||||
PAYLOAD=$(echo "${TOKEN_PARTS[1]}" | base64 -d 2>/dev/null || echo "${TOKEN_PARTS[1]}" | base64 -d -i 2>/dev/null)
|
||||
echo "JWT Payload:"
|
||||
echo "$PAYLOAD" | jq '.' 2>/dev/null || echo "$PAYLOAD"
|
||||
echo ""
|
||||
|
||||
# Extract relevant claims
|
||||
print_info "Checking for Temporal authorization claims..."
|
||||
echo ""
|
||||
|
||||
# Check for permissions claim
|
||||
PERMISSIONS=$(echo "$PAYLOAD" | jq -r '.permissions // empty' 2>/dev/null)
|
||||
if [ ! -z "$PERMISSIONS" ] && [ "$PERMISSIONS" != "null" ]; then
|
||||
print_success "Found 'permissions' claim"
|
||||
echo "Permissions:"
|
||||
echo "$PAYLOAD" | jq '.permissions'
|
||||
echo ""
|
||||
else
|
||||
print_warning "No 'permissions' claim found in JWT"
|
||||
echo "Temporal requires a 'permissions' array in the JWT token."
|
||||
echo "Each permission should be in the format: 'namespace:role'"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check for groups claim
|
||||
GROUPS=$(echo "$PAYLOAD" | jq -r '.groups // empty' 2>/dev/null)
|
||||
if [ ! -z "$GROUPS" ] && [ "$GROUPS" != "null" ]; then
|
||||
print_info "Found 'groups' claim:"
|
||||
echo "$PAYLOAD" | jq '.groups'
|
||||
echo ""
|
||||
else
|
||||
print_warning "No 'groups' claim found"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check for roles claim
|
||||
ROLES=$(echo "$PAYLOAD" | jq -r '.roles // empty' 2>/dev/null)
|
||||
if [ ! -z "$ROLES" ] && [ "$ROLES" != "null" ]; then
|
||||
print_info "Found 'roles' claim:"
|
||||
echo "$PAYLOAD" | jq '.roles'
|
||||
echo ""
|
||||
else
|
||||
print_warning "No 'roles' claim found"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check token expiration
|
||||
EXP=$(echo "$PAYLOAD" | jq -r '.exp // empty' 2>/dev/null)
|
||||
if [ ! -z "$EXP" ]; then
|
||||
CURRENT_TIME=$(date +%s)
|
||||
TIME_LEFT=$((EXP - CURRENT_TIME))
|
||||
if [ $TIME_LEFT -gt 0 ]; then
|
||||
print_success "Token is valid (expires in $((TIME_LEFT / 60)) minutes)"
|
||||
else
|
||||
print_error "Token has expired!"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check issuer
|
||||
ISSUER=$(echo "$PAYLOAD" | jq -r '.iss // empty' 2>/dev/null)
|
||||
if [ ! -z "$ISSUER" ]; then
|
||||
print_info "Token issuer: $ISSUER"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Verify JWKS endpoint is accessible
|
||||
print_info "Verifying JWKS endpoint..."
|
||||
JWKS_URL="${ISSUER}/protocol/openid-connect/certs"
|
||||
JWKS_RESPONSE=$(curl -s "$JWKS_URL")
|
||||
if echo "$JWKS_RESPONSE" | jq -e '.keys' >/dev/null 2>&1; then
|
||||
KEY_COUNT=$(echo "$JWKS_RESPONSE" | jq '.keys | length')
|
||||
print_success "JWKS endpoint accessible ($KEY_COUNT keys available)"
|
||||
echo "JWKS URL: $JWKS_URL"
|
||||
else
|
||||
print_error "JWKS endpoint not accessible or invalid response"
|
||||
echo "URL: $JWKS_URL"
|
||||
echo "Response: $JWKS_RESPONSE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test Temporal Server connection (optional)
|
||||
read -p "Do you want to test connection to Temporal Server? (y/n): " TEST_TEMPORAL
|
||||
if [ "$TEST_TEMPORAL" == "y" ]; then
|
||||
read -p "Enter Temporal Server address (e.g., localhost:7233): " TEMPORAL_ADDR
|
||||
|
||||
print_info "Testing Temporal Server connection..."
|
||||
|
||||
# Try to list namespaces with the token
|
||||
# Note: This requires grpcurl to be installed
|
||||
if command -v grpcurl >/dev/null 2>&1; then
|
||||
RESULT=$(grpcurl -plaintext \
|
||||
-H "authorization: Bearer $ACCESS_TOKEN" \
|
||||
-d '{"pageSize": 10}' \
|
||||
"$TEMPORAL_ADDR" \
|
||||
temporal.api.workflowservice.v1.WorkflowService/ListNamespaces 2>&1)
|
||||
|
||||
if echo "$RESULT" | grep -q "PermissionDenied"; then
|
||||
print_warning "Server rejected request (PermissionDenied)"
|
||||
echo "This likely means authorization is working but the user lacks permissions."
|
||||
echo "Check that the 'permissions' claim in the JWT matches your Temporal namespaces."
|
||||
elif echo "$RESULT" | grep -q "namespaces"; then
|
||||
print_success "Successfully connected to Temporal Server!"
|
||||
echo "User has access to namespaces."
|
||||
else
|
||||
print_error "Unexpected response from Temporal Server:"
|
||||
echo "$RESULT"
|
||||
fi
|
||||
else
|
||||
print_warning "grpcurl not installed, skipping Temporal Server test"
|
||||
echo "Install grpcurl to test server connection: https://github.com/fullstorydev/grpcurl"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo "=================================="
|
||||
echo "Summary"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
if [ ! -z "$PERMISSIONS" ] && [ "$PERMISSIONS" != "null" ]; then
|
||||
print_success "JWT token contains 'permissions' claim - ready for Temporal authorization!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Verify the permissions match your Temporal namespace names"
|
||||
echo "2. Deploy the custom Temporal server with authorization enabled"
|
||||
echo "3. Test access with different user groups"
|
||||
else
|
||||
print_warning "JWT token does NOT contain 'permissions' claim"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Configure Keycloak protocol mapper to add 'permissions' claim"
|
||||
echo "2. Map user groups/roles to permissions in format: 'namespace:role'"
|
||||
echo "3. Re-run this test script to verify"
|
||||
echo ""
|
||||
echo "See: custom-server/example-keycloak-mapper.json for an example"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=================================="
|
||||
echo "Full JWT Token (for debugging):"
|
||||
echo "=================================="
|
||||
echo "$ACCESS_TOKEN"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user