golib/gcasbin/adapter_redis.go

173 lines
4.7 KiB
Go

//
// adapter_redis.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcasbin
import (
"context"
"errors"
"fmt"
"strings"
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
"github.com/casbin/casbin/v2/util"
"github.com/redis/go-redis/v9"
)
const (
// The key under which the policies are stored in redis
PolicyKey = "casbin:policy"
)
// Adapter is an adapter for policy storage based on Redis
type RedisAdapter struct {
redisCli *redis.Client
}
// NewFromDSN returns a new Adapter by using the given DSN.
// Format: redis://:{password}@{host}:{port}/{database}
// Example: redis://:123@localhost:6379/0
func NewRedisAdapterFromURL(url string) (adapter *RedisAdapter, err error) {
opt, err := redis.ParseURL(url)
if err != nil {
return nil, err
}
redisCli := redis.NewClient(opt)
if err = redisCli.Ping(context.Background()).Err(); err != nil {
return nil, fmt.Errorf("failed to ping redis: %v", err)
}
return NewRedisAdapterFromClient(redisCli), nil
}
// NewFromClient returns a new instance of Adapter from an already existing go-redis client.
func NewRedisAdapterFromClient(redisCli *redis.Client) (adapter *RedisAdapter) {
return &RedisAdapter{redisCli: redisCli}
}
// LoadPolicy loads all policy rules from the storage.
func (a *RedisAdapter) LoadPolicy(model model.Model) (err error) {
ctx := context.Background()
// Using the LoadPolicyLine handler from the Casbin repo for building rules
return a.loadPolicy(ctx, model, persist.LoadPolicyArray)
}
func (a *RedisAdapter) loadPolicy(ctx context.Context, model model.Model, handler func([]string, model.Model) error) (err error) {
// 0, -1 fetches all entries from the list
rules, err := a.redisCli.LRange(ctx, PolicyKey, 0, -1).Result()
if err != nil {
return err
}
// Parse the rules from Redis
for _, rule := range rules {
handler(strings.Split(rule, ", "), model)
}
return
}
// SavePolicy saves all policy rules to the storage.
func (a *RedisAdapter) SavePolicy(model model.Model) (err error) {
ctx := context.Background()
var rules []string
// Serialize the policies into a string slice
for ptype, assertion := range model["p"] {
for _, rule := range assertion.Policy {
rules = append(rules, buildRuleStr(ptype, rule))
}
}
// Append the group policies to the slice
for ptype, assertion := range model["g"] {
for _, rule := range assertion.Policy {
rules = append(rules, buildRuleStr(ptype, rule))
}
}
// If an empty ruleset is saved, the policy is completely deleted from Redis.
if len(rules) > 0 {
return a.savePolicy(ctx, rules)
}
return a.delPolicy(ctx)
}
func (a *RedisAdapter) savePolicy(ctx context.Context, rules []string) (err error) {
// Use a transaction for deleting the key & creating a new one.
// This only uses one round trip to Redis and also makes sure nothing bad happens.
cmd, err := a.redisCli.TxPipelined(ctx, func(tx redis.Pipeliner) error {
tx.Del(ctx, PolicyKey)
tx.RPush(ctx, PolicyKey, strToInterfaceSlice(rules)...)
return nil
})
if err = cmd[0].Err(); err != nil {
return fmt.Errorf("failed to delete policy key: %v", err)
}
if err = cmd[1].Err(); err != nil {
return fmt.Errorf("failed to save policy: %v", err)
}
return
}
func (a *RedisAdapter) delPolicy(ctx context.Context) (err error) {
if err = a.redisCli.Del(ctx, PolicyKey).Err(); err != nil {
return err
}
return
}
// AddPolicy adds a policy rule to the storage.
func (a *RedisAdapter) AddPolicy(_ string, ptype string, rule []string) (err error) {
ctx := context.Background()
return a.addPolicy(ctx, buildRuleStr(ptype, rule))
}
func (a *RedisAdapter) addPolicy(ctx context.Context, rule string) (err error) {
if err = a.redisCli.RPush(ctx, PolicyKey, rule).Err(); err != nil {
return err
}
return
}
// RemovePolicy removes a policy rule from the storage.
func (a *RedisAdapter) RemovePolicy(_ string, ptype string, rule []string) (err error) {
ctx := context.Background()
return a.removePolicy(ctx, buildRuleStr(ptype, rule))
}
func (a *RedisAdapter) removePolicy(ctx context.Context, rule string) (err error) {
if err = a.redisCli.LRem(ctx, PolicyKey, 1, rule).Err(); err != nil {
return err
}
return
}
// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
func (a *RedisAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
return errors.New("not implemented")
}
// Converts a string slice to an interface{} slice.
// Needed for pushing elements to a redis list.
func strToInterfaceSlice(ss []string) (is []interface{}) {
for _, s := range ss {
is = append(is, s)
}
return
}
func buildRuleStr(ptype string, rule []string) string {
return ptype + ", " + util.ArrayToString(rule)
}