Skip to main content

MEMO-009: Topaz Local Authorizer Configuration

Purpose

This memo documents how to configure Topaz as a local authorizer for two critical scenarios:

  1. Development Iteration: Fast, lightweight authorization during local development
  2. Integration Testing: Realistic authorization testing in CI/CD pipelines

Topaz is part of Prism's local infrastructure layer - reusable components that provide production-like services without external dependencies. This follows our local-first testing philosophy.

Overview

Topaz by Aserto provides local authorization enforcement with:

  • Embedded directory service (users, groups, resources)
  • Policy engine (OPA/Rego)
  • gRPC and REST APIs
  • In-memory caching for <1ms decisions

Key Insight: Topaz runs as a local sidecar - no cloud dependencies, no network latency, fully reproducible.

┌──────────────────────────────────────────────────────────────┐
│ Local Development Stack │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Prism │──▶│ Topaz │ │ Dex │ │
│ │ Proxy │ │ (authz) │ │ (authn) │ │
│ │ :50051 │ │ :8282 │ │ :5556 │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ └────────────────┴──────────────────┘ │
│ All on localhost │
└──────────────────────────────────────────────────────────────┘

Local Infrastructure Layer

Topaz is one component of the local infrastructure layer:

ComponentPurposePortStatus
TopazAuthorization (policy engine)8282This memo
DexAuthentication (OIDC provider)5556ADR-046
VaultSecret management8200RFC-016
SignozObservability3301ADR-048

Design principle: Each component can run independently or as part of a composed stack.

Scenario 1: Development Iteration

Requirements

For local development, we need:

  • Fast startup (<1 second)
  • No external dependencies
  • Simple user/group setup
  • Policy hot-reload (no restart)
  • Clear error messages

Docker Compose Configuration

# docker-compose.local.yml
version: '3.8'

services:
topaz:
image: ghcr.io/aserto-dev/topaz:0.30.14
container_name: prism-topaz-local
ports:
- "8282:8282" # gRPC API (authorization)
- "8383:8383" # REST API (directory management)
- "8484:8484" # Console UI (http://localhost:8484)
volumes:
- ./topaz/config.local.yaml:/config/topaz-config.yaml:ro
- ./topaz/policies:/policies:ro
- ./topaz/data:/data
environment:
- TOPAZ_DB_PATH=/data/topaz.db
- TOPAZ_POLICY_ROOT=/policies
- TOPAZ_LOG_LEVEL=info
command: run -c /config/topaz-config.yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8383/health"]
interval: 5s
timeout: 3s
retries: 3

# Optional: Proxy that uses Topaz
prism-proxy:
build: ./proxy
container_name: prism-proxy-local
depends_on:
topaz:
condition: service_healthy
environment:
- TOPAZ_ENDPOINT=topaz:8282
- TOPAZ_ENABLED=true
- TOPAZ_FAIL_OPEN=true # Allow requests if Topaz unavailable (dev mode)
ports:
- "50051:50051"

Configuration File

topaz/config.local.yaml:

# Topaz configuration for local development
version: 2

# Logging
logger:
prod: false
log_level: info

# API configuration
api:
grpc:
listen_address: "0.0.0.0:8282"
connection_timeout: 5s
rest:
listen_address: "0.0.0.0:8383"
gateway:
listen_address: "0.0.0.0:8484"
http: true
read_timeout: 5s
write_timeout: 5s

# Directory configuration (embedded)
directory:
db:
type: sqlite
path: /data/topaz.db
seed_metadata: true

# Policy engine configuration
policy:
engine: opa
policy_root: /policies

# Edge configuration (sync with remote - disabled for local)
edge:
enabled: false # No cloud sync in local dev

# Decision logging (for debugging)
decision_logger:
type: self
config:
store_directory: /data/decisions

# Authorization configuration
authorizer:
grpc:
connection_timeout: 5s
needs:
- kind: policy
- kind: directory

Seed Data Setup

Topaz Directory Initialization - topaz/seed/bootstrap.sh:

#!/usr/bin/env bash
# Bootstrap Topaz directory with development users and permissions

set -euo pipefail

TOPAZ_REST="http://localhost:8383"

echo "🔐 Bootstrapping Topaz directory..."

# Wait for Topaz to be ready
until curl -s "$TOPAZ_REST/health" > /dev/null; do
echo "Waiting for Topaz..."
sleep 1
done

echo "✅ Topaz is ready"

# Create users
echo "👤 Creating users..."
curl -X POST "$TOPAZ_REST/api/v2/directory/objects" \
-H "Content-Type: application/json" \
-d '{
"object": {
"type": "user",
"id": "dev@local.prism",
"display_name": "Local Developer",
"properties": {
"email": "dev@local.prism",
"roles": ["developer"]
}
}
}'

curl -X POST "$TOPAZ_REST/api/v2/directory/objects" \
-H "Content-Type: application/json" \
-d '{
"object": {
"type": "user",
"id": "admin@local.prism",
"display_name": "Local Admin",
"properties": {
"email": "admin@local.prism",
"roles": ["admin"]
}
}
}'

# Create groups
echo "👥 Creating groups..."
curl -X POST "$TOPAZ_REST/api/v2/directory/objects" \
-H "Content-Type: application/json" \
-d '{
"object": {
"type": "group",
"id": "developers",
"display_name": "Developers"
}
}'

curl -X POST "$TOPAZ_REST/api/v2/directory/objects" \
-H "Content-Type: application/json" \
-d '{
"object": {
"type": "group",
"id": "admins",
"display_name": "Administrators"
}
}'

# Add users to groups
echo "🔗 Creating group memberships..."
curl -X POST "$TOPAZ_REST/api/v2/directory/relations" \
-H "Content-Type: application/json" \
-d '{
"relation": {
"object_type": "group",
"object_id": "developers",
"relation": "member",
"subject_type": "user",
"subject_id": "dev@local.prism"
}
}'

curl -X POST "$TOPAZ_REST/api/v2/directory/relations" \
-H "Content-Type: application/json" \
-d '{
"relation": {
"object_type": "group",
"object_id": "admins",
"relation": "member",
"subject_type": "user",
"subject_id": "admin@local.prism"
}
}'

# Create namespaces
echo "📦 Creating namespaces..."
curl -X POST "$TOPAZ_REST/api/v2/directory/objects" \
-H "Content-Type: application/json" \
-d '{
"object": {
"type": "namespace",
"id": "dev-playground",
"display_name": "Developer Playground",
"properties": {
"description": "Sandbox for local development"
}
}
}'

curl -X POST "$TOPAZ_REST/api/v2/directory/objects" \
-H "Content-Type: application/json" \
-d '{
"object": {
"type": "namespace",
"id": "test-namespace",
"display_name": "Test Namespace",
"properties": {
"description": "For integration tests"
}
}
}'

# Grant permissions
echo "🔑 Granting permissions..."
# Developers → dev-playground
curl -X POST "$TOPAZ_REST/api/v2/directory/relations" \
-H "Content-Type: application/json" \
-d '{
"relation": {
"object_type": "namespace",
"object_id": "dev-playground",
"relation": "developer",
"subject_type": "group",
"subject_id": "developers"
}
}'

# Admins → all namespaces
curl -X POST "$TOPAZ_REST/api/v2/directory/relations" \
-H "Content-Type: application/json" \
-d '{
"relation": {
"object_type": "namespace",
"object_id": "dev-playground",
"relation": "admin",
"subject_type": "group",
"subject_id": "admins"
}
}'

curl -X POST "$TOPAZ_REST/api/v2/directory/relations" \
-H "Content-Type: application/json" \
-d '{
"relation": {
"object_type": "namespace",
"object_id": "test-namespace",
"relation": "admin",
"subject_type": "group",
"subject_id": "admins"
}
}'

echo "✅ Topaz directory bootstrapped successfully!"
echo ""
echo "Test users created:"
echo " - dev@local.prism (developer role)"
echo " - admin@local.prism (admin role)"
echo ""
echo "Test namespaces created:"
echo " - dev-playground (developers can access)"
echo " - test-namespace (admins can access)"

Policy Files

topaz/policies/prism.rego - Main authorization policy:

package prism.authz

import future.keywords.contains
import future.keywords.if
import future.keywords.in

# Default deny
default allow = false

# Allow if user has permission via direct relationship
allow if {
input.permission in ["read", "write", "admin"]
has_permission(input.user, input.permission, input.resource)
}

# Check if user has permission on resource
has_permission(user, permission, resource) if {
# Parse resource (format: "namespace:dev-playground")
[resource_type, resource_id] := split(resource, ":")

# Query directory for user's permissions
user_permissions := directory_check(user, resource_type, resource_id)

# Check if permission is granted
permission in user_permissions
}

# Helper: Query Topaz directory for user permissions
directory_check(user, resource_type, resource_id) = permissions if {
# Get user's groups
user_groups := data.directory.user_groups[user]

# Collect all permissions from groups
permissions := {p |
some group in user_groups
relation := data.directory.relations[group][resource_type][resource_id]
p := permission_from_relation(relation)
}
}

# Map relationship to permission
permission_from_relation("viewer") = "read"
permission_from_relation("developer") = "read"
permission_from_relation("developer") = "write"
permission_from_relation("admin") = "read"
permission_from_relation("admin") = "write"
permission_from_relation("admin") = "admin"

# Development mode: Allow all if explicitly enabled
allow if {
input.mode == "development"
input.allow_all == true
}

topaz/policies/namespace_isolation.rego - Multi-tenancy enforcement:

package prism.authz.namespace

import future.keywords.if

# Namespace isolation: Users can only access namespaces they have explicit access to
violation[msg] if {
input.resource_type == "namespace"
not has_namespace_access(input.user, input.resource_id)
msg := sprintf("User %v does not have access to namespace %v", [input.user, input.resource_id])
}

# Check if user has access to namespace (via group membership)
has_namespace_access(user, namespace_id) if {
user_groups := data.directory.user_groups[user]
some group in user_groups
group_namespaces := data.directory.group_namespaces[group]
namespace_id in group_namespaces
}

Developer Workflow

Starting Topaz locally:

# Start Topaz
docker compose -f docker-compose.local.yml up -d topaz

# Wait for startup
docker compose -f docker-compose.local.yml logs -f topaz

# Bootstrap directory
bash topaz/seed/bootstrap.sh

# Verify setup
curl http://localhost:8383/api/v2/directory/objects?object_type=user | jq .

# Open console UI
open http://localhost:8484

Testing authorization from command line:

# Check if dev@local.prism can read dev-playground
curl -X POST http://localhost:8282/api/v2/authz/is \
-H "Content-Type: application/json" \
-d '{
"identity_context": {
"type": "IDENTITY_TYPE_SUB",
"identity": "dev@local.prism"
},
"resource_context": {
"object_type": "namespace",
"object_id": "dev-playground"
},
"policy_context": {
"path": "prism.authz",
"decisions": ["allowed"]
}
}' | jq .

# Expected output:
# {
# "decisions": {
# "allowed": true
# }
# }

Policy hot-reload (no restart required):

# Edit policy file
vi topaz/policies/prism.rego

# Policies are automatically reloaded by Topaz
# No restart needed!

# Verify policy change
curl http://localhost:8383/api/v2/policies | jq .

Scenario 2: Integration Testing

Requirements

For integration tests, we need:

  • Reproducible setup (same users/permissions every test run)
  • Fast teardown/reset (clean state between tests)
  • Parallel test execution (isolated Topaz instances)
  • CI/CD integration (GitHub Actions)

Test Container Setup

Using testcontainers for Go tests:

// tests/integration/topaz_test.go
package integration_test

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

func TestAuthorizationWithTopaz(t *testing.T) {
ctx := context.Background()

// Start Topaz container
topazContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "ghcr.io/aserto-dev/topaz:0.30.14",
ExposedPorts: []string{"8282/tcp", "8383/tcp"},
WaitingFor: wait.ForHTTP("/health").
WithPort("8383/tcp").
WithStartupTimeout(30 * time.Second),
Env: map[string]string{
"TOPAZ_DB_PATH": "/tmp/topaz.db",
"TOPAZ_POLICY_ROOT": "/policies",
},
Files: []testcontainers.ContainerFile{
{
HostFilePath: "../../topaz/config.test.yaml",
ContainerFilePath: "/config/topaz-config.yaml",
FileMode: 0644,
},
{
HostFilePath: "../../topaz/policies",
ContainerFilePath: "/policies",
FileMode: 0755,
},
},
Cmd: []string{"run", "-c", "/config/topaz-config.yaml"},
},
Started: true,
})
assert.NoError(t, err)
defer topazContainer.Terminate(ctx)

// Get Topaz endpoint
host, _ := topazContainer.Host(ctx)
port, _ := topazContainer.MappedPort(ctx, "8282")
topazEndpoint := fmt.Sprintf("%s:%s", host, port.Port())

// Bootstrap test data
restPort, _ := topazContainer.MappedPort(ctx, "8383")
bootstrapTopaz(t, host, restPort.Port())

// Run authorization tests
t.Run("DeveloperCanReadNamespace", func(t *testing.T) {
allowed := checkAuthorization(t, topazEndpoint, AuthzRequest{
User: "dev@local.prism",
Permission: "read",
Resource: "namespace:dev-playground",
})
assert.True(t, allowed, "Developer should be able to read dev-playground")
})

t.Run("DeveloperCannotAdminNamespace", func(t *testing.T) {
allowed := checkAuthorization(t, topazEndpoint, AuthzRequest{
User: "dev@local.prism",
Permission: "admin",
Resource: "namespace:dev-playground",
})
assert.False(t, allowed, "Developer should NOT be able to admin dev-playground")
})

t.Run("AdminCanAccessAllNamespaces", func(t *testing.T) {
allowed := checkAuthorization(t, topazEndpoint, AuthzRequest{
User: "admin@local.prism",
Permission: "admin",
Resource: "namespace:test-namespace",
})
assert.True(t, allowed, "Admin should have access to all namespaces")
})
}

func bootstrapTopaz(t *testing.T, host, port string) {
// Execute bootstrap script against container
restURL := fmt.Sprintf("http://%s:%s", host, port)

// Create test users
createUser(t, restURL, "dev@local.prism", "Local Developer")
createUser(t, restURL, "admin@local.prism", "Local Admin")

// Create groups
createGroup(t, restURL, "developers")
createGroup(t, restURL, "admins")

// Create relationships
addUserToGroup(t, restURL, "dev@local.prism", "developers")
addUserToGroup(t, restURL, "admin@local.prism", "admins")

// Create namespaces and permissions
createNamespace(t, restURL, "dev-playground")
grantPermission(t, restURL, "developers", "developer", "dev-playground")
grantPermission(t, restURL, "admins", "admin", "dev-playground")
}

CI/CD Configuration (GitHub Actions)

.github/workflows/integration-tests.yml:

name: Integration Tests with Topaz

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
integration-test:
runs-on: ubuntu-latest

services:
# Topaz service container
topaz:
image: ghcr.io/aserto-dev/topaz:0.30.14
ports:
- 8282:8282
- 8383:8383
options: >-
--health-cmd "curl -f http://localhost:8383/health"
--health-interval 10s
--health-timeout 5s
--health-retries 5
volumes:
- ${{ github.workspace }}/topaz/config.test.yaml:/config/topaz-config.yaml:ro
- ${{ github.workspace }}/topaz/policies:/policies:ro

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'

- name: Bootstrap Topaz directory
run: |
bash topaz/seed/bootstrap.sh
env:
TOPAZ_REST: http://localhost:8383

- name: Run integration tests
run: |
go test -v ./tests/integration/... -tags=integration
env:
TOPAZ_ENDPOINT: localhost:8282

- name: Dump Topaz logs on failure
if: failure()
run: |
docker logs ${{ job.services.topaz.id }}

Test Configuration

topaz/config.test.yaml (optimized for testing):

version: 2

logger:
prod: true
log_level: warn # Less verbose for tests

api:
grpc:
listen_address: "0.0.0.0:8282"
connection_timeout: 2s
rest:
listen_address: "0.0.0.0:8383"

directory:
db:
type: sqlite
path: ":memory:" # In-memory database for fast tests
seed_metadata: false

policy:
engine: opa
policy_root: /policies

edge:
enabled: false # No remote sync in tests

authorizer:
grpc:
connection_timeout: 2s
needs:
- kind: policy
- kind: directory

Performance Characteristics

Local Development

Startup Time:

  • Topaz container: ~2 seconds
  • Policy load: ~100ms
  • Directory bootstrap: ~500ms
  • Total: <3 seconds

Authorization Latency:

  • First check (cold): ~5ms
  • Subsequent checks (cached): <1ms
  • P99: <2ms

Resource Usage:

  • Memory: ~50 MB (idle), ~100 MB (active)
  • CPU: <1% (idle), ~5% (under load)

Integration Testing

Test Suite Performance (50 authorization tests):

  • Sequential execution: ~2 seconds
  • Parallel execution: ~500ms
  • Per-test overhead: <10ms

Container Lifecycle:

  • Startup: ~2 seconds
  • Teardown: <1 second
  • Total test time: <5 seconds (including container lifecycle)

Troubleshooting

Issue 1: Topaz Container Won't Start

Symptom: docker compose up fails with connection refused

Diagnosis:

# Check Topaz logs
docker compose logs topaz

# Common errors:
# - Port 8282 already in use
# - Config file not found
# - Policy files have syntax errors

Solution:

# Check port availability
lsof -i :8282

# Validate config file
docker run --rm -v $(pwd)/topaz:/config ghcr.io/aserto-dev/topaz:0.30.14 \
validate -c /config/config.local.yaml

# Validate policies
docker run --rm -v $(pwd)/topaz/policies:/policies \
openpolicyagent/opa:latest test /policies

Issue 2: Bootstrap Script Fails

Symptom: bootstrap.sh exits with "Topaz not ready"

Diagnosis:

# Check if Topaz is listening
curl -v http://localhost:8383/health

# Check Topaz startup logs
docker compose logs topaz | grep -i error

Solution:

# Increase wait time in bootstrap script
until curl -s "$TOPAZ_REST/health" > /dev/null; do
echo "Waiting for Topaz..."
sleep 2 # Increase from 1 to 2 seconds
done

# Or check specific endpoint
curl -f http://localhost:8383/api/v2/directory/objects || exit 1

Issue 3: Authorization Always Denied

Symptom: All authorization checks return allowed: false

Diagnosis:

# Check directory state
curl http://localhost:8383/api/v2/directory/objects | jq .

# Check relations
curl http://localhost:8383/api/v2/directory/relations | jq .

# Check policy evaluation
curl -X POST http://localhost:8282/api/v2/authz/is \
-H "Content-Type: application/json" \
-d '{...}' | jq .

Solution:

# Re-run bootstrap
bash topaz/seed/bootstrap.sh

# Verify user exists
curl http://localhost:8383/api/v2/directory/objects?object_type=user | \
jq '.results[] | select(.id=="dev@local.prism")'

# Verify relationships
curl http://localhost:8383/api/v2/directory/relations | \
jq '.results[] | select(.subject_id=="dev@local.prism")'

# Check policy syntax
docker run --rm -v $(pwd)/topaz/policies:/policies \
openpolicyagent/opa:latest test /policies -v

Issue 4: Policy Changes Not Applied

Symptom: Modified policies don't take effect

Solution:

# Topaz should auto-reload, but force reload:
docker compose restart topaz

# Or use policy API to reload
curl -X POST http://localhost:8383/api/v2/policies/reload

# Verify policy version
curl http://localhost:8383/api/v2/policies | jq '.policies[].version'

Integration with Pattern SDK

Patterns (formerly plugins) integrate with local Topaz using the authorization layer from RFC-019:

Pattern configuration (patterns/redis/config.local.yaml):

authz:
token:
enabled: false # Token validation disabled for local dev

topaz:
enabled: true
endpoint: "localhost:8282"
timeout: 2s
cache_ttl: 5s
tls:
enabled: false

audit:
enabled: true
destination: "stdout"

enforce: false # Log violations but don't block in dev mode

Pattern usage:

// patterns/redis/main.go
import "github.com/prism/pattern-sdk/authz"

func main() {
// Initialize authorizer with local Topaz
authzConfig := authz.Config{
Topaz: authz.TopazConfig{
Enabled: true,
Endpoint: "localhost:8282",
},
Enforce: false, // Dev mode: log but don't block
}

authorizer, _ := authz.NewAuthorizer(authzConfig)

// Use in pattern
pattern := &RedisPattern{
authz: authorizer,
}

// Authorization automatically enforced via gRPC interceptor
server := grpc.NewServer(
grpc.UnaryInterceptor(authz.UnaryServerInterceptor(authorizer)),
)
}

Comparison: Development vs Integration Testing vs Production

AspectDevelopmentIntegration TestingProduction
StartupDocker ComposetestcontainersKubernetes sidecar
DatabaseSQLite fileSQLite in-memoryPostgreSQL
Policy syncDisabled (local files)Disabled (local files)Enabled (Git + Aserto)
EnforcementWarn only (enforce: false)Strict (enforce: true)Strict (enforce: true)
Fail modeFail-open (allow if down)Fail-closed (deny if down)Fail-closed (deny if down)
Audit logsStdoutStdoutCentralized (gRPC)
UsersStatic seed dataStatic test dataDynamic (synced from OIDC)

Revision History

  • 2025-10-11: Updated terminology from "Plugin SDK" to "Pattern SDK" for consistency with RFC-022
  • 2025-10-09: Initial memo documenting Topaz as local authorizer for development and integration testing