├── LICENSE ├── README.md ├── config.go ├── example ├── main.go ├── model.conf └── policy.csv ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── options.go └── utils.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alireza Salary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAS BEEN MOVED TO https://github.com/gofiber/contrib/tree/main/casbin 2 | 3 | ### Casbin 4 | Casbin middleware for Fiber 5 | 6 | ### Install 7 | ``` 8 | go get -u github.com/gofiber/fiber/v2 9 | go get -u github.com/arsmn/fiber-casbin/v2 10 | ``` 11 | choose an adapter from [here](https://casbin.org/docs/en/adapters) 12 | ``` 13 | go get -u github.com/casbin/xorm-adapter 14 | ``` 15 | 16 | ### Signature 17 | ```go 18 | fibercasbin.New(config ...fibercasbin.Config) *fibercasbin.CasbinMiddleware 19 | ``` 20 | 21 | ### Config 22 | | Property | Type | Description | Default | 23 | | :--- | :--- | :--- | :--- | 24 | | ModelFilePath | `string` | Model file path | `"./model.conf"` | 25 | | PolicyAdapter | `persist.Adapter` | Database adapter for policies | `./policy.csv` | 26 | | Enforcer | `*casbin.Enforcer` | Custom casbin enforcer | `Middleware generated enforcer using ModelFilePath & PolicyAdapter` | 27 | | Lookup | `func(*fiber.Ctx) string` | Look up for current subject | `""` | 28 | | Unauthorized | `func(*fiber.Ctx) error` | Response body for unauthorized responses | `Unauthorized` | 29 | | Forbidden | `func(*fiber.Ctx) error` | Response body for forbidden responses | `Forbidden` | 30 | 31 | ### Examples 32 | - [Gorm Adapter](https://github.com/svcg/-fiber_casbin_demo) 33 | - [File Adapter](https://github.com/arsmn/fiber-casbin/tree/master/example) 34 | 35 | ### CustomPermission 36 | 37 | ```go 38 | package main 39 | 40 | import ( 41 | "github.com/gofiber/fiber/v2" 42 | "github.com/arsmn/fiber-casbin/v2" 43 | _ "github.com/go-sql-driver/mysql" 44 | "github.com/casbin/xorm-adapter/v2" 45 | ) 46 | 47 | func main() { 48 | app := fiber.New() 49 | 50 | authz := fibercasbin.New(fibercasbin.Config{ 51 | ModelFilePath: "path/to/rbac_model.conf", 52 | PolicyAdapter: xormadapter.NewAdapter("mysql", "root:@tcp(127.0.0.1:3306)/"), 53 | Lookup: func(c *fiber.Ctx) string { 54 | // fetch authenticated user subject 55 | }, 56 | }) 57 | 58 | app.Post("/blog", 59 | authz.RequiresPermissions([]string{"blog:create"}, fibercasbin.WithValidationRule(fibercasbin.MatchAllRule)), 60 | func(c *fiber.Ctx) error { 61 | // your handler 62 | }, 63 | ) 64 | 65 | app.Delete("/blog/:id", 66 | authz.RequiresPermissions([]string{"blog:create", "blog:delete"}, fibercasbin.WithValidationRule(fibercasbin.AtLeastOneRule)), 67 | func(c *fiber.Ctx) error { 68 | // your handler 69 | }, 70 | ) 71 | 72 | app.Listen(":8080") 73 | } 74 | ``` 75 | 76 | ### RoutePermission 77 | 78 | ```go 79 | package main 80 | 81 | import ( 82 | "github.com/gofiber/fiber/v2" 83 | "github.com/arsmn/fiber-casbin/v2" 84 | _ "github.com/go-sql-driver/mysql" 85 | "github.com/casbin/xorm-adapter/v2" 86 | ) 87 | 88 | func main() { 89 | app := fiber.New() 90 | 91 | authz := fibercasbin.New(fibercasbin.Config{ 92 | ModelFilePath: "path/to/rbac_model.conf", 93 | PolicyAdapter: xormadapter.NewAdapter("mysql", "root:@tcp(127.0.0.1:3306)/"), 94 | Lookup: func(c *fiber.Ctx) string { 95 | // fetch authenticated user subject 96 | }, 97 | }) 98 | 99 | // check permission with Method and Path 100 | app.Post("/blog", 101 | authz.RoutePermission(), 102 | func(c *fiber.Ctx) error { 103 | // your handler 104 | }, 105 | ) 106 | 107 | app.Listen(":8080") 108 | } 109 | ``` 110 | 111 | ### RoleAuthorization 112 | 113 | ```go 114 | package main 115 | 116 | import ( 117 | "github.com/gofiber/fiber/v2" 118 | "github.com/arsmn/fiber-casbin/v2" 119 | _ "github.com/go-sql-driver/mysql" 120 | "github.com/casbin/xorm-adapter/v2" 121 | ) 122 | 123 | func main() { 124 | app := fiber.New() 125 | 126 | authz := fibercasbin.New(fibercasbin.Config{ 127 | ModelFilePath: "path/to/rbac_model.conf", 128 | PolicyAdapter: xormadapter.NewAdapter("mysql", "root:@tcp(127.0.0.1:3306)/"), 129 | Lookup: func(c *fiber.Ctx) string { 130 | // fetch authenticated user subject 131 | }, 132 | }) 133 | 134 | app.Put("/blog/:id", 135 | authz.RequiresRoles([]string{"admin"}), 136 | func(c *fiber.Ctx) error { 137 | // your handler 138 | }, 139 | ) 140 | 141 | app.Listen(":8080") 142 | } 143 | ``` 144 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package fibercasbin 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/casbin/casbin/v2" 7 | "github.com/casbin/casbin/v2/persist" 8 | fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" 9 | "github.com/gofiber/fiber/v2" 10 | ) 11 | 12 | // Config holds the configuration for the middleware 13 | type Config struct { 14 | // ModelFilePath is path to model file for Casbin. 15 | // Optional. Default: "./model.conf". 16 | ModelFilePath string 17 | 18 | // PolicyAdapter is an interface for different persistent providers. 19 | // Optional. Default: fileadapter.NewAdapter("./policy.csv"). 20 | PolicyAdapter persist.Adapter 21 | 22 | // Enforcer is an enforcer. If you want to use your own enforcer. 23 | // Optional. Default: nil 24 | Enforcer *casbin.Enforcer 25 | 26 | // Lookup is a function that is used to look up current subject. 27 | // An empty string is considered as unauthenticated user. 28 | // Optional. Default: func(c *fiber.Ctx) string { return "" } 29 | Lookup func(*fiber.Ctx) string 30 | 31 | // Unauthorized defines the response body for unauthorized responses. 32 | // Optional. Default: func(c *fiber.Ctx) error { return c.SendStatus(401) } 33 | Unauthorized fiber.Handler 34 | 35 | // Forbidden defines the response body for forbidden responses. 36 | // Optional. Default: func(c *fiber.Ctx) error { return c.SendStatus(403) } 37 | Forbidden fiber.Handler 38 | } 39 | 40 | var ConfigDefault = Config{ 41 | ModelFilePath: "./model.conf", 42 | PolicyAdapter: fileadapter.NewAdapter("./policy.csv"), 43 | Lookup: func(c *fiber.Ctx) string { return "" }, 44 | Unauthorized: func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusUnauthorized) }, 45 | Forbidden: func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusForbidden) }, 46 | } 47 | 48 | // Helper function to set default values 49 | func configDefault(config ...Config) Config { 50 | // Return default config if nothing provided 51 | if len(config) < 1 { 52 | return ConfigDefault 53 | } 54 | 55 | // Override default config 56 | cfg := config[0] 57 | 58 | if cfg.Enforcer == nil { 59 | if cfg.ModelFilePath == "" { 60 | cfg.ModelFilePath = ConfigDefault.ModelFilePath 61 | } 62 | 63 | if cfg.PolicyAdapter == nil { 64 | cfg.PolicyAdapter = ConfigDefault.PolicyAdapter 65 | } 66 | 67 | enforcer, err := casbin.NewEnforcer(cfg.ModelFilePath, cfg.PolicyAdapter) 68 | if err != nil { 69 | log.Fatalf("Fiber: Casbin middleware error -> %v", err) 70 | } 71 | 72 | cfg.Enforcer = enforcer 73 | } 74 | 75 | if cfg.Lookup == nil { 76 | cfg.Lookup = ConfigDefault.Lookup 77 | } 78 | 79 | if cfg.Unauthorized == nil { 80 | cfg.Unauthorized = ConfigDefault.Unauthorized 81 | } 82 | 83 | if cfg.Forbidden == nil { 84 | cfg.Forbidden = ConfigDefault.Forbidden 85 | } 86 | 87 | return cfg 88 | } 89 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | fibercasbin "github.com/arsmn/fiber-casbin/v2" 7 | fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" 8 | "github.com/gofiber/fiber/v2" 9 | ) 10 | 11 | func main() { 12 | app := fiber.New() 13 | 14 | authz := fibercasbin.New(fibercasbin.Config{ 15 | ModelFilePath: "model.conf", 16 | PolicyAdapter: fileadapter.NewAdapter("policy.csv"), 17 | Lookup: func(c *fiber.Ctx) string { 18 | // get subject from BasicAuth, JWT, Cookie etc in real world 19 | return "alice" 20 | }, 21 | }) 22 | 23 | app.Post("/blog", 24 | authz.RequiresPermissions([]string{"blog:create"}), 25 | func(c *fiber.Ctx) error { 26 | return c.SendString("Blog created") 27 | }, 28 | ) 29 | 30 | app.Put("/blog/:id", 31 | authz.RequiresRoles([]string{"admin"}), 32 | func(c *fiber.Ctx) error { 33 | return c.SendString(fmt.Sprintf("Blog updated with Id: %s", c.Params("id"))) 34 | }, 35 | ) 36 | 37 | app.Listen(":8080") 38 | } 39 | -------------------------------------------------------------------------------- /example/model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act -------------------------------------------------------------------------------- /example/policy.csv: -------------------------------------------------------------------------------- 1 | p,admin,blog,create 2 | p,admin,blog,update 3 | p,admin,blog,delete 4 | p,user,comment,create 5 | p,user,comment,delete 6 | 7 | p,admin,/blog,POST 8 | p,admin,/blog/1,PUT 9 | p,admin,/blog/1,DELETE 10 | p,user,/comment,POST 11 | 12 | 13 | g,alice,admin 14 | g,alice,user 15 | g,bob,user -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/arsmn/fiber-casbin/v2 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/casbin/casbin/v2 v2.43.1 7 | github.com/gofiber/fiber/v2 v2.31.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= 2 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 3 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 4 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 5 | github.com/casbin/casbin/v2 v2.43.1 h1:lAPFgWZf2XLrItMHzMb+fCW0TK3eiA3PCqPXYkgEHYI= 6 | github.com/casbin/casbin/v2 v2.43.1/go.mod h1:sEL80qBYTbd+BPeL4iyvwYzFT3qwLaESq5aFKVLbLfA= 7 | github.com/gofiber/fiber/v2 v2.31.0 h1:M2rWPQbD5fDVAjcoOLjKRXTIlHesI5Eq7I5FEQPt4Ow= 8 | github.com/gofiber/fiber/v2 v2.31.0/go.mod h1:1Ega6O199a3Y7yDGuM9FyXDPYQfv+7/y48wl6WCwUF4= 9 | github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 10 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 11 | github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= 12 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 13 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 14 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 15 | github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= 16 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 17 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 18 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 19 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 20 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 21 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 22 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 23 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 24 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 25 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 26 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= 31 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 33 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 34 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 35 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 36 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 37 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 38 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 39 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package fibercasbin 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | ) 6 | 7 | // CasbinMiddleware ... 8 | type CasbinMiddleware struct { 9 | config Config 10 | } 11 | 12 | // New creates an authorization middleware for use in Fiber 13 | func New(config ...Config) *CasbinMiddleware { 14 | return &CasbinMiddleware{ 15 | config: configDefault(config...), 16 | } 17 | } 18 | 19 | // RequiresPermissions tries to find the current subject and determine if the 20 | // subject has the required permissions according to predefined Casbin policies. 21 | func (cm *CasbinMiddleware) RequiresPermissions(permissions []string, opts ...Option) fiber.Handler { 22 | options := optionsDefault(opts...) 23 | 24 | return func(c *fiber.Ctx) error { 25 | if len(permissions) == 0 { 26 | return c.Next() 27 | } 28 | 29 | sub := cm.config.Lookup(c) 30 | if len(sub) == 0 { 31 | return cm.config.Unauthorized(c) 32 | } 33 | 34 | if options.ValidationRule == MatchAllRule { 35 | for _, permission := range permissions { 36 | vals := append([]string{sub}, options.PermissionParser(permission)...) 37 | if ok, err := cm.config.Enforcer.Enforce(stringSliceToInterfaceSlice(vals)...); err != nil { 38 | return c.SendStatus(fiber.StatusInternalServerError) 39 | } else if !ok { 40 | return cm.config.Forbidden(c) 41 | } 42 | } 43 | return c.Next() 44 | } else if options.ValidationRule == AtLeastOneRule { 45 | for _, permission := range permissions { 46 | vals := append([]string{sub}, options.PermissionParser(permission)...) 47 | if ok, err := cm.config.Enforcer.Enforce(stringSliceToInterfaceSlice(vals)...); err != nil { 48 | return c.SendStatus(fiber.StatusInternalServerError) 49 | } else if ok { 50 | return c.Next() 51 | } 52 | } 53 | return cm.config.Forbidden(c) 54 | } 55 | 56 | return c.Next() 57 | } 58 | } 59 | 60 | // RoutePermission tries to find the current subject and determine if the 61 | // subject has the required permissions according to predefined Casbin policies. 62 | // This method uses http Path and Method as object and action. 63 | func (cm *CasbinMiddleware) RoutePermission() fiber.Handler { 64 | return func(c *fiber.Ctx) error { 65 | sub := cm.config.Lookup(c) 66 | if len(sub) == 0 { 67 | return cm.config.Unauthorized(c) 68 | } 69 | 70 | if ok, err := cm.config.Enforcer.Enforce(sub, c.Path(), c.Method()); err != nil { 71 | return c.SendStatus(fiber.StatusInternalServerError) 72 | } else if !ok { 73 | return cm.config.Forbidden(c) 74 | } 75 | 76 | return c.Next() 77 | } 78 | } 79 | 80 | // RequiresRoles tries to find the current subject and determine if the 81 | // subject has the required roles according to predefined Casbin policies. 82 | func (cm *CasbinMiddleware) RequiresRoles(roles []string, opts ...Option) fiber.Handler { 83 | options := optionsDefault(opts...) 84 | 85 | return func(c *fiber.Ctx) error { 86 | if len(roles) == 0 { 87 | return c.Next() 88 | } 89 | 90 | sub := cm.config.Lookup(c) 91 | if len(sub) == 0 { 92 | return cm.config.Unauthorized(c) 93 | } 94 | 95 | userRoles, err := cm.config.Enforcer.GetRolesForUser(sub) 96 | if err != nil { 97 | return c.SendStatus(fiber.StatusInternalServerError) 98 | } 99 | 100 | if options.ValidationRule == MatchAllRule { 101 | for _, role := range roles { 102 | if !containsString(userRoles, role) { 103 | return cm.config.Forbidden(c) 104 | } 105 | } 106 | return c.Next() 107 | } else if options.ValidationRule == AtLeastOneRule { 108 | for _, role := range roles { 109 | if containsString(userRoles, role) { 110 | return c.Next() 111 | } 112 | } 113 | return cm.config.Forbidden(c) 114 | } 115 | 116 | return c.Next() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package fibercasbin 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/casbin/casbin/v2" 9 | 10 | fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" 11 | "github.com/gofiber/fiber/v2" 12 | ) 13 | 14 | var ( 15 | subjectAlice = func(c *fiber.Ctx) string { return "alice" } 16 | subjectBob = func(c *fiber.Ctx) string { return "bob" } 17 | subjectNil = func(c *fiber.Ctx) string { return "" } 18 | ) 19 | 20 | func Test_RequiresPermission(t *testing.T) { 21 | tests := []struct { 22 | name string 23 | lookup func(*fiber.Ctx) string 24 | permissions []string 25 | opts []Option 26 | statusCode int 27 | }{ 28 | { 29 | name: "alice has permission to create blog", 30 | lookup: subjectAlice, 31 | permissions: []string{"blog:create"}, 32 | opts: []Option{WithValidationRule(MatchAllRule)}, 33 | statusCode: 200, 34 | }, 35 | { 36 | name: "alice has permission to create blog", 37 | lookup: subjectAlice, 38 | permissions: []string{"blog:create"}, 39 | opts: []Option{WithValidationRule(AtLeastOneRule)}, 40 | statusCode: 200, 41 | }, 42 | { 43 | name: "alice has permission to create and update blog", 44 | lookup: subjectAlice, 45 | permissions: []string{"blog:create", "blog:update"}, 46 | opts: []Option{WithValidationRule(MatchAllRule)}, 47 | statusCode: 200, 48 | }, 49 | { 50 | name: "alice has permission to create comment or blog", 51 | lookup: subjectAlice, 52 | permissions: []string{"comment:create", "blog:create"}, 53 | opts: []Option{WithValidationRule(AtLeastOneRule)}, 54 | statusCode: 200, 55 | }, 56 | { 57 | name: "bob has only permission to create comment", 58 | lookup: subjectBob, 59 | permissions: []string{"comment:create", "blog:create"}, 60 | opts: []Option{WithValidationRule(AtLeastOneRule)}, 61 | statusCode: 200, 62 | }, 63 | { 64 | name: "unauthenticated user has no permissions", 65 | lookup: subjectNil, 66 | permissions: []string{"comment:create"}, 67 | opts: []Option{WithValidationRule(MatchAllRule)}, 68 | statusCode: 401, 69 | }, 70 | { 71 | name: "bob has not permission to create blog", 72 | lookup: subjectBob, 73 | permissions: []string{"blog:create"}, 74 | opts: []Option{WithValidationRule(MatchAllRule)}, 75 | statusCode: 403, 76 | }, 77 | { 78 | name: "bob has not permission to delete blog", 79 | lookup: subjectBob, 80 | permissions: []string{"blog:delete"}, 81 | opts: []Option{WithValidationRule(MatchAllRule)}, 82 | statusCode: 403, 83 | }, 84 | { 85 | name: "invalid permission", 86 | lookup: subjectBob, 87 | permissions: []string{"unknown"}, 88 | opts: []Option{WithValidationRule(MatchAllRule)}, 89 | statusCode: 500, 90 | }, 91 | } 92 | 93 | for _, tt := range tests { 94 | app := *fiber.New() 95 | authz := New(Config{ 96 | ModelFilePath: "./example/model.conf", 97 | PolicyAdapter: fileadapter.NewAdapter("./example/policy.csv"), 98 | Lookup: tt.lookup, 99 | }) 100 | 101 | app.Post("/blog", 102 | authz.RequiresPermissions(tt.permissions, tt.opts...), 103 | func(c *fiber.Ctx) error { 104 | return c.SendStatus(fiber.StatusOK) 105 | }, 106 | ) 107 | 108 | t.Run(tt.name, func(t *testing.T) { 109 | req, _ := http.NewRequest("POST", "/blog", nil) 110 | resp, err := app.Test(req) 111 | if err != nil { 112 | t.Fatalf(`%s: %s`, t.Name(), err) 113 | } 114 | 115 | if resp.StatusCode != tt.statusCode { 116 | t.Fatalf(`%s: StatusCode: got %v - expected %v`, t.Name(), resp.StatusCode, tt.statusCode) 117 | } 118 | }) 119 | } 120 | } 121 | 122 | func Test_RequiresRoles(t *testing.T) { 123 | tests := []struct { 124 | name string 125 | lookup func(*fiber.Ctx) string 126 | roles []string 127 | opts []Option 128 | statusCode int 129 | }{ 130 | { 131 | name: "alice has user role", 132 | lookup: subjectAlice, 133 | roles: []string{"user"}, 134 | opts: []Option{WithValidationRule(MatchAllRule)}, 135 | statusCode: 200, 136 | }, 137 | { 138 | name: "alice has admin role", 139 | lookup: subjectAlice, 140 | roles: []string{"admin"}, 141 | opts: []Option{WithValidationRule(AtLeastOneRule)}, 142 | statusCode: 200, 143 | }, 144 | { 145 | name: "alice has both user and admin roles", 146 | lookup: subjectAlice, 147 | roles: []string{"user", "admin"}, 148 | opts: []Option{WithValidationRule(MatchAllRule)}, 149 | statusCode: 200, 150 | }, 151 | { 152 | name: "alice has both user and admin roles", 153 | lookup: subjectAlice, 154 | roles: []string{"user", "admin"}, 155 | opts: []Option{WithValidationRule(AtLeastOneRule)}, 156 | statusCode: 200, 157 | }, 158 | { 159 | name: "bob has only user role", 160 | lookup: subjectBob, 161 | roles: []string{"user"}, 162 | opts: []Option{WithValidationRule(AtLeastOneRule)}, 163 | statusCode: 200, 164 | }, 165 | { 166 | name: "unauthenticated user has no permissions", 167 | lookup: subjectNil, 168 | roles: []string{"user"}, 169 | opts: []Option{WithValidationRule(MatchAllRule)}, 170 | statusCode: 401, 171 | }, 172 | { 173 | name: "bob has not admin role", 174 | lookup: subjectBob, 175 | roles: []string{"admin"}, 176 | opts: []Option{WithValidationRule(MatchAllRule)}, 177 | statusCode: 403, 178 | }, 179 | { 180 | name: "bob has only user role", 181 | lookup: subjectBob, 182 | roles: []string{"admin", "user"}, 183 | opts: []Option{WithValidationRule(AtLeastOneRule)}, 184 | statusCode: 200, 185 | }, 186 | { 187 | name: "invalid role", 188 | lookup: subjectBob, 189 | roles: []string{"unknown"}, 190 | opts: []Option{WithValidationRule(MatchAllRule)}, 191 | statusCode: 403, 192 | }, 193 | } 194 | 195 | for _, tt := range tests { 196 | app := *fiber.New() 197 | authz := New(Config{ 198 | ModelFilePath: "./example/model.conf", 199 | PolicyAdapter: fileadapter.NewAdapter("./example/policy.csv"), 200 | Lookup: tt.lookup, 201 | }) 202 | 203 | app.Post("/blog", 204 | authz.RequiresRoles(tt.roles, tt.opts...), 205 | func(c *fiber.Ctx) error { 206 | return c.SendStatus(fiber.StatusOK) 207 | }, 208 | ) 209 | 210 | t.Run(tt.name, func(t *testing.T) { 211 | req, _ := http.NewRequest("POST", "/blog", nil) 212 | resp, err := app.Test(req) 213 | if err != nil { 214 | t.Fatalf(`%s: %s`, t.Name(), err) 215 | } 216 | 217 | if resp.StatusCode != tt.statusCode { 218 | t.Fatalf(`%s: StatusCode: got %v - expected %v`, t.Name(), resp.StatusCode, tt.statusCode) 219 | } 220 | }) 221 | } 222 | } 223 | 224 | func Test_RoutePermission(t *testing.T) { 225 | tests := []struct { 226 | name string 227 | url string 228 | method string 229 | subject string 230 | statusCode int 231 | }{ 232 | { 233 | name: "alice has permission to create blog", 234 | url: "/blog", 235 | method: "POST", 236 | subject: "alice", 237 | statusCode: 200, 238 | }, 239 | { 240 | name: "alice has permission to update blog", 241 | url: "/blog/1", 242 | method: "PUT", 243 | subject: "alice", 244 | statusCode: 200, 245 | }, 246 | { 247 | name: "bob has only permission to create comment", 248 | url: "/comment", 249 | method: "POST", 250 | subject: "bob", 251 | statusCode: 200, 252 | }, 253 | { 254 | name: "unauthenticated user has no permissions", 255 | url: "/", 256 | method: "POST", 257 | subject: "", 258 | statusCode: 401, 259 | }, 260 | { 261 | name: "bob has not permission to create blog", 262 | url: "/blog", 263 | method: "POST", 264 | subject: "bob", 265 | statusCode: 403, 266 | }, 267 | { 268 | name: "bob has not permission to delete blog", 269 | url: "/blog/1", 270 | method: "DELETE", 271 | subject: "bob", 272 | statusCode: 403, 273 | }, 274 | } 275 | 276 | app := *fiber.New() 277 | authz := New(Config{ 278 | ModelFilePath: "./example/model.conf", 279 | PolicyAdapter: fileadapter.NewAdapter("./example/policy.csv"), 280 | Lookup: func(c *fiber.Ctx) string { 281 | return c.Get("x-subject") 282 | }, 283 | }) 284 | 285 | app.Use(authz.RoutePermission()) 286 | app.Post("/blog", 287 | func(c *fiber.Ctx) error { 288 | return c.SendStatus(fiber.StatusOK) 289 | }, 290 | ) 291 | app.Put("/blog/:id", 292 | func(c *fiber.Ctx) error { 293 | return c.SendStatus(fiber.StatusOK) 294 | }, 295 | ) 296 | app.Delete("/blog/:id", 297 | func(c *fiber.Ctx) error { 298 | return c.SendStatus(fiber.StatusOK) 299 | }, 300 | ) 301 | app.Post("/comment", 302 | func(c *fiber.Ctx) error { 303 | return c.SendStatus(fiber.StatusOK) 304 | }, 305 | ) 306 | 307 | for _, tt := range tests { 308 | 309 | t.Run(tt.name, func(t *testing.T) { 310 | req, _ := http.NewRequest(tt.method, tt.url, nil) 311 | req.Header.Set("x-subject", tt.subject) 312 | resp, err := app.Test(req) 313 | if err != nil { 314 | t.Fatalf(`%s: %s`, t.Name(), err) 315 | } 316 | 317 | if resp.StatusCode != tt.statusCode { 318 | t.Fatalf(`%s: StatusCode: got %v - expected %v`, t.Name(), resp.StatusCode, tt.statusCode) 319 | } 320 | }) 321 | } 322 | } 323 | 324 | func Test_ModeEnforcer(t *testing.T) { 325 | tests := []struct { 326 | name string 327 | url string 328 | method string 329 | subject string 330 | statusCode int 331 | }{ 332 | { 333 | name: "alice has permission to create blog", 334 | url: "/blog", 335 | method: "POST", 336 | subject: "alice", 337 | statusCode: 200, 338 | }, 339 | { 340 | name: "alice has permission to update blog", 341 | url: "/blog/1", 342 | method: "PUT", 343 | subject: "alice", 344 | statusCode: 200, 345 | }, 346 | { 347 | name: "bob has only permission to create comment", 348 | url: "/comment", 349 | method: "POST", 350 | subject: "bob", 351 | statusCode: 200, 352 | }, 353 | { 354 | name: "unauthenticated user has no permissions", 355 | url: "/", 356 | method: "POST", 357 | subject: "", 358 | statusCode: 401, 359 | }, 360 | { 361 | name: "bob has not permission to create blog", 362 | url: "/blog", 363 | method: "POST", 364 | subject: "bob", 365 | statusCode: 403, 366 | }, 367 | { 368 | name: "bob has not permission to delete blog", 369 | url: "/blog/1", 370 | method: "DELETE", 371 | subject: "bob", 372 | statusCode: 403, 373 | }, 374 | } 375 | 376 | app := *fiber.New() 377 | 378 | enforcer, err := casbin.NewEnforcer("./example/model.conf", fileadapter.NewAdapter("./example/policy.csv")) 379 | if err != nil { 380 | log.Fatal(err) 381 | } 382 | 383 | authz := New(Config{ 384 | Enforcer: enforcer, 385 | Lookup: func(c *fiber.Ctx) string { 386 | return c.Get("x-subject") 387 | }, 388 | }) 389 | 390 | app.Use(authz.RoutePermission()) 391 | app.Post("/blog", 392 | func(c *fiber.Ctx) error { 393 | return c.SendStatus(fiber.StatusOK) 394 | }, 395 | ) 396 | app.Put("/blog/:id", 397 | func(c *fiber.Ctx) error { 398 | return c.SendStatus(fiber.StatusOK) 399 | }, 400 | ) 401 | app.Delete("/blog/:id", 402 | func(c *fiber.Ctx) error { 403 | return c.SendStatus(fiber.StatusOK) 404 | }, 405 | ) 406 | app.Post("/comment", 407 | func(c *fiber.Ctx) error { 408 | return c.SendStatus(fiber.StatusOK) 409 | }, 410 | ) 411 | 412 | for _, tt := range tests { 413 | 414 | t.Run(tt.name, func(t *testing.T) { 415 | req, _ := http.NewRequest(tt.method, tt.url, nil) 416 | req.Header.Set("x-subject", tt.subject) 417 | resp, err := app.Test(req) 418 | if err != nil { 419 | t.Fatalf(`%s: %s`, t.Name(), err) 420 | } 421 | 422 | if resp.StatusCode != tt.statusCode { 423 | t.Fatalf(`%s: StatusCode: got %v - expected %v`, t.Name(), resp.StatusCode, tt.statusCode) 424 | } 425 | }) 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package fibercasbin 2 | 3 | import "strings" 4 | 5 | const ( 6 | MatchAllRule ValidationRule = iota 7 | AtLeastOneRule 8 | ) 9 | 10 | var OptionsDefault = Options{ 11 | ValidationRule: MatchAllRule, 12 | PermissionParser: PermissionParserWithSeperator(":"), 13 | } 14 | 15 | type ( 16 | ValidationRule int 17 | // PermissionParserFunc is used for parsing the permission 18 | // to extract object and action usually 19 | PermissionParserFunc func(str string) []string 20 | OptionFunc func(*Options) 21 | // Option specifies casbin configuration options. 22 | Option interface { 23 | apply(*Options) 24 | } 25 | // Options holds Options of middleware 26 | Options struct { 27 | ValidationRule ValidationRule 28 | PermissionParser PermissionParserFunc 29 | } 30 | ) 31 | 32 | func (of OptionFunc) apply(o *Options) { 33 | of(o) 34 | } 35 | 36 | func WithValidationRule(vr ValidationRule) Option { 37 | return OptionFunc(func(o *Options) { 38 | o.ValidationRule = vr 39 | }) 40 | } 41 | 42 | func WithPermissionParser(pp PermissionParserFunc) Option { 43 | return OptionFunc(func(o *Options) { 44 | o.PermissionParser = pp 45 | }) 46 | } 47 | 48 | func PermissionParserWithSeperator(sep string) PermissionParserFunc { 49 | return func(str string) []string { 50 | return strings.Split(str, sep) 51 | } 52 | } 53 | 54 | // Helper function to set default values 55 | func optionsDefault(opts ...Option) Options { 56 | cfg := OptionsDefault 57 | 58 | for _, opt := range opts { 59 | opt.apply(&cfg) 60 | } 61 | 62 | return cfg 63 | } 64 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package fibercasbin 2 | 3 | func containsString(s []string, v string) bool { 4 | for _, vv := range s { 5 | if vv == v { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | 12 | func stringSliceToInterfaceSlice(arr []string) []interface{} { 13 | in := make([]interface{}, len(arr)) 14 | for i, a := range arr { 15 | in[i] = a 16 | } 17 | return in 18 | } 19 | --------------------------------------------------------------------------------