Skip to content

AbduAllahGabbar/service

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Zitadel Role Service — README

A small, reusable Go package and HTTP service that provides role management and a Redis-based roles cache for use with Zitadel. This README explains how to include and use the package in other Go projects, how to run the included HTTP server (locally and with Docker), and provides examples for adding roles, assigning roles to users, and using the HasAnyRole helper.

Contents

  • Overview
  • Requirements
  • Environment variables
  • Quick start (library usage)
  • Example: create a Service and call APIs (Go code)
  • Example: call the HTTP endpoints with curl
  • Middleware and HasAnyRole usage example
  • Cache cleanup job (role removal) and job status
  • Docker / building the server
  • Notes, error handling and tips

Overview

This repository contains:

  • A small HTTP server (in cmd/server) exposing role management endpoints (/v1/*).
  • A pkg/zitadel HTTP client that talks to Zitadel management API.
  • A pkg/cache implementation using Redis to cache per-user role lists and run background jobs to remove a role from all caches.
  • pkg/service which composes the Zitadel client and cache and provides higher-level operations used by the HTTP handlers.
  • pkg/middleware providing RoleMiddleware and helper HasAnyRole.

You can use the library directly in your Go services (recommended) or run the HTTP server and call it over HTTP.


Requirements

  • Go 1.21+ (module-aware)
  • Redis server accessible to your application
  • A Zitadel management API (project id and service account token)

Environment variables

The server and library configuration comes from environment variables (see .env in repo for example). Important variables:

  • ZITADEL_DOMAIN — base URL of Zitadel management API (e.g. https://zitadel.example.com or http://localhost:8080).
  • SERVICE_ACCOUNT_TOKEN — service account token used to call Zitadel management API.
  • PROJECT_ID — Zitadel project id used when creating/assigning roles.
  • REDIS_ADDR — Redis host:port (default localhost:6379).
  • REDIS_PASSWORD — Redis password (if any).
  • REDIS_DB — Redis DB number (integer, default 0).
  • CACHE_TTL — default TTL for role cache entries (e.g. 300s).
  • PORT — port where HTTP server listens (default 3000).
  • REQUEST_TIMEOUT — HTTP client timeout for calls to Zitadel (default 8s).
  • RETRY_MAX — number of retries for Zitadel calls (default 3).
  • Circuit breaker settings: CB_INTERVAL, CB_TIMEOUT, optionally CBMaxRequests.

Do not commit real tokens to source control. Use environment files or a secret manager.


Quick start — using the package directly in your Go project

  1. Add the module to your go.mod (use the module path of this repo). Example:
go get github.com/AbduAllahGabbar/service@latest
  1. Initialize the dependencies and construct the service object. Example snippet showing the recommended wiring:
import (
    "context"
    "time"

    "github.com/redis/go-redis/v9"
    "github.com/AbduAllahGabbar/service/pkg/cache"
    "github.com/AbduAllahGabbar/service/pkg/config"
    "github.com/AbduAllahGabbar/service/pkg/service"
    "github.com/AbduAllahGabbar/service/pkg/zitadel"
)

func BuildService() (*service.Service, error) {
    cfg := config.LoadConfig()

    rdb := redis.NewClient(&redis.Options{
        Addr:     cfg.RedisAddr,
        Password: cfg.RedisPassword,
        DB:       cfg.RedisDB,
    })

    // optional: ping Redis to verify
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := rdb.Ping(ctx).Err(); err != nil {
        return nil, err
    }

    cacheImpl := cache.NewRedisCache(rdb, cfg.CacheTTL)
    zitadelClient := zitadel.NewHTTPClient(cfg.ZitadelBaseURL, cfg.ZitadelToken, cfg)
    svc := service.New(zitadelClient, cacheImpl, cfg.CacheTTL)
    return svc, nil
}

After svc is created you can call its methods to create roles, assign roles, query roles, etc.


Service API (useful methods)

These are the main Service methods you will use:

svc.CreateRoles(ctx, roles []zitadel.RoleInput) ([]string, error)
svc.AssignRolesToUser(ctx, userID string, roleIDs []string) error
svc.DeleteRole(ctx, roleID string) error // triggers async cache cleanup job
svc.RemoveRoleFromUser(ctx, roleID, userID string) error
svc.GetUserRoles(ctx, userID string) ([]string, error)
svc.InvalidateRoles(ctx, userID string) error
svc.StartRemoveRoleCleanup(ctx, role string) (jobID string, err error)
svc.GetCleanupJobStatus(ctx, jobID string) (*cache.CleanupJobStatus, error)

zitadel.RoleInput structure:

type RoleInput struct {
    Name string `json:"name"`
    Desc string `json:"desc,omitempty"`
}

Example: create a role and assign to a user (Go)

ctx := context.Background()


// Alternatively create many roles in bulk
roles := []zitadel.RoleInput{{Name: "viewer", Desc: "Read only"}, {Name: "editor", Desc: "Edit content"}}
keys, err := svc.CreateRoles(ctx, roles)
if err != nil {
    log.Fatalf("create roles bulk: %v", err)
}
log.Printf("bulk created keys: %v", keys)

// Assign multiple role keys to the user at once
if err := svc.AssignRolesToUser(ctx, "user-123", keys); err != nil {
    log.Fatalf("assign roles: %v", err)
}

Example: call HTTP endpoints with curl

If you prefer to run the bundled HTTP server (cmd/server), here are example requests (replace host:port and token/user ids):

Create role

curl -X POST http://localhost:8081/v1/roles \
  -H "Content-Type: application/json" \
  -d '{"name":"team:lead","desc":"Team lead"}'
# Response: {"role_id":"team:lead"}

Assign role to user

curl -X POST http://localhost:8081/v1/roles/assign \
  -H "Content-Type: application/json" \
  -d '{"role_id":"team:lead","user_id":"user-123"}'
# Response: {"ok": true}

Create roles in bulk

curl -X POST http://localhost:8081/v1/roles/batch \
  -H "Content-Type: application/json" \
  -d '[{"name":"viewer","desc":"read only"},{"name":"editor","desc":"edit"}]'
# Response: {"ok": true} or error

Assign many role keys to user

curl -X POST http://localhost:8081/v1/roles/assign/batch \
  -H "Content-Type: application/json" \
  -d '{"user_id":"user-123","role_ids":["viewer","editor"]}'
# Response: {"ok": true}

Delete role (triggers async cleanup job)

curl -X DELETE http://localhost:8081/v1/roles/team:lead
# Response: {"ok": true}

# or async remove request (explicit)
curl -X POST http://localhost:8081/v1/roles/remove/async \
  -H "Content-Type: application/json" \
  -d '{"role":"team:lead"}'
# Response: {"job_id":"<job id>"}

Check cleanup job status

curl http://localhost:8081/v1/jobs/<job id>
# Response: JSON with job status and counts

Remove role from a particular user's grants

curl -X DELETE http://localhost:8081/v1/roles/team:lead/users/user-123
# Response: {"ok": true}

Middleware & HasAnyRole example

The package exposes pkg/middleware with a RoleMiddleware(svc) which populates the Gin context with the resolved user ID and roles. Use it like this in your Gin routes:

r := gin.New()
// attach middleware to some routes that require role lookup
r.GET("/v1/me/profile", middleware.RoleMiddleware(svc), func(c *gin.Context) {
    rolesI, _ := c.Get(middleware.ContextRolesKey)
    userID, _ := c.Get(middleware.ContextUserIDKey)
    c.JSON(200, gin.H{"user": userID, "roles": rolesI})
})

HasAnyRole helper usage (to check authorization within your code):

userRoles := []string{"viewer", "editor"}
if middleware.HasAnyRole(userRoles, "admin", "editor") {
    // user has at least one of the required roles
}

HasAnyRole is a simple set membership helper that returns true if any of rolesToCheck is present in userRoles.


Cache cleanup job (how role removal works)

  • When DeleteRole is called in Zitadel, the service starts a background job (stored in Redis key job:roles_cleanup:<jobid>) that scans roles:* keys and removes the role from cached role lists.
  • The job updates its progress periodically and can be polled via svc.GetCleanupJobStatus or the HTTP endpoint /v1/jobs/:id.
  • The job runs in the background inside the running process. If your process restarts, the job will not be resumed. You can use the job status key (it lives 24h in Redis) to inspect the last result.

Important: the long-running background job is implemented as a goroutine that scans Redis keys and updates them using pipelines. If you expect extremely large user counts, consider running this cleanup in a dedicated worker process or offloading to a separate job queue for better reliability.


Docker / build instructions

To build the image (server) provided in cmd/server/Dockerfile the repository contains a multi-stage Dockerfile.

Build locally:

# from repo root
docker build -f cmd/server/DockerFile -t my-authz:latest .

# run (provide env values)
docker run -e ZITADEL_DOMAIN="http://host.docker.internal:8080" \
  -e SERVICE_ACCOUNT_TOKEN="<token>" \
  -e PROJECT_ID="<project id>" \
  -e REDIS_ADDR="host.docker.internal:6379" \
  -p 8081:8081 my-authz:latest

Notes about Dockerfile in repo:

  • Builds Go binary with CGO_ENABLED=0 GOOS=linux GOARCH=amd64.
  • Uses alpine:3.18 runtime with tini and a non-root app user.
  • Exposes port 8081 by default (from .env in your repo you might use PORT=8081).

Error handling and tips

  • When calling Zitadel, transient errors are retried using hashicorp/go-retryablehttp with a circuit breaker around requests (sony/gobreaker). Tune retries and circuit breaker values using REQUEST_TIMEOUT, RETRY_MAX, CB_INTERVAL, and CB_TIMEOUT env vars.
  • Cache entries are JSON objects with roles and fetched_at fields. Deleting a role triggers an async background job which may take time depending on number of cached users.
  • If you call AssignRole / AssignRolesToUser the code invalidates the cache for the affected user ID so the next request will fetch fresh roles from Zitadel.
  • The RoleMiddleware resolves user id either from X-User-ID header (preferred for internal calls) or by calling the Zitadel /oidc/v1/userinfo endpoint with the Bearer token present in Authorization header. For opaque tokens, ensure ZITADEL_DOMAIN is set.

FAQ / Common scenarios

Q: I want the background cleanup to survive restarts. A: The current implementation runs jobs in-process. For resilience, extract the cleanup loop into a separate worker service (could be the same binary but a different process mode) or push jobs to a durable queue (e.g., Redis streams, RabbitMQ) and run dedicated workers.

Q: How do I handle many role keys per user? A: The cache stores role arrays. The cleanup logic uses in-place slice filtering and pipelines. If you expect huge role lists, consider paginating or sharding cache keys.

Q: How is TTL handled when cleaning up? A: The cleaner attempts to preserve TTLs by reading existing TTL and re-applying it. If TTL can't be read, the default TTL from config is used.


Contributing

  • Follow existing repository style
  • Add unit tests for pkg/cache and pkg/zitadel client parsing where possible
  • Do not commit secrets

License

(Include your license here — e.g., MIT)


If you'd like, I can also:

  • Provide a shorter README (one-page quickstart)
  • Generate examples/ with small Go programs demonstrating each operation
  • Translate to Arabic

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published