diff --git a/authz.go b/authz.go new file mode 100644 index 00000000..362574ae --- /dev/null +++ b/authz.go @@ -0,0 +1,49 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "net/http" + + "github.com/casbin/casbin" +) + +// NewAuthorizer returns the authorizer, uses a Casbin enforcer as input +func NewAuthorizer(e *casbin.Enforcer) HandlerFunc { + return func(c *Context) { + a := &BasicAuthorizer{enforcer: e} + + if !a.CheckPermission(c.Request) { + a.RequirePermission(c.Writer) + } + } +} + +// BasicAuthorizer stores the casbin handler +type BasicAuthorizer struct { + enforcer *casbin.Enforcer +} + +// GetUserName gets the user name from the request. +// Currently, only HTTP basic authentication is supported +func (a *BasicAuthorizer) GetUserName(r *http.Request) string { + username, _, _ := r.BasicAuth() + return username +} + +// CheckPermission checks the user/method/path combination from the request. +// Returns true (permission granted) or false (permission forbidden) +func (a *BasicAuthorizer) CheckPermission(r *http.Request) bool { + user := a.GetUserName(r) + method := r.Method + path := r.URL.Path + return a.enforcer.Enforce(user, path, method) +} + +// RequirePermission returns the 403 Forbidden to the client +func (a *BasicAuthorizer) RequirePermission(w http.ResponseWriter) { + w.WriteHeader(403) + w.Write([]byte("403 Forbidden\n")) +} diff --git a/authz_model.conf b/authz_model.conf new file mode 100644 index 00000000..d1b3dbd7 --- /dev/null +++ b/authz_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 == "*") \ No newline at end of file diff --git a/authz_policy.csv b/authz_policy.csv new file mode 100644 index 00000000..c062dd3e --- /dev/null +++ b/authz_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 \ No newline at end of file diff --git a/authz_test.go b/authz_test.go new file mode 100644 index 00000000..81bdc085 --- /dev/null +++ b/authz_test.go @@ -0,0 +1,101 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/casbin/casbin" +) + +func testAuthzRequest(t *testing.T, router *Engine, user string, path string, method string, code int) { + r, _ := http.NewRequest(method, path, nil) + r.SetBasicAuth(user, "123") + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + if w.Code != code { + t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, w.Code, code) + } +} + +func TestBasic(t *testing.T) { + router := New() + router.Use(NewAuthorizer(casbin.NewEnforcer("authz_model.conf", "authz_policy.csv"))) + router.Any("/dataset1/resource1", func(c *Context) { + c.Status(200) + }) + router.Any("/dataset1/resource2", func(c *Context) { + c.Status(200) + }) + + testAuthzRequest(t, router, "alice", "/dataset1/resource1", "GET", 200) + testAuthzRequest(t, router, "alice", "/dataset1/resource1", "POST", 200) + testAuthzRequest(t, router, "alice", "/dataset1/resource2", "GET", 200) + testAuthzRequest(t, router, "alice", "/dataset1/resource2", "POST", 403) +} + +func TestPathWildcard(t *testing.T) { + router := New() + router.Use(NewAuthorizer(casbin.NewEnforcer("authz_model.conf", "authz_policy.csv"))) + router.Any("/dataset2/resource1", func(c *Context) { + c.Status(200) + }) + router.Any("/dataset2/resource2", func(c *Context) { + c.Status(200) + }) + router.Any("/dataset2/folder1/item1", func(c *Context) { + c.Status(200) + }) + router.Any("/dataset2/folder1/item2", func(c *Context) { + c.Status(200) + }) + + testAuthzRequest(t, router, "bob", "/dataset2/resource1", "GET", 200) + testAuthzRequest(t, router, "bob", "/dataset2/resource1", "POST", 200) + testAuthzRequest(t, router, "bob", "/dataset2/resource1", "DELETE", 200) + testAuthzRequest(t, router, "bob", "/dataset2/resource2", "GET", 200) + testAuthzRequest(t, router, "bob", "/dataset2/resource2", "POST", 403) + testAuthzRequest(t, router, "bob", "/dataset2/resource2", "DELETE", 403) + + testAuthzRequest(t, router, "bob", "/dataset2/folder1/item1", "GET", 403) + testAuthzRequest(t, router, "bob", "/dataset2/folder1/item1", "POST", 200) + testAuthzRequest(t, router, "bob", "/dataset2/folder1/item1", "DELETE", 403) + testAuthzRequest(t, router, "bob", "/dataset2/folder1/item2", "GET", 403) + testAuthzRequest(t, router, "bob", "/dataset2/folder1/item2", "POST", 200) + testAuthzRequest(t, router, "bob", "/dataset2/folder1/item2", "DELETE", 403) +} + +func TestRBAC(t *testing.T) { + router := New() + e := casbin.NewEnforcer("authz_model.conf", "authz_policy.csv") + router.Use(NewAuthorizer(e)) + router.Any("/dataset1/item", func(c *Context) { + c.Status(200) + }) + router.Any("/dataset2/item", func(c *Context) { + c.Status(200) + }) + + // cathy can access all /dataset1/* resources via all methods because it has the dataset1_admin role. + testAuthzRequest(t, router, "cathy", "/dataset1/item", "GET", 200) + testAuthzRequest(t, router, "cathy", "/dataset1/item", "POST", 200) + testAuthzRequest(t, router, "cathy", "/dataset1/item", "DELETE", 200) + testAuthzRequest(t, router, "cathy", "/dataset2/item", "GET", 403) + testAuthzRequest(t, router, "cathy", "/dataset2/item", "POST", 403) + testAuthzRequest(t, router, "cathy", "/dataset2/item", "DELETE", 403) + + // delete all roles on user cathy, so cathy cannot access any resources now. + e.DeleteRolesForUser("cathy") + + testAuthzRequest(t, router, "cathy", "/dataset1/item", "GET", 403) + testAuthzRequest(t, router, "cathy", "/dataset1/item", "POST", 403) + testAuthzRequest(t, router, "cathy", "/dataset1/item", "DELETE", 403) + testAuthzRequest(t, router, "cathy", "/dataset2/item", "GET", 403) + testAuthzRequest(t, router, "cathy", "/dataset2/item", "POST", 403) + testAuthzRequest(t, router, "cathy", "/dataset2/item", "DELETE", 403) +}