mirror of
https://github.com/labstack/echo.git
synced 2024-11-28 08:38:39 +02:00
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
This commit is contained in:
parent
466d509e34
commit
c73dd1f54c
101
middleware/casbin_auth.go
Normal file
101
middleware/casbin_auth.go
Normal file
@ -0,0 +1,101 @@
|
||||
// Package middleware CasbinAuth provides handlers to enable ACL, RBAC, ABAC authorization support.
|
||||
// Simple Usage:
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "github.com/casbin/casbin"
|
||||
// "github.com/labstack/echo"
|
||||
// "github.com/labstack/echo/middleware"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// e := echo.New()
|
||||
//
|
||||
// // mediate the access for every request
|
||||
// e.Use(middleware.CasbinAuth(casbin.NewEnforcer("casbin_auth_model.conf", "casbin_auth_policy.csv")))
|
||||
//
|
||||
// e.Logger.Fatal(e.Start(":1323"))
|
||||
// }
|
||||
//
|
||||
// Advanced Usage:
|
||||
//
|
||||
// func main(){
|
||||
// ce := casbin.NewEnforcer("casbin_auth_model.conf", "")
|
||||
// ce.AddRoleForUser("alice", "admin")
|
||||
// ce.AddPolicy(...)
|
||||
//
|
||||
// e := echo.New()
|
||||
//
|
||||
// echo.Use(middleware.CasbinAuth(ce))
|
||||
//
|
||||
// e.Logger.Fatal(e.Start(":1323"))
|
||||
// }
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/casbin/casbin"
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
type (
|
||||
// CasbinAuthConfig defines the config for CasbinAuth middleware.
|
||||
CasbinAuthConfig struct {
|
||||
// Skipper defines a function to skip middleware.
|
||||
Skipper Skipper
|
||||
// Enforcer CasbinAuth main rule.
|
||||
// Required.
|
||||
Enforcer *casbin.Enforcer
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultCasbinAuthConfig is the default CasbinAuth middleware config.
|
||||
DefaultCasbinAuthConfig = CasbinAuthConfig{
|
||||
Skipper: DefaultSkipper,
|
||||
}
|
||||
)
|
||||
|
||||
// CasbinAuth returns an CasbinAuth middleware.
|
||||
//
|
||||
// For valid credentials it calls the next handler.
|
||||
// For missing or invalid credentials, it sends "401 - Unauthorized" response.
|
||||
func CasbinAuth(ce *casbin.Enforcer) echo.MiddlewareFunc {
|
||||
c := DefaultCasbinAuthConfig
|
||||
c.Enforcer = ce
|
||||
return CasbinAuthWithConfig(c)
|
||||
}
|
||||
|
||||
// CasbinAuthWithConfig returns an CasbinAuth middleware with config.
|
||||
// See `CasbinAuth()`.
|
||||
func CasbinAuthWithConfig(config CasbinAuthConfig) echo.MiddlewareFunc {
|
||||
// Defaults
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultCasbinAuthConfig.Skipper
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if config.Skipper(c) || config.CheckPermission(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
return echo.ErrForbidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserName gets the user name from the request.
|
||||
// Currently, only HTTP basic authentication is supported
|
||||
func (a *CasbinAuthConfig) GetUserName(c echo.Context) string {
|
||||
username, _, _ := c.Request().BasicAuth()
|
||||
return username
|
||||
}
|
||||
|
||||
// CheckPermission checks the user/method/path combination from the request.
|
||||
// Returns true (permission granted) or false (permission forbidden)
|
||||
func (a *CasbinAuthConfig) CheckPermission(c echo.Context) bool {
|
||||
user := a.GetUserName(c)
|
||||
method := c.Request().Method
|
||||
path := c.Request().URL.Path
|
||||
return a.Enforcer.Enforce(user, path, method)
|
||||
}
|
14
middleware/casbin_auth_model.conf
Normal file
14
middleware/casbin_auth_model.conf
Normal file
@ -0,0 +1,14 @@
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
|
7
middleware/casbin_auth_policy.csv
Normal file
7
middleware/casbin_auth_policy.csv
Normal file
@ -0,0 +1,7 @@
|
||||
p, alice, /dataset1/*, GET
|
||||
p, alice, /dataset1/resource1, POST
|
||||
p, bob, /dataset2/resource1, *
|
||||
p, bob, /dataset2/resource2, GET
|
||||
p, bob, /dataset2/folder1/*, POST
|
||||
p, dataset1_admin, /dataset1/*, *
|
||||
g, cathy, dataset1_admin
|
|
86
middleware/casbin_auth_test.go
Normal file
86
middleware/casbin_auth_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/casbin/casbin"
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
func testRequest(t *testing.T, ce *casbin.Enforcer, user string, path string, method string, code int) {
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(method, path, nil)
|
||||
req.SetBasicAuth(user, "secret")
|
||||
res := httptest.NewRecorder()
|
||||
c := e.NewContext(req, res)
|
||||
h := CasbinAuth(ce)(func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "test")
|
||||
})
|
||||
|
||||
err := h(c)
|
||||
|
||||
if err != nil {
|
||||
if errObj, ok := err.(*echo.HTTPError); ok {
|
||||
if errObj.Code != code {
|
||||
t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, errObj.Code, code)
|
||||
}
|
||||
} else {
|
||||
t.Error(err)
|
||||
}
|
||||
} else {
|
||||
if c.Response().Status != code {
|
||||
t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, c.Response().Status, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCasbinAuth(t *testing.T) {
|
||||
ce := casbin.NewEnforcer("casbin_auth_model.conf", "casbin_auth_policy.csv")
|
||||
|
||||
testRequest(t, ce, "alice", "/dataset1/resource1", echo.GET, 200)
|
||||
testRequest(t, ce, "alice", "/dataset1/resource1", echo.POST, 200)
|
||||
testRequest(t, ce, "alice", "/dataset1/resource2", echo.GET, 200)
|
||||
testRequest(t, ce, "alice", "/dataset1/resource2", echo.POST, 403)
|
||||
}
|
||||
|
||||
func TestPathWildcard(t *testing.T) {
|
||||
ce := casbin.NewEnforcer("casbin_auth_model.conf", "casbin_auth_policy.csv")
|
||||
|
||||
testRequest(t, ce, "bob", "/dataset2/resource1", "GET", 200)
|
||||
testRequest(t, ce, "bob", "/dataset2/resource1", "POST", 200)
|
||||
testRequest(t, ce, "bob", "/dataset2/resource1", "DELETE", 200)
|
||||
testRequest(t, ce, "bob", "/dataset2/resource2", "GET", 200)
|
||||
testRequest(t, ce, "bob", "/dataset2/resource2", "POST", 403)
|
||||
testRequest(t, ce, "bob", "/dataset2/resource2", "DELETE", 403)
|
||||
|
||||
testRequest(t, ce, "bob", "/dataset2/folder1/item1", "GET", 403)
|
||||
testRequest(t, ce, "bob", "/dataset2/folder1/item1", "POST", 200)
|
||||
testRequest(t, ce, "bob", "/dataset2/folder1/item1", "DELETE", 403)
|
||||
testRequest(t, ce, "bob", "/dataset2/folder1/item2", "GET", 403)
|
||||
testRequest(t, ce, "bob", "/dataset2/folder1/item2", "POST", 200)
|
||||
testRequest(t, ce, "bob", "/dataset2/folder1/item2", "DELETE", 403)
|
||||
}
|
||||
|
||||
func TestRBAC(t *testing.T) {
|
||||
ce := casbin.NewEnforcer("casbin_auth_model.conf", "casbin_auth_policy.csv")
|
||||
|
||||
// cathy can access all /dataset1/* resources via all methods because it has the dataset1_admin role.
|
||||
testRequest(t, ce, "cathy", "/dataset1/item", "GET", 200)
|
||||
testRequest(t, ce, "cathy", "/dataset1/item", "POST", 200)
|
||||
testRequest(t, ce, "cathy", "/dataset1/item", "DELETE", 200)
|
||||
testRequest(t, ce, "cathy", "/dataset2/item", "GET", 403)
|
||||
testRequest(t, ce, "cathy", "/dataset2/item", "POST", 403)
|
||||
testRequest(t, ce, "cathy", "/dataset2/item", "DELETE", 403)
|
||||
|
||||
// delete all roles on user cathy, so cathy cannot access any resources now.
|
||||
ce.DeleteRolesForUser("cathy")
|
||||
|
||||
testRequest(t, ce, "cathy", "/dataset1/item", "GET", 403)
|
||||
testRequest(t, ce, "cathy", "/dataset1/item", "POST", 403)
|
||||
testRequest(t, ce, "cathy", "/dataset1/item", "DELETE", 403)
|
||||
testRequest(t, ce, "cathy", "/dataset2/item", "GET", 403)
|
||||
testRequest(t, ce, "cathy", "/dataset2/item", "POST", 403)
|
||||
testRequest(t, ce, "cathy", "/dataset2/item", "DELETE", 403)
|
||||
}
|
Loading…
Reference in New Issue
Block a user