blob: 776ee7a0badfb6480aaf9ae4385519cfd253f204 [file]
package rediswatcher
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"strings"
"sync"
"github.com/casbin/casbin/v3"
"github.com/casbin/casbin/v3/model"
"github.com/casbin/casbin/v3/persist"
rds "github.com/redis/go-redis/v9"
)
type Watcher struct {
l sync.Mutex
subClient rds.UniversalClient
pubClient rds.UniversalClient
options WatcherOptions
close chan struct{}
callback func(string)
ctx context.Context
}
func DefaultUpdateCallback(e casbin.IEnforcer) func(string) {
return func(msg string) {
msgStruct := &MSG{}
err := msgStruct.UnmarshalBinary([]byte(msg))
if err != nil {
log.Println(err)
return
}
var res bool
switch msgStruct.Method {
case Update, UpdateForSavePolicy:
err = e.LoadPolicy()
res = true
case UpdateForAddPolicy:
res, err = e.SelfAddPolicy(msgStruct.Sec, msgStruct.Ptype, msgStruct.NewRule)
case UpdateForAddPolicies:
res, err = e.SelfAddPolicies(msgStruct.Sec, msgStruct.Ptype, msgStruct.NewRules)
case UpdateForRemovePolicy:
res, err = e.SelfRemovePolicy(msgStruct.Sec, msgStruct.Ptype, msgStruct.NewRule)
case UpdateForRemoveFilteredPolicy:
res, err = e.SelfRemoveFilteredPolicy(msgStruct.Sec, msgStruct.Ptype, msgStruct.FieldIndex, msgStruct.FieldValues...)
case UpdateForRemovePolicies:
res, err = e.SelfRemovePolicies(msgStruct.Sec, msgStruct.Ptype, msgStruct.NewRules)
case UpdateForUpdatePolicy:
res, err = e.SelfUpdatePolicy(msgStruct.Sec, msgStruct.Ptype, msgStruct.OldRule, msgStruct.NewRule)
case UpdateForUpdatePolicies:
res, err = e.SelfUpdatePolicies(msgStruct.Sec, msgStruct.Ptype, msgStruct.OldRules, msgStruct.NewRules)
default:
err = errors.New("unknown update type")
}
if err != nil {
log.Println(err)
}
if !res {
log.Println("callback update policy failed")
}
}
}
type MSG struct {
Method UpdateType
ID string
Sec string
Ptype string
OldRule []string
OldRules [][]string
NewRule []string
NewRules [][]string
FieldIndex int
FieldValues []string
}
type UpdateType string
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"
)
func (m *MSG) MarshalBinary() ([]byte, error) {
return json.Marshal(m)
}
// UnmarshalBinary decodes the struct into a User
func (m *MSG) UnmarshalBinary(data []byte) error {
if err := json.Unmarshal(data, m); err != nil {
return err
}
return nil
}
// NewWatcher creates a new Watcher to be used with a Casbin enforcer
// addr is a redis target string in the format "host:port"
// setters allows for inline WatcherOptions
//
// Example:
// w, err := rediswatcher.NewWatcher("127.0.0.1:6379",WatcherOptions{}, nil)
func NewWatcher(addr string, option WatcherOptions) (persist.Watcher, error) {
option.Options.Addr = addr
initConfig(&option)
w := &Watcher{
ctx: context.Background(),
close: make(chan struct{}),
}
if err := w.initConfig(option); err != nil {
return nil, err
}
if err := w.subClient.Ping(w.ctx).Err(); err != nil {
return nil, err
}
if err := w.pubClient.Ping(w.ctx).Err(); err != nil {
return nil, err
}
w.options = option
w.subscribe()
return w, nil
}
// NewWatcherWithCluster creates a new Watcher to be used with a Casbin enforcer
// addrs is a redis-cluster target string in the format "host1:port1,host2:port2,host3:port3"
//
// Example:
// w, err := rediswatcher.NewWatcherWithCluster("127.0.0.1:6379,127.0.0.1:6379,127.0.0.1:6379",WatcherOptions{})
func NewWatcherWithCluster(addrs string, option WatcherOptions) (persist.Watcher, error) {
addrsStr := strings.Split(addrs, ",")
option.ClusterOptions.Addrs = addrsStr
initConfig(&option)
w := &Watcher{
subClient: rds.NewClusterClient(&rds.ClusterOptions{
Addrs: addrsStr,
Password: option.ClusterOptions.Password,
}),
pubClient: rds.NewClusterClient(&rds.ClusterOptions{
Addrs: addrsStr,
Password: option.ClusterOptions.Password,
}),
ctx: context.Background(),
close: make(chan struct{}),
}
err := w.initConfig(option, true)
if err != nil {
return nil, err
}
if err := w.subClient.Ping(w.ctx).Err(); err != nil {
return nil, err
}
if err := w.pubClient.Ping(w.ctx).Err(); err != nil {
return nil, err
}
w.options = option
w.subscribe()
return w, nil
}
func (w *Watcher) initConfig(option WatcherOptions, cluster ...bool) error {
var err error
if option.OptionalUpdateCallback != nil {
err = w.SetUpdateCallback(option.OptionalUpdateCallback)
} else {
err = w.SetUpdateCallback(func(string) {
log.Println("Casbin Redis Watcher callback not set when an update was received")
})
}
if err != nil {
return err
}
if option.SubClient != nil {
w.subClient = option.SubClient
} else {
if len(cluster) > 0 && cluster[0] {
w.subClient = rds.NewClusterClient(&option.ClusterOptions)
} else {
w.subClient = rds.NewClient(&option.Options)
}
}
if option.PubClient != nil {
w.pubClient = option.PubClient
} else {
if len(cluster) > 0 && cluster[0] {
w.pubClient = rds.NewClusterClient(&option.ClusterOptions)
} else {
w.pubClient = rds.NewClient(&option.Options)
}
}
return nil
}
// NewPublishWatcher return a Watcher only publish but not subscribe
func NewPublishWatcher(addr string, option WatcherOptions) (persist.Watcher, error) {
option.Options.Addr = addr
w := &Watcher{
pubClient: rds.NewClient(&option.Options),
ctx: context.Background(),
close: make(chan struct{}),
}
initConfig(&option)
w.options = option
return w, nil
}
// SetUpdateCallback sets the update callback function invoked by the watcher
// when the policy is updated. Defaults to Enforcer.LoadPolicy()
func (w *Watcher) SetUpdateCallback(callback func(string)) error {
w.l.Lock()
w.callback = callback
w.l.Unlock()
return nil
}
// Update publishes a message to all other casbin instances telling them to
// invoke their update callback
func (w *Watcher) Update() error {
return w.logRecord(func() error {
w.l.Lock()
defer w.l.Unlock()
return w.pubClient.Publish(
context.Background(),
w.options.Channel,
&MSG{
Method: Update,
ID: w.options.LocalID,
},
).Err()
})
}
// UpdateForAddPolicy calls the update callback of other instances to synchronize their policy.
// It is called after Enforcer.AddPolicy()
func (w *Watcher) UpdateForAddPolicy(sec, ptype string, params ...string) error {
return w.logRecord(func() error {
w.l.Lock()
defer w.l.Unlock()
return w.pubClient.Publish(
context.Background(),
w.options.Channel,
&MSG{
Method: UpdateForAddPolicy,
ID: w.options.LocalID,
Sec: sec,
Ptype: ptype,
NewRule: params,
}).Err()
})
}
// UpdateForRemovePolicy calls the update callback of other instances to synchronize their policy.
// It is called after Enforcer.RemovePolicy()
func (w *Watcher) UpdateForRemovePolicy(sec, ptype string, params ...string) error {
return w.logRecord(func() error {
w.l.Lock()
defer w.l.Unlock()
return w.pubClient.Publish(
context.Background(),
w.options.Channel,
&MSG{
Method: UpdateForRemovePolicy,
ID: w.options.LocalID,
Sec: sec,
Ptype: ptype,
NewRule: params,
},
).Err()
})
}
// UpdateForRemoveFilteredPolicy calls the update callback of other instances to synchronize their policy.
// It is called after Enforcer.RemoveFilteredNamedGroupingPolicy()
func (w *Watcher) UpdateForRemoveFilteredPolicy(sec, ptype string, fieldIndex int, fieldValues ...string) error {
return w.logRecord(func() error {
w.l.Lock()
defer w.l.Unlock()
return w.pubClient.Publish(
context.Background(),
w.options.Channel,
&MSG{
Method: UpdateForRemoveFilteredPolicy,
ID: w.options.LocalID,
Sec: sec,
Ptype: ptype,
FieldIndex: fieldIndex,
FieldValues: fieldValues,
},
).Err()
})
}
// UpdateForSavePolicy calls the update callback of other instances to synchronize their policy.
// It is called after Enforcer.RemoveFilteredNamedGroupingPolicy()
func (w *Watcher) UpdateForSavePolicy(model model.Model) error {
return w.logRecord(func() error {
w.l.Lock()
defer w.l.Unlock()
return w.pubClient.Publish(
context.Background(),
w.options.Channel,
&MSG{
Method: UpdateForSavePolicy,
ID: w.options.LocalID,
},
).Err()
})
}
// UpdateForAddPolicies calls the update callback of other instances to synchronize their policies in batch.
// It is called after Enforcer.AddPolicies()
func (w *Watcher) UpdateForAddPolicies(sec string, ptype string, rules ...[]string) error {
return w.logRecord(func() error {
w.l.Lock()
defer w.l.Unlock()
return w.pubClient.Publish(
context.Background(),
w.options.Channel,
&MSG{
Method: UpdateForAddPolicies,
ID: w.options.LocalID,
Sec: sec,
Ptype: ptype,
NewRules: rules,
},
).Err()
})
}
// UpdateForRemovePolicies calls the update callback of other instances to synchronize their policies in batch.
// It is called after Enforcer.RemovePolicies()
func (w *Watcher) UpdateForRemovePolicies(sec string, ptype string, rules ...[]string) error {
return w.logRecord(func() error {
w.l.Lock()
defer w.l.Unlock()
return w.pubClient.Publish(
context.Background(),
w.options.Channel,
&MSG{
Method: UpdateForRemovePolicies,
ID: w.options.LocalID,
Sec: sec,
Ptype: ptype,
NewRules: rules,
},
).Err()
})
}
// UpdateForUpdatePolicy calls the update callback of other instances to synchronize their policy.
// It is called after Enforcer.UpdatePolicy()
func (w *Watcher) UpdateForUpdatePolicy(sec string, ptype string, oldRule, newRule []string) error {
return w.logRecord(func() error {
w.l.Lock()
defer w.l.Unlock()
return w.pubClient.Publish(
context.Background(),
w.options.Channel,
&MSG{
Method: UpdateForUpdatePolicy,
ID: w.options.LocalID,
Sec: sec,
Ptype: ptype,
OldRule: oldRule,
NewRule: newRule,
},
).Err()
})
}
// UpdateForUpdatePolicies calls the update callback of other instances to synchronize their policy.
// It is called after Enforcer.UpdatePolicies()
func (w *Watcher) UpdateForUpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) error {
return w.logRecord(func() error {
w.l.Lock()
defer w.l.Unlock()
return w.pubClient.Publish(
context.Background(),
w.options.Channel,
&MSG{
Method: UpdateForUpdatePolicies,
ID: w.options.LocalID,
Sec: sec,
Ptype: ptype,
OldRules: oldRules,
NewRules: newRules,
},
).Err()
})
}
func (w *Watcher) logRecord(f func() error) error {
err := f()
if err != nil {
log.Println(err)
}
return err
}
func (w *Watcher) unsubscribe(psc *rds.PubSub) error {
return psc.Unsubscribe(w.ctx)
}
func (w *Watcher) subscribe() {
w.l.Lock()
sub := w.subClient.Subscribe(w.ctx, w.options.Channel)
w.l.Unlock()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer func() {
err := sub.Close()
if err != nil {
log.Println(err)
}
err = w.pubClient.Close()
if err != nil {
log.Println(err)
}
err = w.subClient.Close()
if err != nil {
log.Println(err)
}
}()
ch := sub.Channel()
wg.Done()
for msg := range ch {
select {
case <-w.close:
return
default:
}
data := msg.Payload
msgStruct := &MSG{}
err := msgStruct.UnmarshalBinary([]byte(data))
if err != nil {
log.Println(fmt.Printf("Failed to parse message: %s with error: %s\n", data, err.Error()))
} else {
isSelf := msgStruct.ID == w.options.LocalID
if !(w.options.IgnoreSelf && isSelf) {
w.callback(data)
}
}
}
}()
wg.Wait()
}
func (w *Watcher) GetWatcherOptions() WatcherOptions {
w.l.Lock()
defer w.l.Unlock()
return w.options
}
func (w *Watcher) Close() {
w.l.Lock()
defer w.l.Unlock()
close(w.close)
w.pubClient.Publish(w.ctx, w.options.Channel, "Close")
}