Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions middleware/casbin_auth.go
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions middleware/casbin_auth_model.conf
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions middleware/casbin_auth_policy.csv
Original file line number Diff line number Diff line change
@@ -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 changes: 86 additions & 0 deletions middleware/casbin_auth_test.go
Original file line number Diff line number Diff line change
@@ -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)
}