diff --git a/middleware/casbin_auth.go b/middleware/casbin_auth.go new file mode 100644 index 000000000..4beaa93f2 --- /dev/null +++ b/middleware/casbin_auth.go @@ -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) +} diff --git a/middleware/casbin_auth_model.conf b/middleware/casbin_auth_model.conf new file mode 100644 index 000000000..fd2f08df6 --- /dev/null +++ b/middleware/casbin_auth_model.conf @@ -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 == "*") diff --git a/middleware/casbin_auth_policy.csv b/middleware/casbin_auth_policy.csv new file mode 100644 index 000000000..9203e11f8 --- /dev/null +++ b/middleware/casbin_auth_policy.csv @@ -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 diff --git a/middleware/casbin_auth_test.go b/middleware/casbin_auth_test.go new file mode 100644 index 000000000..bf4a38b8c --- /dev/null +++ b/middleware/casbin_auth_test.go @@ -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) +}