Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(casbin): let the caller decide the method of user identification #41

Merged
merged 3 commits into from
Feb 10, 2021
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
21 changes: 16 additions & 5 deletions casbin/casbin.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,20 @@ type (
// Enforcer CasbinAuth main rule.
// Required.
Enforcer *casbin.Enforcer

// Method to get the username - defaults to using basic auth
UserGetter func(c echo.Context) (string, error)
}
)

var (
// DefaultConfig is the default CasbinAuth middleware config.
DefaultConfig = Config{
Skipper: middleware.DefaultSkipper,
UserGetter: func(c echo.Context) (string, error) {
username, _, _ := c.Request().BasicAuth()
return username, nil
},
}
)

Expand Down Expand Up @@ -107,16 +114,20 @@ func MiddlewareWithConfig(config Config) echo.MiddlewareFunc {
}

// GetUserName gets the user name from the request.
// Currently, only HTTP basic authentication is supported
func (a *Config) GetUserName(c echo.Context) string {
username, _, _ := c.Request().BasicAuth()
return username
// It calls the UserGetter field of the Config struct that allows the caller to customize user identification.
func (a *Config) GetUserName(c echo.Context) (string, error) {
username, err := a.UserGetter(c)
return username, err
}

// CheckPermission checks the user/method/path combination from the request.
// Returns true (permission granted) or false (permission forbidden)
func (a *Config) CheckPermission(c echo.Context) (bool, error) {
user := a.GetUserName(c)
user, err := a.GetUserName(c)
if err != nil {
// Fail safe and do not propagate
return false, nil
}
method := c.Request().Method
path := c.Request().URL.Path
return a.Enforcer.Enforce(user, path, method)
Expand Down
110 changes: 76 additions & 34 deletions casbin/casbin_test.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
package casbin

import (
"errors"
"net/http"
"net/http/httptest"
"testing"

"github.com/casbin/casbin/v2"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

func testRequest(t *testing.T, ce *casbin.Enforcer, user string, path string, method string, code int) {
func testRequest(t *testing.T, h echo.HandlerFunc, 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 := Middleware(ce)(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})

err := h(c)

Expand All @@ -38,54 +37,97 @@ func testRequest(t *testing.T, ce *casbin.Enforcer, user string, path string, me

func TestAuth(t *testing.T) {
ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv")
h := Middleware(ce)(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})

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)
testRequest(t, h, "alice", "/dataset1/resource1", echo.GET, 200)
testRequest(t, h, "alice", "/dataset1/resource1", echo.POST, 200)
testRequest(t, h, "alice", "/dataset1/resource2", echo.GET, 200)
testRequest(t, h, "alice", "/dataset1/resource2", echo.POST, 403)
}

func TestPathWildcard(t *testing.T) {
ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv")
h := Middleware(ce)(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})

testRequest(t, h, "bob", "/dataset2/resource1", "GET", 200)
testRequest(t, h, "bob", "/dataset2/resource1", "POST", 200)
testRequest(t, h, "bob", "/dataset2/resource1", "DELETE", 200)
testRequest(t, h, "bob", "/dataset2/resource2", "GET", 200)
testRequest(t, h, "bob", "/dataset2/resource2", "POST", 403)
testRequest(t, h, "bob", "/dataset2/resource2", "DELETE", 403)

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)
testRequest(t, h, "bob", "/dataset2/folder1/item1", "GET", 403)
testRequest(t, h, "bob", "/dataset2/folder1/item1", "POST", 200)
testRequest(t, h, "bob", "/dataset2/folder1/item1", "DELETE", 403)
testRequest(t, h, "bob", "/dataset2/folder1/item2", "GET", 403)
testRequest(t, h, "bob", "/dataset2/folder1/item2", "POST", 200)
testRequest(t, h, "bob", "/dataset2/folder1/item2", "DELETE", 403)
}

func TestRBAC(t *testing.T) {
ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv")
h := Middleware(ce)(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})

// 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)
testRequest(t, h, "cathy", "/dataset1/item", "GET", 200)
testRequest(t, h, "cathy", "/dataset1/item", "POST", 200)
testRequest(t, h, "cathy", "/dataset1/item", "DELETE", 200)
testRequest(t, h, "cathy", "/dataset2/item", "GET", 403)
testRequest(t, h, "cathy", "/dataset2/item", "POST", 403)
testRequest(t, h, "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)
testRequest(t, h, "cathy", "/dataset1/item", "GET", 403)
testRequest(t, h, "cathy", "/dataset1/item", "POST", 403)
testRequest(t, h, "cathy", "/dataset1/item", "DELETE", 403)
testRequest(t, h, "cathy", "/dataset2/item", "GET", 403)
testRequest(t, h, "cathy", "/dataset2/item", "POST", 403)
testRequest(t, h, "cathy", "/dataset2/item", "DELETE", 403)
}

func TestEnforceError(t *testing.T) {
ce, _ := casbin.NewEnforcer("broken_auth_model.conf", "auth_policy.csv")
testRequest(t, ce, "cathy", "/dataset1/item", "GET", 500)
h := Middleware(ce)(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})

testRequest(t, h, "cathy", "/dataset1/item", "GET", 500)
}

func TestCustomUserGetter(t *testing.T) {
ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv")
cnf := Config{
Skipper: middleware.DefaultSkipper,
Enforcer: ce,
UserGetter: func(c echo.Context) (string, error) {
return "not_cathy_at_all", nil
},
}
h := MiddlewareWithConfig(cnf)(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})
testRequest(t, h, "cathy", "/dataset1/item", "GET", 403)
}

func TestUserGetterError(t *testing.T) {
ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv")
cnf := Config{
Skipper: middleware.DefaultSkipper,
Enforcer: ce,
UserGetter: func(c echo.Context) (string, error) {
return "", errors.New("no idea who you are")
},
}
h := MiddlewareWithConfig(cnf)(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})
testRequest(t, h, "cathy", "/dataset1/item", "GET", 403)
}