Skip to content

Commit

Permalink
RBAC with multiple roles and chains-friendly (#66)
Browse files Browse the repository at this point in the history
* convert rbac middleware to traditional form

* fix test for rbac, not expecting token refresh anymore

* support multiple roles in RBAC middleware

* add rbac test with no role in
  • Loading branch information
umputun committed Aug 17, 2020
1 parent 9331f97 commit 9a46108
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 43 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func main() {
- `middleware.Auth` - requires authenticated user
- `middleware.Admin` - requires authenticated admin user
- `middleware.Trace` - doesn't require authenticated user, but adds user info to request
- `middleware.RBAC` - requires authenticated user with passed role
- `middleware.RBAC` - requires authenticated user with passed role(s)

Also, there is a special middleware `middleware.UpdateUser` for population and modifying UserInfo in every request. See "Customization" for more details.

Expand Down
35 changes: 23 additions & 12 deletions middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,19 +191,30 @@ func (a *Authenticator) basicAdminUser(r *http.Request) bool {

// RBAC middleware allows role based control for routes
// this handler internally wrapped with auth(true) to avoid situation if RBAC defined without prior Auth
func (a *Authenticator) RBAC(role string, next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
user, err := token.GetUserInfo(r)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
func (a *Authenticator) RBAC(roles ...string) func(http.Handler) http.Handler {

if !strings.EqualFold(role, user.Role) {
http.Error(w, "Access denied", http.StatusForbidden)
return
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
user, err := token.GetUserInfo(r)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}

var matched bool
for _, role := range roles {
if strings.EqualFold(role, user.Role) {
matched = true
break
}
}
if !matched {
http.Error(w, "Access denied", http.StatusForbidden)
return
}
h.ServeHTTP(w, r)
}
next.ServeHTTP(w, r)
return a.auth(true)(http.HandlerFunc(fn)) // enforce auth
}
return a.auth(true)(http.HandlerFunc(fn)) // enforce auth
return f
}
55 changes: 26 additions & 29 deletions middleware/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ var testJwtWithHandshake = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0ZXN

var testJwtNoUser = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyfQ.sBpblkbBRzZsBSPPNrTWqA5h7h54solrw5L4IypJT_o"

var testJwtWithRole = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0ZXN0X3N5cyIsImV4cCI6Mjc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjY4ODQyMjIsInVzZXIiOnsibmFtZSI6Im5hbWUxIiwiaWQiOiJpZDEiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoibWVAZXhhbXBsZS5jb20iLCJhdHRycyI6eyJib29sYSI6dHJ1ZSwic3RyYSI6InN0cmEtdmFsIn0sInJvbGUiOiJlbXBsb3llZSJ9fQ.VLW4_LUDZq_eFc9F1Zx1lbv2Whic2VHy6C0dJ5azL8A"

func TestAuthJWTCookie(t *testing.T) {
a := makeTestAuth(t)

Expand Down Expand Up @@ -368,37 +370,54 @@ func TestAdminRequired(t *testing.T) {
}

func TestRBAC(t *testing.T) {
a := makeRBACTestAuth(t)
a := makeTestAuth(t)

mux := http.NewServeMux()
handler := func(w http.ResponseWriter, r *http.Request) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, err := token.GetUserInfo(r)
assert.NoError(t, err)
assert.Equal(t, token.User{Name: "name1", ID: "id1", Picture: "http://example.com/pic.png",
IP: "127.0.0.1", Email: "me@example.com", Audience: "test_sys",
Attributes: map[string]interface{}{"boola": true, "stra": "stra-val"},
Role: "employee"}, u)
w.WriteHeader(201)
}
mux.Handle("/authForEmployees", a.RBAC("employee", http.HandlerFunc(handler)))
mux.Handle("/authForExternals", a.RBAC("external", http.HandlerFunc(handler)))
})

mux.Handle("/authForEmployees", a.RBAC("someone", "employee")(handler))
mux.Handle("/authForExternals", a.RBAC("external")(handler))
server := httptest.NewServer(mux)
defer server.Close()

// employee route only, token with employee role
expiration := int(365 * 24 * time.Hour.Seconds()) //nolint
req, err := http.NewRequest("GET", server.URL+"/authForEmployees", nil)
require.Nil(t, err)
req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtValid, HttpOnly: true, Path: "/", MaxAge: expiration, Secure: false})
req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtWithRole, HttpOnly: true, Path: "/",
MaxAge: expiration, Secure: false})
req.Header.Add("X-XSRF-TOKEN", "random id")

client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode, "valid token user")

// employee route only, token without employee role
expiration = int(365 * 24 * time.Hour.Seconds()) //nolint
req, err = http.NewRequest("GET", server.URL+"/authForEmployees", nil)
require.Nil(t, err)
req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtValid, HttpOnly: true, Path: "/",
MaxAge: expiration, Secure: false})
req.Header.Add("X-XSRF-TOKEN", "random id")

client = &http.Client{Timeout: 5 * time.Second}
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, 403, resp.StatusCode, "valid token user, incorrect role")

// external route only, token with employee role
req, err = http.NewRequest("GET", server.URL+"/authForExternals", nil)
require.Nil(t, err)
req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtValid, HttpOnly: true, Path: "/", MaxAge: expiration, Secure: false})
req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtWithRole, HttpOnly: true, Path: "/", MaxAge: expiration, Secure: false})
req.Header.Add("X-XSRF-TOKEN", "random id")
resp, err = client.Do(req)
require.NoError(t, err)
Expand Down Expand Up @@ -444,28 +463,6 @@ func makeTestAuth(_ *testing.T) Authenticator {
}
}

func makeRBACTestAuth(_ *testing.T) Authenticator {
j := token.NewService(token.Opts{
SecretReader: token.SecretFunc(func(string) (string, error) { return "xyz 12345", nil }),
SecureCookies: false,
TokenDuration: time.Second,
CookieDuration: time.Hour * 24 * 31,
ClaimsUpd: token.ClaimsUpdFunc(func(claims token.Claims) token.Claims {
claims.User.SetStrAttr("stra", "stra-val")
claims.User.SetBoolAttr("boola", true)
claims.User.SetRole("employee")
return claims
}),
})

return Authenticator{
AdminPasswd: "123456",
JWTService: j,
Validator: token.ValidatorFunc(func(token string, claims token.Claims) bool { return true }),
L: logger.Std,
}
}

type testRefreshCache struct {
data map[interface{}]interface{}
sync.RWMutex
Expand Down
2 changes: 1 addition & 1 deletion middleware/user_updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (f UserUpdFunc) Update(user token.User) token.User {
}

// UpdateUser update user info with UserUpdater if it exists in request's context. Otherwise do nothing.
// should be places after either Auth, Trace or AdminOnly middleware.
// should be placed after either Auth, Trace. AdminOnly or RBAC middleware.
func (a *Authenticator) UpdateUser(upd UserUpdater) func(http.Handler) http.Handler {
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
Expand Down

0 comments on commit 9a46108

Please sign in to comment.