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

RBAC with multiple roles and chains-friendly #66

Merged
merged 4 commits into from
Aug 17, 2020
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
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