Skip to content

Commit c73dd1f

Browse files
xqbumuvishr
authored andcommitted
Add an authorization middleware that supports ACL, RBAC based on casbin. (#939)
* Add an authorization middleware that supports ACL, RBAC based on casbin. fix some coveralls check. change test file with new rule. renaming the prefix Authz to CasbinAuth use built-in SetBasicAuth method in test file. export all casbin auth configs. * fix example code
1 parent 466d509 commit c73dd1f

File tree

4 files changed

+208
-0
lines changed

4 files changed

+208
-0
lines changed

middleware/casbin_auth.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Package middleware CasbinAuth provides handlers to enable ACL, RBAC, ABAC authorization support.
2+
// Simple Usage:
3+
// package main
4+
//
5+
// import (
6+
// "github.com/casbin/casbin"
7+
// "github.com/labstack/echo"
8+
// "github.com/labstack/echo/middleware"
9+
// )
10+
//
11+
// func main() {
12+
// e := echo.New()
13+
//
14+
// // mediate the access for every request
15+
// e.Use(middleware.CasbinAuth(casbin.NewEnforcer("casbin_auth_model.conf", "casbin_auth_policy.csv")))
16+
//
17+
// e.Logger.Fatal(e.Start(":1323"))
18+
// }
19+
//
20+
// Advanced Usage:
21+
//
22+
// func main(){
23+
// ce := casbin.NewEnforcer("casbin_auth_model.conf", "")
24+
// ce.AddRoleForUser("alice", "admin")
25+
// ce.AddPolicy(...)
26+
//
27+
// e := echo.New()
28+
//
29+
// echo.Use(middleware.CasbinAuth(ce))
30+
//
31+
// e.Logger.Fatal(e.Start(":1323"))
32+
// }
33+
package middleware
34+
35+
import (
36+
"github.com/casbin/casbin"
37+
"github.com/labstack/echo"
38+
)
39+
40+
type (
41+
// CasbinAuthConfig defines the config for CasbinAuth middleware.
42+
CasbinAuthConfig struct {
43+
// Skipper defines a function to skip middleware.
44+
Skipper Skipper
45+
// Enforcer CasbinAuth main rule.
46+
// Required.
47+
Enforcer *casbin.Enforcer
48+
}
49+
)
50+
51+
var (
52+
// DefaultCasbinAuthConfig is the default CasbinAuth middleware config.
53+
DefaultCasbinAuthConfig = CasbinAuthConfig{
54+
Skipper: DefaultSkipper,
55+
}
56+
)
57+
58+
// CasbinAuth returns an CasbinAuth middleware.
59+
//
60+
// For valid credentials it calls the next handler.
61+
// For missing or invalid credentials, it sends "401 - Unauthorized" response.
62+
func CasbinAuth(ce *casbin.Enforcer) echo.MiddlewareFunc {
63+
c := DefaultCasbinAuthConfig
64+
c.Enforcer = ce
65+
return CasbinAuthWithConfig(c)
66+
}
67+
68+
// CasbinAuthWithConfig returns an CasbinAuth middleware with config.
69+
// See `CasbinAuth()`.
70+
func CasbinAuthWithConfig(config CasbinAuthConfig) echo.MiddlewareFunc {
71+
// Defaults
72+
if config.Skipper == nil {
73+
config.Skipper = DefaultCasbinAuthConfig.Skipper
74+
}
75+
76+
return func(next echo.HandlerFunc) echo.HandlerFunc {
77+
return func(c echo.Context) error {
78+
if config.Skipper(c) || config.CheckPermission(c) {
79+
return next(c)
80+
}
81+
82+
return echo.ErrForbidden
83+
}
84+
}
85+
}
86+
87+
// GetUserName gets the user name from the request.
88+
// Currently, only HTTP basic authentication is supported
89+
func (a *CasbinAuthConfig) GetUserName(c echo.Context) string {
90+
username, _, _ := c.Request().BasicAuth()
91+
return username
92+
}
93+
94+
// CheckPermission checks the user/method/path combination from the request.
95+
// Returns true (permission granted) or false (permission forbidden)
96+
func (a *CasbinAuthConfig) CheckPermission(c echo.Context) bool {
97+
user := a.GetUserName(c)
98+
method := c.Request().Method
99+
path := c.Request().URL.Path
100+
return a.Enforcer.Enforce(user, path, method)
101+
}

middleware/casbin_auth_model.conf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")

middleware/casbin_auth_policy.csv

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
p, alice, /dataset1/*, GET
2+
p, alice, /dataset1/resource1, POST
3+
p, bob, /dataset2/resource1, *
4+
p, bob, /dataset2/resource2, GET
5+
p, bob, /dataset2/folder1/*, POST
6+
p, dataset1_admin, /dataset1/*, *
7+
g, cathy, dataset1_admin

middleware/casbin_auth_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/casbin/casbin"
9+
"github.com/labstack/echo"
10+
)
11+
12+
func testRequest(t *testing.T, ce *casbin.Enforcer, user string, path string, method string, code int) {
13+
e := echo.New()
14+
req := httptest.NewRequest(method, path, nil)
15+
req.SetBasicAuth(user, "secret")
16+
res := httptest.NewRecorder()
17+
c := e.NewContext(req, res)
18+
h := CasbinAuth(ce)(func(c echo.Context) error {
19+
return c.String(http.StatusOK, "test")
20+
})
21+
22+
err := h(c)
23+
24+
if err != nil {
25+
if errObj, ok := err.(*echo.HTTPError); ok {
26+
if errObj.Code != code {
27+
t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, errObj.Code, code)
28+
}
29+
} else {
30+
t.Error(err)
31+
}
32+
} else {
33+
if c.Response().Status != code {
34+
t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, c.Response().Status, code)
35+
}
36+
}
37+
}
38+
39+
func TestCasbinAuth(t *testing.T) {
40+
ce := casbin.NewEnforcer("casbin_auth_model.conf", "casbin_auth_policy.csv")
41+
42+
testRequest(t, ce, "alice", "/dataset1/resource1", echo.GET, 200)
43+
testRequest(t, ce, "alice", "/dataset1/resource1", echo.POST, 200)
44+
testRequest(t, ce, "alice", "/dataset1/resource2", echo.GET, 200)
45+
testRequest(t, ce, "alice", "/dataset1/resource2", echo.POST, 403)
46+
}
47+
48+
func TestPathWildcard(t *testing.T) {
49+
ce := casbin.NewEnforcer("casbin_auth_model.conf", "casbin_auth_policy.csv")
50+
51+
testRequest(t, ce, "bob", "/dataset2/resource1", "GET", 200)
52+
testRequest(t, ce, "bob", "/dataset2/resource1", "POST", 200)
53+
testRequest(t, ce, "bob", "/dataset2/resource1", "DELETE", 200)
54+
testRequest(t, ce, "bob", "/dataset2/resource2", "GET", 200)
55+
testRequest(t, ce, "bob", "/dataset2/resource2", "POST", 403)
56+
testRequest(t, ce, "bob", "/dataset2/resource2", "DELETE", 403)
57+
58+
testRequest(t, ce, "bob", "/dataset2/folder1/item1", "GET", 403)
59+
testRequest(t, ce, "bob", "/dataset2/folder1/item1", "POST", 200)
60+
testRequest(t, ce, "bob", "/dataset2/folder1/item1", "DELETE", 403)
61+
testRequest(t, ce, "bob", "/dataset2/folder1/item2", "GET", 403)
62+
testRequest(t, ce, "bob", "/dataset2/folder1/item2", "POST", 200)
63+
testRequest(t, ce, "bob", "/dataset2/folder1/item2", "DELETE", 403)
64+
}
65+
66+
func TestRBAC(t *testing.T) {
67+
ce := casbin.NewEnforcer("casbin_auth_model.conf", "casbin_auth_policy.csv")
68+
69+
// cathy can access all /dataset1/* resources via all methods because it has the dataset1_admin role.
70+
testRequest(t, ce, "cathy", "/dataset1/item", "GET", 200)
71+
testRequest(t, ce, "cathy", "/dataset1/item", "POST", 200)
72+
testRequest(t, ce, "cathy", "/dataset1/item", "DELETE", 200)
73+
testRequest(t, ce, "cathy", "/dataset2/item", "GET", 403)
74+
testRequest(t, ce, "cathy", "/dataset2/item", "POST", 403)
75+
testRequest(t, ce, "cathy", "/dataset2/item", "DELETE", 403)
76+
77+
// delete all roles on user cathy, so cathy cannot access any resources now.
78+
ce.DeleteRolesForUser("cathy")
79+
80+
testRequest(t, ce, "cathy", "/dataset1/item", "GET", 403)
81+
testRequest(t, ce, "cathy", "/dataset1/item", "POST", 403)
82+
testRequest(t, ce, "cathy", "/dataset1/item", "DELETE", 403)
83+
testRequest(t, ce, "cathy", "/dataset2/item", "GET", 403)
84+
testRequest(t, ce, "cathy", "/dataset2/item", "POST", 403)
85+
testRequest(t, ce, "cathy", "/dataset2/item", "DELETE", 403)
86+
}

0 commit comments

Comments
 (0)