diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b31900..87b0bfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,7 @@ jobs: run: go test -v -coverprofile=profile.cov ./... - name: Install goveralls - env: - GO111MODULE: off - run: go get github.com/mattn/goveralls + run: go install github.com/mattn/goveralls@v0.0.11 - name: Send coverage env: diff --git a/README.md b/README.md index 2ee6a18..b3b311f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,21 @@ Redis Adapter is the [Redis](https://redis.io/) adapter for [Casbin](https://git go get github.com/casbin/redis-adapter/v3 -## Simple Example +## Configuration Options + +The `Config` struct supports the following options: + +- `Network` (string): Network type, e.g., "tcp", "unix" (required when not using Pool) +- `Address` (string): Redis server address, e.g., "127.0.0.1:6379" (required when not using Pool) +- `Key` (string): Redis key to store Casbin rules (default: "casbin_rules") +- `Username` (string): Username for Redis authentication (optional) +- `Password` (string): Password for Redis authentication (optional) +- `TLSConfig` (*tls.Config): TLS configuration for secure connections (optional) +- `Pool` (*redis.Pool): Existing Redis connection pool (optional, if provided, other connection options are ignored) + +## Usage Examples + +### Basic Usage ```go package main @@ -26,28 +40,23 @@ import ( ) func main() { - // Direct Initialization: - // Initialize a Redis adapter and use it in a Casbin enforcer: - a, _ := redisadapter.NewAdapter("tcp", "127.0.0.1:6379") // Your Redis network and address. + // Recommended approach using Config + config := &redisadapter.Config{Network: "tcp", Address: "127.0.0.1:6379"} + a, _ := redisadapter.NewAdapter(config) - // Use the following if Redis has password like "123" - // a, err := redisadapter.NewAdapterWithPassword("tcp", "127.0.0.1:6379", "123") + // With password authentication + // config := &redisadapter.Config{Network: "tcp", Address: "127.0.0.1:6379", Password: "123"} + // a, _ := redisadapter.NewAdapter(config) - // Use the following if you use Redis with a specific user - // a, err := redisadapter.NewAdapterWithUser("tcp", "127.0.0.1:6379", "username", "password") + // With user credentials + // config := &redisadapter.Config{Network: "tcp", Address: "127.0.0.1:6379", Username: "user", Password: "pass"} + // a, _ := redisadapter.NewAdapter(config) - // Use the following if you use Redis connections pool - // pool := &redis.Pool{} - // a, err := redisadapter.NewAdapterWithPool(pool) - - // Initialization with different user options: - // Use the following if you use Redis with passowrd like "123": - // a, err := redisadapter.NewAdapterWithOption(redisadapter.WithNetwork("tcp"), redisadapter.WithAddress("127.0.0.1:6379"), redisadapter.WithPassword("123")) - - // Use the following if you use Redis with username, password, and TLS option: + // With TLS configuration // var clientTLSConfig tls.Config // ... - // a, err := redisadapter.NewAdapterWithOption(redisadapter.WithNetwork("tcp"), redisadapter.WithAddress("127.0.0.1:6379"), redisadapter.WithUsername("testAccount"), redisadapter.WithPassword("123456"), redisadapter.WithTls(&clientTLSConfig)) + // config := &redisadapter.Config{Network: "tcp", Address: "127.0.0.1:6379", Username: "testAccount", Password: "123456", TLSConfig: &clientTLSConfig} + // a, _ := redisadapter.NewAdapter(config) e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) @@ -66,6 +75,35 @@ func main() { } ``` +### With Connection Pool + +```go +package main + +import ( + "github.com/casbin/casbin/v2" + "github.com/casbin/redis-adapter/v3" + "github.com/gomodule/redigo/redis" +) + +func main() { + pool := &redis.Pool{Dial: func() (redis.Conn, error) { return redis.Dial("tcp", "127.0.0.1:6379") }} + config := &redisadapter.Config{Pool: pool, Key: "casbin_rules"} + a, _ := redisadapter.NewAdapter(config) + + e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) + + // Load the policy from DB. + e.LoadPolicy() + + // Check the permission. + e.Enforce("alice", "data1", "read") + + // Save the policy back to DB. + e.SavePolicy() +} +``` + ## Getting Help - [Casbin](https://github.com/casbin/casbin) diff --git a/adapter.go b/adapter.go index 6266c07..f46f317 100644 --- a/adapter.go +++ b/adapter.go @@ -40,6 +40,25 @@ type CasbinRule struct { V5 string } +// Config represents the configuration for the Redis adapter. +type Config struct { + // Network is the network type, e.g., "tcp", "unix" + Network string + // Address is the Redis server address, e.g., "127.0.0.1:6379" + Address string + // Key is the Redis key to store Casbin rules (default: "casbin_rules") + Key string + // Username for Redis authentication (optional) + Username string + // Password for Redis authentication (optional) + Password string + // TLSConfig for secure connections (optional) + TLSConfig *tls.Config + // Pool is an existing Redis connection pool (optional) + // If provided, Network, Address, Username, Password, and TLSConfig are ignored + Pool *redis.Pool +} + // Adapter represents the Redis adapter for policy storage. type Adapter struct { network string @@ -78,94 +97,148 @@ func finalizer(a *Adapter) { } } -func newAdapter(network string, address string, key string, - username string, password string) (*Adapter, error) { +// NewAdapter creates a new Redis adapter with the provided configuration. +func NewAdapter(config *Config) (*Adapter, error) { + if config == nil { + return nil, errors.New("config cannot be nil") + } + a := &Adapter{} - a.network = network - a.address = address - a.key = key - a.username = username - a.password = password - // Open the DB, create it if not existed. - err := a.open() + // Set default key if not provided + if config.Key == "" { + a.key = "casbin_rules" + } else { + a.key = config.Key + } + + // If a pool is provided, use it + if config.Pool != nil { + a._pool = config.Pool + } else { + // Otherwise, create a new connection + if config.Network == "" { + return nil, errors.New("network is required when not using a pool") + } + if config.Address == "" { + return nil, errors.New("address is required when not using a pool") + } + + a.network = config.Network + a.address = config.Address + a.username = config.Username + a.password = config.Password + a.tlsConfig = config.TLSConfig + + // Open the DB connection + err := a.open() + if err != nil { + return nil, err + } + } // Call the destructor when the object is released. runtime.SetFinalizer(a, finalizer) - return a, err + return a, nil } -// NewAdapter is the constructor for Adapter. -func NewAdapter(network string, address string) (*Adapter, error) { - return newAdapter(network, address, "casbin_rules", "", "") +// Legacy constructor functions (deprecated) +// These are kept for backward compatibility but should be avoided in new code + +// NewAdapterBasic is the basic constructor for Adapter. +// Deprecated: Use NewAdapter with Config struct instead. +func NewAdapterBasic(network string, address string) (*Adapter, error) { + config := &Config{ + Network: network, + Address: address, + } + return NewAdapter(config) } +// NewAdapterWithUser creates adapter with user credentials. +// Deprecated: Use NewAdapter with Config struct instead. func NewAdapterWithUser(network string, address string, username string, password string) (*Adapter, error) { - return newAdapter(network, address, "casbin_rules", username, password) + config := &Config{ + Network: network, + Address: address, + Username: username, + Password: password, + } + return NewAdapter(config) } -// NewAdapterWithPassword is the constructor for Adapter. +// NewAdapterWithPassword creates adapter with password authentication. +// Deprecated: Use NewAdapter with Config struct instead. func NewAdapterWithPassword(network string, address string, password string) (*Adapter, error) { - return newAdapter(network, address, "casbin_rules", "", password) + config := &Config{ + Network: network, + Address: address, + Password: password, + } + return NewAdapter(config) } -// NewAdapterWithKey is the constructor for Adapter. +// NewAdapterWithKey creates adapter with custom key. +// Deprecated: Use NewAdapter with Config struct instead. func NewAdapterWithKey(network string, address string, key string) (*Adapter, error) { - return newAdapter(network, address, key, "", "") + config := &Config{ + Network: network, + Address: address, + Key: key, + } + return NewAdapter(config) } -// NewAdapterWithPool is the constructor for Adapter. +// NewAdapterWithPool creates adapter with connection pool. +// Deprecated: Use NewAdapter with Config struct instead. func NewAdapterWithPool(pool *redis.Pool) (*Adapter, error) { - a := &Adapter{} - a.key = "casbin_rules" - - conn := pool.Get() - defer a.release(conn) - - a._conn = conn - a._pool = pool - - // Call the destructor when the object is released. - runtime.SetFinalizer(a, finalizer) - - return a, nil + config := &Config{ + Pool: pool, + } + return NewAdapter(config) } -// NewAdapterWithPoolAndOptions is the constructor for Adapter. +// NewAdapterWithPoolAndOptions creates adapter with pool and options. +// Deprecated: Use NewAdapter with Config struct instead. func NewAdapterWithPoolAndOptions(pool *redis.Pool, options ...Option) (*Adapter, error) { - a := &Adapter{} - a.key = "casbin_rules" + config := &Config{ + Pool: pool, + } + a, err := NewAdapter(config) + if err != nil { + return nil, err + } + + // Apply options for backward compatibility for _, option := range options { option(a) } - conn := pool.Get() - defer a.release(conn) - - a._conn = conn - a._pool = pool - - // Call the destructor when the object is released. - runtime.SetFinalizer(a, finalizer) - return a, nil } type Option func(*Adapter) +// NewAdapterWithOption creates adapter with options pattern. +// Deprecated: Use NewAdapter with Config struct instead. func NewAdapterWithOption(options ...Option) (*Adapter, error) { a := &Adapter{} for _, option := range options { option(a) } - // Open the DB, create it if not existed. - err := a.open() - // Call the destructor when the object is released. - runtime.SetFinalizer(a, finalizer) + // Convert to new config-based approach + config := &Config{ + Network: a.network, + Address: a.address, + Key: a.key, + Username: a.username, + Password: a.password, + TLSConfig: a.tlsConfig, + } - return a, err + return NewAdapter(config) } func WithAddress(address string) Option { @@ -191,6 +264,7 @@ func WithNetwork(network string) Option { a.network = network } } + func WithKey(key string) Option { return func(a *Adapter) { a.key = key diff --git a/adapter_config_test.go b/adapter_config_test.go new file mode 100644 index 0000000..226e8e0 --- /dev/null +++ b/adapter_config_test.go @@ -0,0 +1,177 @@ +// Copyright 2025 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redisadapter + +import ( + "testing" + + "github.com/casbin/casbin/v2" + "github.com/gomodule/redigo/redis" +) + +func TestNewAdapterWithConfig(t *testing.T) { + // Test basic configuration + config := &Config{ + Network: "tcp", + Address: "127.0.0.1:6379", + } + a, _ := NewAdapter(config) + + testSaveLoad(t, a) + testAutoSave(t, a) + testFilteredPolicy(t, a) + testAddPolicies(t, a) + testRemovePolicies(t, a) + testUpdatePolicies(t, a) + testUpdateFilteredPolicies(t, a) +} + +func TestNewAdapterWithPool(t *testing.T) { + // Test with connection pool + pool := &redis.Pool{ + MaxIdle: 3, + MaxActive: 5, + Dial: func() (redis.Conn, error) { + return redis.Dial("tcp", "127.0.0.1:6379") + }, + } + config := &Config{ + Pool: pool, + Key: "pool_test_rules", + } + a, err := NewAdapter(config) + if err != nil { + t.Fatal(err) + } + + testSaveLoad(t, a) + testAutoSave(t, a) + testFilteredPolicy(t, a) + testAddPolicies(t, a) + testRemovePolicies(t, a) + testUpdatePolicies(t, a) + testUpdateFilteredPolicies(t, a) +} + +func TestNewAdapterErrorCases(t *testing.T) { + // Test error cases + _, err := NewAdapter(nil) + if err == nil { + t.Error("NewAdapter should fail with nil config") + } + + config := &Config{ + Network: "", + Address: "127.0.0.1:6379", + } + _, err = NewAdapter(config) + if err == nil { + t.Error("NewAdapter should fail with empty network") + } + + config = &Config{ + Network: "tcp", + Address: "", + } + _, err = NewAdapter(config) + if err == nil { + t.Error("NewAdapter should fail with empty address") + } +} + +func TestNewAdapterWithPassword(t *testing.T) { + // Test with password authentication + a, err := NewAdapterWithPassword("tcp", "127.0.0.1:6379", "testpass") + if err != nil { + t.Skipf("Password authentication test skipped (Redis may not have auth configured): %v", err) + } + + testSaveLoad(t, a) + testAutoSave(t, a) + testFilteredPolicy(t, a) + testAddPolicies(t, a) + testRemovePolicies(t, a) + testUpdatePolicies(t, a) + testUpdateFilteredPolicies(t, a) +} + +func TestNewAdapterWithUser(t *testing.T) { + // Test with username and password authentication + a, err := NewAdapterWithUser("tcp", "127.0.0.1:6379", "testuser", "testpass") + if err != nil { + t.Skipf("User authentication test skipped (Redis may not have auth configured): %v", err) + } + + testSaveLoad(t, a) + testAutoSave(t, a) + testFilteredPolicy(t, a) + testAddPolicies(t, a) + testRemovePolicies(t, a) + testUpdatePolicies(t, a) + testUpdateFilteredPolicies(t, a) +} + +func TestNewAdapterWithKey(t *testing.T) { + // Test with custom key + a, err := NewAdapterWithKey("tcp", "127.0.0.1:6379", "custom_casbin_rules") + if err != nil { + t.Fatal(err) + } + + testSaveLoad(t, a) + testAutoSave(t, a) + testFilteredPolicy(t, a) + testAddPolicies(t, a) + testRemovePolicies(t, a) + testUpdatePolicies(t, a) + testUpdateFilteredPolicies(t, a) +} + +func TestFilterFunctionality(t *testing.T) { + // Test various filter functionality + a, err := NewAdapterBasic("tcp", "127.0.0.1:6379") + if err != nil { + t.Fatal(err) + } + + // Initialize policy + initPolicy(t, a) + + // Create enforcer + e, _ := casbin.NewEnforcer("examples/rbac_model.conf") + e.SetAdapter(a) + + // Test filtering by subject + filter := &Filter{V0: []string{"alice"}} + err = a.LoadFilteredPolicy(e.GetModel(), filter) + if err != nil { + t.Fatal(err) + } + + policies := e.GetPolicy() + if len(policies) == 0 { + t.Log("No policies found for alice (this might be expected)") + } + + // Test filtering by object + filter = &Filter{V0: []string{"", "data1"}} + err = a.LoadFilteredPolicy(e.GetModel(), filter) + if err != nil { + t.Fatal(err) + } + + policies = e.GetPolicy() + t.Logf("Found %d policies for data1", len(policies)) +} diff --git a/adapter_test.go b/adapter_test.go index 0c3769e..e69f8a5 100644 --- a/adapter_test.go +++ b/adapter_test.go @@ -354,7 +354,7 @@ func arrayEqualsWithoutOrder(a [][]string, b [][]string) bool { } func TestAdapters(t *testing.T) { - a, _ := NewAdapter("tcp", "127.0.0.1:6379") + a, _ := NewAdapterBasic("tcp", "127.0.0.1:6379") // Use the following if Redis has password like "123" // a, err := NewAdapterWithPassword("tcp", "127.0.0.1:6379", "123")