package repository import ( "context" "encoding/json" "errors" "fmt" redislib "gateway/internal/library/redis" permission "gateway/internal/model/permission/domain" domrepo "gateway/internal/model/permission/domain/repository" "github.com/zeromicro/go-zero/core/stores/redis" ) // CasbinRedisAdapter is a tenant-scoped Redis-backed Casbin policy store. // Layout: // // permission:casbin:rules:{tenant_id} → Redis Set of JSON-encoded // []string rule rows. // // Atomicity: SaveAll uses a Pipelined DEL+SADD; AddPolicy/RemovePolicy // rely on Redis Set semantics (idempotent inserts, single-shot removes). type CasbinRedisAdapter struct { client *redis.Redis } // NewCasbinRedisAdapter returns a CasbinPolicyAdapter backed by Redis. func NewCasbinRedisAdapter(client *redislib.Client) (domrepo.CasbinPolicyAdapter, error) { if client == nil || client.Zero() == nil { return nil, fmt.Errorf("permission: redis client is required for casbin adapter") } return &CasbinRedisAdapter{client: client.Zero()}, nil } func (a *CasbinRedisAdapter) key(tenantID string) string { return permission.GetCasbinRulesRedisKey(tenantID) } // LoadAll returns every rule for tenantID. func (a *CasbinRedisAdapter) LoadAll(ctx context.Context, tenantID string) ([][]string, error) { raw, err := a.client.SmembersCtx(ctx, a.key(tenantID)) if err != nil && !errors.Is(err, redis.Nil) { return nil, err } rules := make([][]string, 0, len(raw)) for _, item := range raw { var rule []string if err := json.Unmarshal([]byte(item), &rule); err != nil { continue } if len(rule) == 0 { continue } rules = append(rules, rule) } return rules, nil } // SaveAll replaces all rules for tenantID with rules atomically. func (a *CasbinRedisAdapter) SaveAll(ctx context.Context, tenantID string, rules [][]string) error { key := a.key(tenantID) encoded := make([]any, 0, len(rules)) for _, rule := range rules { raw, err := json.Marshal(rule) if err != nil { return fmt.Errorf("permission: marshal casbin rule: %w", err) } encoded = append(encoded, string(raw)) } return a.client.PipelinedCtx(ctx, func(p redis.Pipeliner) error { if err := p.Del(ctx, key).Err(); err != nil { return err } if len(encoded) == 0 { return nil } return p.SAdd(ctx, key, encoded...).Err() }) } // AddPolicy inserts a single rule (idempotent). func (a *CasbinRedisAdapter) AddPolicy(ctx context.Context, tenantID string, rule []string) error { raw, err := json.Marshal(rule) if err != nil { return err } _, err = a.client.SaddCtx(ctx, a.key(tenantID), string(raw)) return err } // RemovePolicy removes a single rule. func (a *CasbinRedisAdapter) RemovePolicy(ctx context.Context, tenantID string, rule []string) error { raw, err := json.Marshal(rule) if err != nil { return err } _, err = a.client.SremCtx(ctx, a.key(tenantID), string(raw)) return err } // Clear empties all rules for tenantID. func (a *CasbinRedisAdapter) Clear(ctx context.Context, tenantID string) error { _, err := a.client.DelCtx(ctx, a.key(tenantID)) return err } var _ domrepo.CasbinPolicyAdapter = (*CasbinRedisAdapter)(nil)