From d47d4df8e0617f6a9f20ef807b9a96ad5486a135 Mon Sep 17 00:00:00 2001 From: Sven Cludius Date: Fri, 8 Aug 2025 14:35:17 +0200 Subject: [PATCH] Add redis adapter support --- README.md | 2 +- config/connection_config_redis_example.json | 5 ++ go.mod | 5 +- go.sum | 12 ++- server/adapter.go | 40 +++++++++ server/adapter_test.go | 89 +++++++++++++++++++++ 6 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 config/connection_config_redis_example.json diff --git a/README.md b/README.md index bb5dd5f..7266c24 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Alternatively, you can also [run it from an IDE](https://github.com/casbin/casbi Similar to Casbin, Casbin-Server also uses adapters to provide policy storage. However, because Casbin-Server is a service instead of a library, the adapters have to be implemented inside Casbin-Server. As Golang is a static language, each adapter requires to import 3rd-party library for that database. We cannot import all those 3rd-party libraries inside Casbin-Server's code, as it causes dependency overhead. -For now, only [Gorm Adapter](https://github.com/casbin/casbin-server/blob/master/server/adapter.go) is built-in with ``mssql``, ``mysql``, ``postgres`` imports all commented. If you want to use ``Gorm Adapter`` with one of those databases, you should uncomment that import line, or add your own import, or even use another adapter by modifying Casbin-Server's source code. +For now, [Gorm Adapter](https://github.com/casbin/casbin-server/blob/master/server/adapter.go) (with ``mssql``, ``mysql``, ``postgres``), MongoDB und Redis Adapter are built-in imports all commented. If you want to use ``Gorm Adapter`` with one of those databases, you should uncomment that import line, or add your own import, or even use another adapter by modifying Casbin-Server's source code. To allow Casbin-Server to be production-ready, the adapter configuration supports environment variables. For example, assume we created a ``postgres`` database for our RBAC model and want Casbin-Server to use it. Assuming that the environment in which the Casbin-Server runs contains the necessary variables, we can simply use the ``$ENV_VAR`` notation to provide these to the adapter. diff --git a/config/connection_config_redis_example.json b/config/connection_config_redis_example.json new file mode 100644 index 0000000..ce1da7a --- /dev/null +++ b/config/connection_config_redis_example.json @@ -0,0 +1,5 @@ +{ + "driver": "redis", + "connection": "localhost:6379", + "enforcer": "examples/rbac_model.conf" +} diff --git a/go.mod b/go.mod index 2881ab0..8397e80 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,17 @@ module github.com/casbin/casbin-server go 1.19 require ( + github.com/alicebob/miniredis/v2 v2.35.0 github.com/casbin/casbin/v2 v2.100.0 github.com/casbin/gorm-adapter/v3 v3.14.0 github.com/casbin/mongodb-adapter/v3 v3.7.0 + github.com/casbin/redis-adapter/v3 v3.6.0 github.com/stretchr/testify v1.8.0 google.golang.org/grpc v1.42.0 google.golang.org/protobuf v1.27.1 ) require ( - github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/casbin/govaluate v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -23,6 +24,7 @@ require ( github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/protobuf v1.5.0 // indirect github.com/golang/snappy v0.0.1 // indirect + github.com/gomodule/redigo v1.8.9 // indirect github.com/google/uuid v1.3.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.13.0 // indirect @@ -45,6 +47,7 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect go.mongodb.org/mongo-driver v1.12.0 // indirect golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect diff --git a/go.sum b/go.sum index 7e142e0..e191134 100644 --- a/go.sum +++ b/go.sum @@ -5,16 +5,16 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSa github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= +github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/casbin/casbin/v2 v2.55.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= -github.com/casbin/casbin/v2 v2.71.1 h1:LRHyqM0S1LzM/K59PmfUIN0ZJfLgcOjL4OhOQI/FNXU= -github.com/casbin/casbin/v2 v2.71.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/casbin/casbin/v2 v2.60.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= github.com/casbin/casbin/v2 v2.100.0 h1:aeugSNjjHfCrgA22nHkVvw2xsscboHv5r0a13ljQKGQ= github.com/casbin/casbin/v2 v2.100.0/go.mod h1:LO7YPez4dX3LgoTCqSQAleQDo0S0BeZBDxYnPUl95Ng= github.com/casbin/gorm-adapter/v3 v3.14.0 h1:zZ6AIiNHJZ3ntdf5RBrqD+0Cb4UO+uKFk79R9yJ7mpw= @@ -23,6 +23,8 @@ github.com/casbin/govaluate v1.2.0 h1:wXCXFmqyY+1RwiKfYo3jMKyrtZmOL3kHwaqDyCPOYa github.com/casbin/govaluate v1.2.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/casbin/mongodb-adapter/v3 v3.7.0 h1:w9c3bea1BGK4eZTAmk17JkY52yv/xSZDSHKji8q+z6E= github.com/casbin/mongodb-adapter/v3 v3.7.0/go.mod h1:F1mu4ojoJVE/8VhIMxMedhjfwRDdIXgANYs6Sd0MgVA= +github.com/casbin/redis-adapter/v3 v3.6.0 h1:JY9eUJeF428e87s1HvNZxrgUHLGFcJqNiDg5JUftkas= +github.com/casbin/redis-adapter/v3 v3.6.0/go.mod h1:SGL+D0Gx7dQIR8frcnZeq8E0pT2WYuJ05gcEH4c2elY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -89,6 +91,8 @@ github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -234,6 +238,8 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= diff --git a/server/adapter.go b/server/adapter.go index 0fb5c24..344fd42 100644 --- a/server/adapter.go +++ b/server/adapter.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "os" "regexp" "strings" @@ -27,10 +28,31 @@ import ( fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" gormadapter "github.com/casbin/gorm-adapter/v3" mongodbadapter "github.com/casbin/mongodb-adapter/v3" + redisadapter "github.com/casbin/redis-adapter/v3" ) var errDriverName = errors.New("currently supported DriverName: file | mysql | postgres | mssql") +func parseRedisUrl(redisURL string) (host, port, username, password string, err error) { + if redisURL == "" { + return "", "", "", "", errors.New("redis URL cannot be empty") + } + if !strings.Contains(redisURL, "://") { + redisURL = "redis://" + redisURL + } + u, err := url.Parse(redisURL) + if err != nil { + return "", "", "", "", err + } + host = u.Hostname() + port = u.Port() + if u.User != nil { + username = u.User.Username() + password, _ = u.User.Password() + } + return host, port, username, password, nil +} + func newAdapter(in *pb.NewAdapterRequest) (persist.Adapter, error) { var a persist.Adapter in = checkLocalConfig(in) @@ -45,6 +67,24 @@ func newAdapter(in *pb.NewAdapterRequest) (persist.Adapter, error) { if err != nil { return nil, err } + case "redis": + var err error + host, port, username, password, err := parseRedisUrl(in.ConnectString) + if err != nil { + return nil, err + } + hostWithPort := fmt.Sprintf("%s:%s", host, port) + + config := &redisadapter.Config{ + Network: "tcp", + Address: hostWithPort, + Username: username, + Password: password, + } + a, err = redisadapter.NewAdapter(config) + if err != nil { + return nil, err + } default: var support = false for _, driverName := range supportDriverNames { diff --git a/server/adapter_test.go b/server/adapter_test.go index 0337b6d..a78bf1e 100644 --- a/server/adapter_test.go +++ b/server/adapter_test.go @@ -4,6 +4,9 @@ import ( "os" "testing" + miniredis "github.com/alicebob/miniredis/v2" + + pb "github.com/casbin/casbin-server/proto" "github.com/stretchr/testify/assert" ) @@ -13,3 +16,89 @@ func TestGetLocalConfig(t *testing.T) { os.Setenv(configFilePathEnvironmentVariable, "dir/custom_path.json") assert.Equal(t, "dir/custom_path.json", getLocalConfigPath()) } + +func runFakeRedis(username string, password string) (host string, port string, err error) { + s, err := miniredis.Run() + if err != nil { + return "", "", err + } + if username != "" && password != "" { + s.RequireUserAuth(username, password) + } + return s.Host(), s.Port(), err +} + +func TestRedisAdapterConfig(t *testing.T) { + os.Setenv(configFilePathEnvironmentVariable, "../config/connection_config.json") + + host, port, err := runFakeRedis("", "") + + in := &pb.NewAdapterRequest{ + DriverName: "redis", + ConnectString: "redis://" + host + ":" + port, + } + + a, err := newAdapter(in) + assert.NoError(t, err, "should create redis adapter without error") + assert.NotNil(t, a, "adapter should not be nil") +} + +func TestRedisAdapterConfigWithUsernameAndPassword(t *testing.T) { + os.Setenv(configFilePathEnvironmentVariable, "../config/connection_config.json") + + username, password := "foo", "bar" + host, port, err := runFakeRedis(username, password) + + in := &pb.NewAdapterRequest{ + DriverName: "redis", + ConnectString: "redis://" + username + ":" + password + "@" + host + ":" + port, + } + + a, err := newAdapter(in) + assert.NoError(t, err, "should create redis adapter without error") + assert.NotNil(t, a, "adapter should not be nil") +} + +func TestRedisAdapterConfigWithoutPrefix(t *testing.T) { + os.Setenv(configFilePathEnvironmentVariable, "../config/connection_config.json") + + host, port, err := runFakeRedis("", "") + + in := &pb.NewAdapterRequest{ + DriverName: "redis", + ConnectString: host + ":" + port, + } + + a, err := newAdapter(in) + assert.NoError(t, err, "should create redis adapter without error") + assert.NotNil(t, a, "adapter should not be nil") +} + +func TestInvalidRedisAdapterConfig(t *testing.T) { + os.Setenv(configFilePathEnvironmentVariable, "../config/connection_config.json") + + _, _, err := runFakeRedis("", "") + + in := &pb.NewAdapterRequest{ + DriverName: "redis", + ConnectString: "invalid-address", + } + + a, err := newAdapter(in) + assert.Error(t, err, "should cause an redis adapter without error") + assert.ErrorContains(t, err, "dial tcp: lookup invalid-address") + assert.Nil(t, a, "adapter should be nil") +} + +func TestRedisAdapterConfigReturnDefaultFallback(t *testing.T) { + os.Setenv(configFilePathEnvironmentVariable, "../config/connection_config.json") + + in := &pb.NewAdapterRequest{ + DriverName: "redis", + ConnectString: "", + } + + a, err := newAdapter(in) + assert.NoError(t, err, "should create file default adapter without error") + assert.NotNil(t, a, "adapter should not be nil") +}