template-monorepo/internal/model/permission/repository/casbin_redis.go

109 lines
3.1 KiB
Go
Raw Normal View History

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)