mycasbin

package
v1.1.63 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

Casbin Multi-Tenant Support

This package provides Casbin enforcer setup with multi-tenant support.

Migration Guide

Old Usage (Deprecated)
// ❌ Old: Direct database connection
enforcer := mycasbin.Setup(db, "")
For security-management (Local Database)
// ✅ security-management uses local database
enforcer, err := mycasbin.SetupForTenant(db, tenantID)
For Microservices (Remote Provider)
// ✅ Microservices use remote provider
provider := grpc_client.NewGrpcCasbinPolicyProvider(connManager)
enforcer, err := mycasbin.SetupWithProvider(provider, tenantID)

API Reference

SetupForTenant
func SetupForTenant(db *gorm.DB, tenantID int) (*casbin.SyncedEnforcer, error)

Creates an independent Casbin enforcer for the specified tenant.

Parameters:

  • db: GORM database connection for the tenant's database
  • tenantID: Tenant identifier (used for logging and to pin this tenant's policy-sync messages to a dedicated dispatch shard via the PSUBSCRIBE multiplexer)

Returns:

  • *casbin.SyncedEnforcer: Tenant-specific enforcer instance
  • error: Error if setup fails

Example:

db := getTenantDatabase(tenantID)
enforcer, err := mycasbin.SetupForTenant(db, tenantID)
if err != nil {
    return fmt.Errorf("租户 %d Casbin 初始化失败: %w", tenantID, err)
}

// Use enforcer for permission checks
allowed, err := enforcer.Enforce("user", "/api/v1/resource", "GET")
SetupWithProvider
func SetupWithProvider(provider PolicyProvider, tenantID int) (*casbin.SyncedEnforcer, error)

Creates a Casbin enforcer using a PolicyProvider instead of a database connection.

Parameters:

  • provider: Policy provider implementation (typically gRPC-based)
  • tenantID: Tenant identifier

Returns:

  • *casbin.SyncedEnforcer: Enforcer with loaded policies
  • error: Error if setup fails

Example:

provider := grpc_client.NewGrpcCasbinPolicyProvider(connManager)
enforcer, err := mycasbin.SetupWithProvider(provider, tenantID)
if err != nil {
    log.Fatalf("Casbin init failed: %v", err)
}

// Use enforcer for permission checks
allowed, err := enforcer.Enforce("user", "/api/v1/resource", "GET")

Usage:

  • For microservices (evidence-management, file-storage-service, tenant-service)
  • NOT for security-management (use SetupForTenant instead)
Setup (Deprecated)
func Setup(db *gorm.DB, _ string) *casbin.SyncedEnforcer

Legacy function for backward compatibility. Creates a single global enforcer.

Deprecated: Use SetupForTenant instead for proper multi-tenant support.

Redis Watcher (PSUBSCRIBE Multiplexer)

Policy synchronization uses a single PSUBSCRIBE connection (the CasbinWatcherMux singleton in watcher_mux.go) rather than one subscriber per tenant. New tenants match the wildcard pattern automatically, with zero additional connection cost.

Three-client Redis model

The watcher relies on jxt-core's split Redis client architecture (sdk/config/option_redis.go):

Client Role Used by the watcher
#1 shared Non-blocking operations PUBLISH to /casbin/tenant/{tenantID}
#2 queue consumer Blocking XREADGROUP — (reserved for the message queue)
#3 subscriber PSUBSCRIBE Listens on /casbin/tenant/* for all tenants
Channel naming
PUBLISH target  (per tenant):    /casbin/tenant/{tenantID}
PSUBSCRIBE pattern (all tenants): /casbin/tenant/*
How a policy change propagates
  1. A tenant's policies are modified and the watcher publishes an MSG to /casbin/tenant/{tenantID} via Client #1.
  2. Every instance's CasbinWatcherMux receives the message on the single Client #3 PSUBSCRIBE connection.
  3. Each message is routed to a dedicated dispatch shard via tenantID % 8, so all messages for one tenant are processed strictly in publish order by a single worker (8 workers, each with a bounded 256-message queue).
  4. The worker applies the update to that tenant's enforcer (LoadPolicy or an incremental SelfAdd/Remove/UpdatePolicy), skipping messages it published itself (UUID-based self-ignore).
Wire format

The MSG payload uses a fixed on-wire format. Do not change the field set or JSON tag names — instances running different jxt-core versions can coexist in the same Redis during a rolling upgrade only as long as that format stays stable.

Lifecycle
  • InitCasbinWatcherMux(pubClient, subClient) — starts the background listener and the 8-shard worker pool. Idempotent (sync.Once); safe to call from concurrent goroutines.
  • ShutdownCasbinWatcherMux() — drains the workers and listener so Redis clients can be closed safely. Wired into Application.Close().
  • The subscription reconnects with exponential backoff (1s → 30s cap), reset to the minimum after each successfully received message.
Graceful Degradation
  • If Redis is unavailable during SetupForTenant, the enforcer is still created successfully.
  • A warning is logged, but the tenant can still function.
  • PUBLISH becomes a no-op when Redis is absent, so policy changes won't auto-synchronize until Redis is available.

Testing

Run tests:

# Unit tests (mock-based)
go test ./sdk/pkg/casbin/... -short

# Integration tests (requires test database)
go test ./sdk/pkg/casbin/... -v

# With coverage
go test ./sdk/pkg/casbin/... -cover -coverprofile=coverage.out

Known Issues

None (as of Stage 3). All multi-tenant isolation issues have been fixed.

Roadmap

  • Stage 1: Remove singleton, add SetupForTenant
  • Stage 2: Redis Watcher per-tenant isolation ✅
  • Stage 3: PSUBSCRIBE multiplexer — single-connection all-tenant sync with 8-shard dispatch preserving per-tenant ordering ✅
  • Stage 4: Update security-management to use new APIs (pending)

Documentation

Index

Constants

View Source
const ModelText = `` /* 251-byte string literal not displayed */

Initialize the model from a string. ModelText is the canonical RBAC model for Casbin enforcers. Exported so security-management's local SetupForTenant reuses the same model instead of copying (avoids model drift between local-DB and remote-provider paths).

Variables

This section is empty.

Functions

func InitCasbinWatcherMux added in v1.1.51

func InitCasbinWatcherMux(pubClient, subClient *redis.Client)

InitCasbinWatcherMux initialises the global CasbinWatcherMux singleton and starts the background listener goroutine. It is safe to call multiple times from concurrent goroutines (subsequent calls after the first are no-ops).

Parameters:

  • pubClient: Client #1 from config.GetRedisClient() for PUBLISH
  • subClient: Client #3 from config.EnsureSubscriberClient() for PSUBSCRIBE

func SetEnforcerLookup added in v1.1.51

func SetEnforcerLookup(fn func(tenantID int) *casbin.SyncedEnforcer)

SetEnforcerLookup sets the function used to look up tenant enforcers. Called by runtime during Application initialization.

func SetupRedisWatcherForEnforcer added in v1.1.53

func SetupRedisWatcherForEnforcer(e *casbin.SyncedEnforcer, tenantID int)

SetupRedisWatcherForEnforcer sets up the CasbinWatcherMux watcher for an enforcer. Called by both SetupForTenant and SetupWithProvider to eliminate code duplication.

Behavior:

  • Lazily initialises the mux from the configured Redis clients on first use (idempotent), so callers don't need to wire InitCasbinWatcherMux manually
  • If Redis is not configured, degrades gracefully (no cross-instance sync)
  • If Client #1 is configured but the subscriber Client #3 is missing, logs a warning so operators can tell that policy sync is silently disabled

func SetupWithProvider added in v1.1.35

func SetupWithProvider(provider PolicyProvider, tenantID int) (*casbin.SyncedEnforcer, error)

SetupWithProvider creates a Casbin enforcer using a PolicyProvider (for microservices) Unlike SetupForTenant, this does not require a local database connection

⚠️ For non-security-management microservices only (remote fetch mode) security-management should continue using SetupForTenant

Parameters:

  • provider: Policy provider (implemented by microservice, typically a gRPC adapter)
  • tenantID: Tenant identifier

Returns:

  • *casbin.SyncedEnforcer: Enforcer with loaded policies
  • error: Initialization error

func ShutdownCasbinWatcherMux added in v1.1.51

func ShutdownCasbinWatcherMux()

ShutdownCasbinWatcherMux stops the mux listener and releases resources. It is safe to call when the mux is not initialised (no-op).

Types

type CasbinWatcherMux added in v1.1.51

type CasbinWatcherMux struct {
	// contains filtered or unexported fields
}

CasbinWatcherMux is a singleton that holds a single PSUBSCRIBE connection (Client #3) and dispatches incoming messages to the correct tenant enforcer.

func (*CasbinWatcherMux) NewWatcher added in v1.1.51

func (m *CasbinWatcherMux) NewWatcher(tenantID int) persist.Watcher

NewWatcher creates a per-tenant watcher handle that implements persist.Watcher + persist.WatcherEx + persist.UpdatableWatcher. The handle uses Client #1 for PUBLISH; lifecycle is managed by the mux.

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

Logger is the implementation for a Logger using zap logger.

func NewLogger added in v1.1.8

func NewLogger() *Logger

NewLogger creates a new Logger instance

func (*Logger) EnableLog

func (l *Logger) EnableLog(enable bool)

EnableLog controls whether print the message.

func (*Logger) IsEnabled

func (l *Logger) IsEnabled() bool

IsEnabled returns if logger is enabled.

func (*Logger) LogEnforce

func (l *Logger) LogEnforce(matcher string, request []interface{}, result bool, explains [][]string)

LogEnforce log info related to enforce.

func (*Logger) LogModel

func (l *Logger) LogModel(model [][]string)

LogModel log info related to model.

func (*Logger) LogPolicy

func (l *Logger) LogPolicy(policy map[string][][]string)

LogPolicy log info related to policy.

func (*Logger) LogRole

func (l *Logger) LogRole(roles []string)

LogRole log info related to role.

type MSG added in v1.1.51

type MSG struct {
	Method      UpdateType `json:"Method"`
	ID          string     `json:"ID"`
	Sec         string     `json:"Sec"`
	Ptype       string     `json:"Ptype"`
	OldRule     []string   `json:"OldRule"`
	OldRules    [][]string `json:"OldRules"`
	NewRule     []string   `json:"NewRule"`
	NewRules    [][]string `json:"NewRules"`
	FieldIndex  int        `json:"FieldIndex"`
	FieldValues []string   `json:"FieldValues"`
}

MSG is the wire payload. Its exported-field JSON encoding is part of the fixed wire format — do not change field names (see the rolling-upgrade note above).

type PolicyProvider added in v1.1.35

type PolicyProvider interface {
	// GetPolicies retrieves all policy rules for the specified tenant
	//
	// Parameters:
	//   - ctx: Context (for timeout control, cancellation, etc.)
	//   - tenantID: Tenant identifier
	//
	// Returns:
	//   - []PolicyRule: List of policy rules (including both p and g types)
	//   - error: Error information
	GetPolicies(ctx context.Context, tenantID int) ([]PolicyRule, error)
}

PolicyProvider is the strategy interface for providing policy data Microservices implement this interface to supply policies from any source (gRPC, HTTP, DB, etc.)

type PolicyRule added in v1.1.35

type PolicyRule struct {
	PType string // Policy type: "p" (policy) or "g" (role inheritance)
	V0    string // Usually sub (role name)
	V1    string // Usually obj (resource path)
	V2    string // Usually act (HTTP method)
	V3    string // Optional extension field
	V4    string // Optional extension field
	V5    string // Optional extension field
}

PolicyRule represents a single Casbin policy rule.

Validation Rules:

  • PType must be a valid Casbin policy type: "p" (policy), "g" (role inheritance), etc.
  • V0-V5 fields must be filled contiguously from left to right without gaps
  • Empty strings at the end of the sequence are allowed (e.g., V3="", V4="", V5="")
  • Empty strings in the middle are NOT allowed (e.g., V0="admin", V1="", V2="GET" is invalid)

This format matches Casbin's standard storage schema (gorm-adapter).

type ProviderAdapter added in v1.1.35

type ProviderAdapter struct {
	// contains filtered or unexported fields
}

ProviderAdapter implements Casbin's persist.Adapter interface using a PolicyProvider This is a read-only adapter - write operations return errors

func NewProviderAdapter added in v1.1.35

func NewProviderAdapter(provider PolicyProvider, tenantID int) *ProviderAdapter

NewProviderAdapter creates a new ProviderAdapter

func (*ProviderAdapter) AddPolicy added in v1.1.35

func (a *ProviderAdapter) AddPolicy(sec string, ptype string, rule []string) error

AddPolicy is not supported (read-only adapter)

func (*ProviderAdapter) LoadPolicy added in v1.1.35

func (a *ProviderAdapter) LoadPolicy(m model.Model) error

LoadPolicy loads all policies from the Provider into the model This is called by Casbin SyncedEnforcer.LoadPolicy()

func (*ProviderAdapter) RemoveFilteredPolicy added in v1.1.35

func (a *ProviderAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error

RemoveFilteredPolicy is not supported (read-only adapter)

func (*ProviderAdapter) RemovePolicy added in v1.1.35

func (a *ProviderAdapter) RemovePolicy(sec string, ptype string, rule []string) error

RemovePolicy is not supported (read-only adapter)

func (*ProviderAdapter) SavePolicy added in v1.1.35

func (a *ProviderAdapter) SavePolicy(m model.Model) error

SavePolicy is not supported (read-only adapter)

type UpdateType added in v1.1.51

type UpdateType string

UpdateType enumerates the policy-change message kinds carried on the wire.

const (
	Update                        UpdateType = "Update"
	UpdateForAddPolicy            UpdateType = "UpdateForAddPolicy"
	UpdateForRemovePolicy         UpdateType = "UpdateForRemovePolicy"
	UpdateForRemoveFilteredPolicy UpdateType = "UpdateForRemoveFilteredPolicy"
	UpdateForSavePolicy           UpdateType = "UpdateForSavePolicy"
	UpdateForAddPolicies          UpdateType = "UpdateForAddPolicies"
	UpdateForRemovePolicies       UpdateType = "UpdateForRemovePolicies"
	UpdateForUpdatePolicy         UpdateType = "UpdateForUpdatePolicy"
	UpdateForUpdatePolicies       UpdateType = "UpdateForUpdatePolicies"
)

Jump to

Keyboard shortcuts

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