Skip to content

Commit

Permalink
Simple Role Based access control
Browse files Browse the repository at this point in the history
* auth.RBAC method for handlers
* developers can set optional roles in claims updater claims.User.SetRole
  • Loading branch information
kleash authored and umputun committed Aug 9, 2020
1 parent e2b238f commit f188a2e
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 1 deletion.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This library provides "social login" with Github, Google, Facebook, Microsoft, T
- Pre-auth and post-auth hooks to handle custom use cases.
- Middleware for easy integration into http routers
- Wrappers to extract user info from the request
- Role based access control

## Install

Expand Down Expand Up @@ -77,6 +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

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

Expand Down Expand Up @@ -281,7 +283,7 @@ service.AddCustomHandler(c)
There are several ways to adjust functionality of the library:

1. `SecretReader` - interface with a single method `Get(aud string) string` to return the secret used for JWT signing and verification
1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on.
1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status, roles and so on.
1. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users.
1. `UserUpdater` - interface with `Update(claims token.User) token.User` method. This method will be called on **each request** wrapped with `UpdateUser` middleware. This will be the place for special logic modify User Info in request context. [Example of usage.]((https://github.com/go-pkgz/auth/blob/master/_example/main.go#L148))

Expand Down
20 changes: 20 additions & 0 deletions middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package middleware
import (
"crypto/subtle"
"net/http"
"strings"

"github.com/pkg/errors"

Expand Down Expand Up @@ -187,3 +188,22 @@ func (a *Authenticator) basicAdminUser(r *http.Request) bool {

return true
}

// 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
}

if !strings.EqualFold(role, user.Role) {
http.Error(w, "Access denied", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
}
return a.auth(true)(http.HandlerFunc(fn)) // enforce auth
}
65 changes: 65 additions & 0 deletions middleware/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,49 @@ func TestAdminRequired(t *testing.T) {
assert.Equal(t, 401, resp.StatusCode, "not authorized")
}

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

mux := http.NewServeMux()
handler := 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)))
server := httptest.NewServer(mux)
defer server.Close()

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, 201, resp.StatusCode, "valid token user")

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.Header.Add("X-XSRF-TOKEN", "random id")
resp, err = client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 403, resp.StatusCode)

data, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "Access denied\n", string(data))
}

func makeTestMux(_ *testing.T, a *Authenticator, required bool) http.Handler {
mux := http.NewServeMux()
authMiddleware := a.Auth
Expand Down Expand Up @@ -401,6 +444,28 @@ 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
11 changes: 11 additions & 0 deletions token/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type User struct {
IP string `json:"ip,omitempty"`
Email string `json:"email,omitempty"`
Attributes map[string]interface{} `json:"attrs,omitempty"`
Role string `json:"role,omitempty"`
}

// SetBoolAttr sets boolean attribute
Expand Down Expand Up @@ -145,3 +146,13 @@ func SetUserInfo(r *http.Request, user User) *http.Request {
ctx = context.WithValue(ctx, contextKey("user"), user)
return r.WithContext(ctx)
}

// SetRole sets user role for RBAC
func (u *User) SetRole(role string) {
u.Role = role
}

// GetRole gets user role
func (u *User) GetRole() string {
return u.Role
}

0 comments on commit f188a2e

Please sign in to comment.