Middleware & Auth
Cedar Authorization
New to acton-service?
Start with the homepage to understand what acton-service is, then explore Core Concepts for foundational explanations. See the Glossary for technical term definitions.
acton-service integrates AWS Cedar for declarative, policy-based authorization. Define who can do what with which resources using human-readable policy files.
What You'll Learn
- Policy-based access control with admin vs user roles
- Resource ownership patterns (users can only access their own documents)
- Custom path normalization for alphanumeric IDs
- Layered security with JWT authentication + Cedar authorization
- Optional Redis caching for sub-5ms policy decisions
Quick Start
cargo run --manifest-path=acton-service/Cargo.toml --example cedar-authz --features cedar-authz,cache
The example automatically creates configuration files in ~/.config/acton-service/cedar-authz-example/:
policies.cedar- Cedar policy definitionsjwt-public.pem- JWT public key for token validationconfig.toml- Service configuration
Server starts on http://localhost:8080
Optional: Enable Policy Decision Caching
For faster policy decisions (1-5ms instead of 10-50ms), start Redis:
docker run -d -p 6379:6379 redis:latest
Without Redis, policy evaluation is still perfectly usable at 10-50ms latency.
Testing Authorization
Step 1: Verify Health Endpoints (No Auth Required)
# Health check - should return 200 OK
curl http://localhost:8080/health
# Readiness check - should return 200 OK
curl http://localhost:8080/ready
Step 2: Test Without Authentication (Should Fail)
# Try to access documents without a token - should return 401 Unauthorized
curl http://localhost:8080/api/v1/documents
Step 3: Generate Test JWT Tokens
Install PyJWT for token generation:
# Create virtual environment and install PyJWT
uv venv .venv
source .venv/bin/activate
uv pip install pyjwt cryptography
Generate tokens with Python:
import jwt
from datetime import datetime, timedelta, UTC
# Read the JWT private key (included in examples/)
with open("acton-service/examples/jwt-private.pem", "r") as f:
private_key = f.read()
# Generate USER token (regular user)
user_payload = {
"sub": "user:123",
"username": "alice",
"email": "alice@example.com",
"roles": ["user"], # Regular user role
"perms": ["read:documents", "write:documents"],
"exp": int((datetime.now(UTC) + timedelta(hours=1)).timestamp()),
"iat": int(datetime.now(UTC).timestamp()),
"jti": "test-user-token"
}
user_token = jwt.encode(user_payload, private_key, algorithm="RS256")
print("USER TOKEN:")
print(user_token)
print()
# Generate ADMIN token (admin user)
admin_payload = {
"sub": "user:456",
"username": "bob",
"email": "bob@example.com",
"roles": ["user", "admin"], # Admin role
"perms": ["read:documents", "write:documents", "admin:all"],
"exp": int((datetime.now(UTC) + timedelta(hours=1)).timestamp()),
"iat": int(datetime.now(UTC).timestamp()),
"jti": "test-admin-token"
}
admin_token = jwt.encode(admin_payload, private_key, algorithm="RS256")
print("ADMIN TOKEN:")
print(admin_token)
Save the tokens for testing:
export USER_TOKEN="<paste-user-token-here>"
export ADMIN_TOKEN="<paste-admin-token-here>"
Step 4: Test Cedar Authorization Policies
Test 1: User can list documents ✅
curl -H "Authorization: Bearer $USER_TOKEN" \
http://localhost:8080/api/v1/documents
# Expected: 200 OK with documents array
# [{"id":"doc1","owner_id":"user123","title":"My Document",...},...]
Test 2: User CANNOT access admin endpoint ❌
curl -H "Authorization: Bearer $USER_TOKEN" \
http://localhost:8080/api/v1/admin/users
# Expected: 403 Forbidden
# {"error":"Access denied by policy","code":"FORBIDDEN","status":403}
Test 3: Admin CAN access admin endpoint ✅
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:8080/api/v1/admin/users
# Expected: 200 OK with users array
# [{"id":"user123","username":"alice","roles":["user"]},...]
Test 4: User can create documents ✅
curl -X POST \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id":"doc-new","owner_id":"user123","title":"New Document","content":"Test"}' \
http://localhost:8080/api/v1/documents
# Expected: 200 OK with created document
# {"id":"doc-new","owner_id":"user123",...}
Test 5: Get specific document (Ownership check)
curl -H "Authorization: Bearer $USER_TOKEN" \
http://localhost:8080/api/v1/documents/user123/doc1
# Expected: 200 OK if user:123 matches the user_id in path
Test 6: Update document (Owner only)
curl -X PUT \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id":"doc1","owner_id":"user123","title":"Updated","content":"New"}' \
http://localhost:8080/api/v1/documents/user123/doc1
# Expected: 200 OK if user owns the document
Test 7: Delete document (Owner or admin)
curl -X DELETE \
-H "Authorization: Bearer $USER_TOKEN" \
http://localhost:8080/api/v1/documents/user123/doc1
# Expected: 200 OK if user owns the document
Cedar Policy Explanation
The example policies demonstrate common authorization patterns:
1. Admin Override
permit(principal, action, resource)
when { principal.roles.contains("admin") };
Admins bypass all restrictions and can perform any action on any resource.
2. Resource Listing
permit(
principal,
action == Action::"GET /api/v1/documents",
resource
);
Any authenticated user can list documents. No ownership check required for browsing.
3. Ownership-based Access
permit(
principal,
action in [Action::"GET /api/v1/documents/{user_id}/{doc_id}", ...],
resource
)
when { principal.sub == resource.owner_id };
Users can only access documents they own. The owner_id attribute from the resource must match the principal's sub claim.
4. Forbid with Unless (Restrictive)
forbid(
principal,
action == Action::"GET /api/v1/admin/users",
resource
)
unless { principal.roles.contains("admin") };
Explicitly deny admin endpoints to non-admin users. More restrictive than permit-only policies.
How It Works
Request Flow
Client Request
↓
JWT Authentication (validates token, extracts claims)
↓
Cedar Authorization (evaluates policies)
↓
Your Handler
Cedar Evaluation Model
Cedar evaluates each request using four components:
Principal (who)
- Extracted from JWT claims:
sub,roles,perms,username,email - Represents the authenticated user or service making the request
Action (what)
- HTTP method + normalized path
- Examples:
GET /api/v1/documents/{user_id}/{doc_id},POST /api/v1/documents
Resource (which)
- Path parameters or request body attributes
- Examples:
owner_id,document_id,user_id
Context (when/where)
- Request metadata:
ip_address,timestamp,user_agent - Environmental factors for conditional policies
Decision Logic
- If any
forbidpolicy matches → Deny - Else if any
permitpolicy matches → Allow - Otherwise → Deny (default deny)
Caching (Optional)
Redis caching reduces policy evaluation latency from 10-50ms to 1-5ms:
- Cache key: Hash of principal, action, resource, context
- Default TTL: 5 minutes (configurable)
- Automatic invalidation on policy reload
- Significant performance improvement for high-traffic endpoints
Configuration Options
[cedar]
enabled = true # Enable/disable Cedar authorization
policy_path = "path/to/policies.cedar" # Path to policy file
hot_reload = false # [IN PROGRESS] Automatic policy file watching
hot_reload_interval_secs = 60 # [IN PROGRESS] Check interval for hot-reload
cache_enabled = true # Enable policy decision caching
cache_ttl_secs = 300 # Cache TTL in seconds
fail_open = false # true = allow on errors, false = deny on errors
Note: Automatic hot-reload is currently in progress. Use the manual reload endpoint (POST /admin/reload-policies) to reload policies without restarting the service.
Fail-Open vs Fail-Closed
Fail-Closed (Recommended for Production)
fail_open = false
- Deny requests if policy evaluation fails
- More secure - prevents accidental access during errors
- May cause downtime if policies are misconfigured
- Always use in production environments
Fail-Open (Development Only)
fail_open = true
- Allow requests if policy evaluation fails
- Less secure - grants access during errors
- Useful for debugging policy issues
- Never use in production
Custom Path Normalization
acton-service supports customizable path normalization to handle various ID formats:
use acton_service::middleware::CedarAuthzLayer;
let authz = CedarAuthzLayer::builder()
.policy_path("policies.cedar")
.path_normalizer(|path, method| {
// Custom normalization for alphanumeric IDs
let normalized = path
.replace(|c: char| c.is_alphanumeric(), "{id}");
format!("{} {}", method, normalized)
})
.build();
Common Patterns:
- UUID IDs:
/api/v1/documents/550e8400-e29b-41d4-a716-446655440000→/api/v1/documents/{id} - Numeric IDs:
/api/v1/users/12345→/api/v1/users/{id} - Slug IDs:
/api/v1/posts/my-blog-post→/api/v1/posts/{slug}
Troubleshooting
403 Forbidden
Symptom: All requests return 403 Forbidden
Possible Causes:
- Cedar is enabled but policies are too restrictive
- Policy file not found or invalid syntax
- JWT claims don't match policy conditions
- Default deny with no matching permit policies
Solutions:
- Check logs for Cedar evaluation details
- Verify policy file exists and is valid Cedar syntax
- Ensure JWT contains required claims (
roles,sub, etc.) - Set
fail_open = truetemporarily to debug (development only) - Add logging to see which policies are evaluated
500 Internal Server Error
Symptom: Requests return 500 errors
Possible Causes:
- Policy file syntax errors (invalid Cedar)
- Policy evaluation errors (missing attributes)
- Cache connection issues (if Redis enabled)
Solutions:
- Check logs for policy parsing errors
- Validate policy syntax with Cedar CLI tools
- Verify Redis is running (if cache enabled)
- Test with
cache_enabled = falseto isolate issue
Policy Not Reloading
Symptom: Policy changes don't take effect
Current Status: Automatic hot-reload is in progress. Policies must be reloaded manually.
Solutions:
- Use the manual reload endpoint:
POST /admin/reload-policies(requires admin role) - Restart the service to load updated policies
- Check file permissions on policy file (must be readable)
Future: Automatic file watching and hot-reload will be implemented soon.
Performance Tips
- Enable caching: Reduces latency by 90% (10-50ms → 1-5ms)
- Use simple policies: Complex conditions increase evaluation time
- Cache warm-up: First requests may be slower as cache populates
- Monitor cache hit rate: Aim for >80% hit rate in production
- Optimize policy order: Put most common permits first
- Use forbid sparingly: Permit-based policies are typically faster
Security Best Practices
- Always use fail-closed in production:
fail_open = false - Validate JWT properly: Use strong algorithms (RS256, ES256)
- Principle of least privilege: Only grant necessary permissions
- Audit policies regularly: Review and update policies quarterly
- Use forbid for sensitive operations: Explicit denials are safer than implicit
- Secure policy reload endpoint: Protect
/admin/reload-policieswith admin-only access - Secure policy files: Restrict file permissions (automatic hot-reload in progress)
- Test policy changes: Validate in staging before production deployment
- Monitor authorization decisions: Track allow/deny rates and investigate anomalies
- Version control policies: Track policy changes in git for audit trail
Integration Patterns
JWT + Cedar Layered Security
ServiceBuilder::new()
.with_routes(routes)
.with_middleware(|router| {
router
.layer(JwtAuth::new("secret")) // First: Authenticate
.layer(CedarAuthzLayer::new(config)) // Second: Authorize
})
.build()
JWT provides authentication (who you are), Cedar provides authorization (what you can do).
gRPC Support
Cedar works identically for gRPC services:
ServiceBuilder::new()
.with_grpc_services(grpc_services)
.with_middleware(|router| {
router.layer(CedarAuthzLayer::new(config))
})
.build()
Path normalization handles gRPC method names automatically.
Next Steps
- Add more policies: Extend the example with your use cases
- Integrate with database: Load resource attributes from DB
- Implement policy management API: CRUD operations for policies
- Add policy testing: Unit tests for Cedar policies
- Monitor policy decisions: Track allow/deny metrics
- Implement policy versioning: Deploy policies with rollback capability
References
- Cedar Policy Language Documentation
- Cedar Rust Crate Documentation
- JWT Authentication - Configure authentication
- Redis Caching - Enable policy decision caching