Casbin Informer Watcher is a Kubernetes informer-based watcher for Casbin. This watcher enables real-time policy synchronization across multiple Casbin enforcer instances by watching Kubernetes Custom Resource Definitions (CRDs).
SyncedEnforcer in multi-threaded environmentsgo get github.com/casbin/casbin-informer-watcher
package main import ( "log" "github.com/casbin/casbin/v2" informerwatcher "github.com/casbin/casbin-informer-watcher" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/tools/clientcmd" ) func main() { // Load Kubernetes configuration config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile) if err != nil { log.Fatalf("Failed to load kubeconfig: %v", err) } // Create dynamic client client, err := dynamic.NewForConfig(config) if err != nil { log.Fatalf("Failed to create dynamic client: %v", err) } // Define the GVR for your policy CRD gvr := schema.GroupVersionResource{ Group: "casbin.org", Version: "v1", Resource: "policies", } // Create the watcher watcher, err := informerwatcher.NewWatcher(client, gvr, "default", informerwatcher.WatcherOptions{}) if err != nil { log.Fatalf("Failed to create watcher: %v", err) } defer watcher.Close() // Initialize the enforcer e, err := casbin.NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") if err != nil { log.Fatalf("Failed to create enforcer: %v", err) } // Set the watcher for the enforcer err = e.SetWatcher(watcher) if err != nil { log.Fatalf("Failed to set watcher: %v", err) } // By default, the watcher's callback is automatically set to the // enforcer's LoadPolicy() in the SetWatcher() call. // You can change it by explicitly setting a callback. err = watcher.SetUpdateCallback(informerwatcher.DefaultUpdateCallback(e)) if err != nil { log.Fatalf("Failed to set callback: %v", err) } log.Println("Watcher is running and monitoring policy changes...") select {} // Keep the program running }
package main import ( "log" "time" "github.com/casbin/casbin/v2" informerwatcher "github.com/casbin/casbin-informer-watcher" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/tools/clientcmd" ) func main() { // Load Kubernetes configuration config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile) if err != nil { log.Fatalf("Failed to load kubeconfig: %v", err) } // Create dynamic client client, err := dynamic.NewForConfig(config) if err != nil { log.Fatalf("Failed to create dynamic client: %v", err) } // Define the GVR for your policy CRD gvr := schema.GroupVersionResource{ Group: "casbin.org", Version: "v1", Resource: "policies", } // Create watcher with custom options options := informerwatcher.WatcherOptions{ LocalID: "instance-1", // Custom instance identifier IgnoreSelf: true, // Ignore updates from this instance ResyncPeriod: 30 * time.Second, // Resync period with API server IncrementalUpdate: true, // Report the rules that changed instead of reloading } watcher, err := informerwatcher.NewWatcher(client, gvr, "default", options) if err != nil { log.Fatalf("Failed to create watcher: %v", err) } defer watcher.Close() // Initialize the enforcer e, err := casbin.NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") if err != nil { log.Fatalf("Failed to create enforcer: %v", err) } // Set the watcher err = e.SetWatcher(watcher) if err != nil { log.Fatalf("Failed to set watcher: %v", err) } // Custom callback that logs updates customCallback := func(msg string) { log.Printf("Policy update received: %s\n", msg) informerwatcher.DefaultUpdateCallback(e)(msg) } err = watcher.SetUpdateCallback(customCallback) if err != nil { log.Fatalf("Failed to set callback: %v", err) } log.Println("Watcher is running with custom configuration...") select {} // Keep the program running }
package main import ( "log" "github.com/casbin/casbin/v2" informerwatcher "github.com/casbin/casbin-informer-watcher" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/tools/clientcmd" ) func main() { // Setup client and GVR (same as basic example) config, _ := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile) client, _ := dynamic.NewForConfig(config) gvr := schema.GroupVersionResource{ Group: "casbin.org", Version: "v1", Resource: "policies", } watcher, _ := informerwatcher.NewWatcher(client, gvr, "default", informerwatcher.WatcherOptions{}) defer watcher.Close() // Use SyncedEnforcer for concurrency-safe operations e, err := casbin.NewSyncedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") if err != nil { log.Fatalf("Failed to create synced enforcer: %v", err) } err = e.SetWatcher(watcher) if err != nil { log.Fatalf("Failed to set watcher: %v", err) } log.Println("SyncedEnforcer is running with watcher...") select {} }
SetUpdateCallback right after NewWatcher, but it is in place before the first event arrives.DefaultUpdateCallback applies each message to the enforcer, either by reloading the policy or by applying the incremental change.Three kinds of event carry no change and are dropped rather than reported:
resourceVersion on a real write, so a resource that comes back with the same one has not changed.IgnoreSelf is enabled and the resource carries the casbin.org/source-id annotation with this watcher's LocalID.Deletions missed while the watch was down arrive as a tombstone rather than the resource itself; the watcher unwraps it and reports the deletion normally.
By default, every observed change asks the enforcer for a full reload (the Update message). Reloading is idempotent, so instances cannot drift apart no matter how a resource was edited. This is the recommended mode when the enforcer's adapter reads the same resources the watcher observes.
With IncrementalUpdate enabled, the watcher reads the rules out of the resource and reports only what changed:
Incremental mode requires policy resources to follow the policy resource layout. Note that Casbin applies these messages through its Self* APIs, which write through to the adapter when AutoSave is enabled: if your adapter is backed by the same resources the watcher observes, keep the default reload mode so the observed change is not written straight back.
In incremental mode the watcher reads rules from two spec fields. Both hold Casbin policy lines in the same format used in a policy CSV file, so the policy type comes first and the section is its first character (p and p2 belong to section p, g and g2 to section g). Blank lines and # comments are skipped.
A single rule, or several newline-separated ones:
apiVersion: casbin.org/v1 kind: Policy metadata: name: alice-can-read spec: policy: "p, alice, data1, read"
Or a list:
apiVersion: casbin.org/v1 kind: Policy metadata: name: team-policies spec: policies: - "p, alice, data1, read" - "p, bob, data2, write" - "g, alice, admin"
The default reload mode does not read these fields, and works with any resource layout.
The watcher supports all standard Casbin policy update operations:
Update: Full policy reloadUpdateForAddPolicy: Add a single policy ruleUpdateForRemovePolicy: Remove a single policy ruleUpdateForAddPolicies: Add multiple policy rulesUpdateForRemovePolicies: Remove multiple policy rulesUpdateForRemoveFilteredPolicy: Remove filtered policy rulesUpdateForUpdatePolicy: Update a single policy ruleUpdateForUpdatePolicies: Update multiple policy rulesUpdateForSavePolicy: Save policy to storageYou‘ll need to define a CRD for your Casbin policies. Here’s a basic example:
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: policies.casbin.org spec: group: casbin.org versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: policy: type: string policies: type: array items: type: string scope: Namespaced names: plural: policies singular: policy kind: Policy
Run the test suite:
go test -v ./...
Run tests with coverage:
go test -v -coverprofile=coverage.out ./... go tool cover -html=coverage.out
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.