mycasbin

package
v1.1.52 Latest Latest
Warning

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

Go to latest
Published: Jun 14, 2026 License: Apache-2.0 Imports: 19 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 compatibility

The MSG payload is byte-for-byte compatible with go-admin-team/redis-watcher/v2, so instances running the old per-tenant watcher and instances running the mux can coexist in the same Redis during a rolling upgrade.

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

This section is empty.

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 Setup

func Setup(db *gorm.DB, _ string) *casbin.SyncedEnforcer

Setup 为指定租户创建 Casbin enforcer(向后兼容函数) 注意: 此函数保留用于向后兼容,新代码应使用 SetupForTenant Deprecated: 使用 SetupForTenant 替代,以获得更好的错误处理和多租户支持

func SetupForTenant added in v1.1.33

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

SetupForTenant 为指定租户创建独立的 Casbin enforcer 每个租户拥有独立的 adapter、enforcer 实例和 Redis Watcher 频道

⚠️ 仅供 security-management 使用(本地数据库模式) 其他微服务请使用 SetupWithProvider

参数:

  • db: 该租户的数据库连接
  • tenantID: 租户ID(用于日志标识和 Redis Watcher 频道隔离)

返回:

  • *casbin.SyncedEnforcer: 该租户专属的 enforcer 实例
  • error: 错误信息

Redis Watcher:

  • 使用 CasbinWatcherMux: 单一 PSUBSCRIBE 连接监听所有租户
  • 每个租户使用独立的 Redis 频道: /casbin/tenant/{tenantID}
  • 当租户的权限策略变更时,通过 Redis pub/sub 自动通知所有实例重新加载策略
  • Redis 不可用时,enforcer 仍能正常创建和使用(优雅降级)

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, identical to redis-watcher/v2 MSG struct. JSON field names are implicit (exported Go fields) so the encoding is compatible.

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 mirrors redis-watcher/v2 UpdateType string constants.

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