Skip to content

Commit

Permalink
Update authz architecture to use Role for all user
Browse files Browse the repository at this point in the history
  • Loading branch information
Masayoshi Mizutani committed Feb 18, 2020
1 parent 15c0757 commit dda74ac
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 16 deletions.
8 changes: 4 additions & 4 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ func reverseProxy(authz *authzService, apiKey, target string) (gin.HandlerFunc,
return
}

user := userData.(string)
allowed, ok := authz.AllowTable[user]
if !ok {
userID := userData.(string)
user := authz.lookup(userID)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized user", "user": user})
return
}

tags := strings.Join(allowed, ",")
tags := strings.Join(user.allowed(), ",")
if tags == "" {
tags = "*"
}
Expand Down
95 changes: 84 additions & 11 deletions authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,109 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"regexp"

"github.com/pkg/errors"
)

type authzUser struct {
UserID string `json:"user_id"`
Role string `json:"role"`
rolePtr *authzRole
}

func (x *authzUser) allowed() []string {
return x.rolePtr.AllowedTags
}

type authzRole struct {
Name string `json:"name"`
AllowedTags []string `json:"allowed_tags"`
}

type authzRule struct {
UserRegex string `json:"user_regex"`
Role string `json:"role"`
regex *regexp.Regexp
rolePtr *authzRole
}

type authzService struct {
AllowTable map[string][]string `json:"allow"`
Users []*authzUser `json:"users"`
Roles []*authzRole `json:"roles"`
Rules []*authzRule `json:"rules"`
UserMap map[string]*authzUser
RoleMap map[string]*authzRole
}

func newAuthzService(filePath string) (*authzService, error) {
func newAuthzServiceFromFile(filePath string) (*authzService, error) {
raw, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "Fail to load authz file: %s", filePath)
}

srv := authzService{
AllowTable: make(map[string][]string),
}
return newAuthzService(raw)
}

func newAuthzService(raw []byte) (*authzService, error) {
var srv authzService

if err := json.Unmarshal(raw, &srv); err != nil {
return nil, errors.Wrapf(err, "Fail to parse authz file: %s", filePath)
return nil, errors.Wrapf(err, "Fail to parse authz data json: %s", string(raw))
}

srv.UserMap = map[string]*authzUser{}
srv.RoleMap = map[string]*authzRole{}

for _, r := range srv.Roles {
if _, ok := srv.RoleMap[r.Name]; ok {
return nil, fmt.Errorf("Role '%s' is duplicated", r.Name)
}
srv.RoleMap[r.Name] = r
}

for _, u := range srv.Users {
if _, ok := srv.UserMap[u.UserID]; ok {
return nil, fmt.Errorf("User '%s' is duplicated", u.UserID)
}

role, ok := srv.RoleMap[u.Role]
if !ok {
return nil, fmt.Errorf("Role '%s' of User '%s' is not found", u.Role, u.UserID)
}
u.rolePtr = role
srv.UserMap[u.UserID] = u
}

for _, rule := range srv.Rules {
role, ok := srv.RoleMap[rule.Role]
if !ok {
return nil, fmt.Errorf("Role '%s' of Rule '%s' is not found", rule.Role, rule.UserRegex)
}
rule.rolePtr = role

ptn, err := regexp.Compile(rule.UserRegex)
if err != nil {
return nil, fmt.Errorf("Fail to compile regex of a rule: %s", rule.UserRegex)
}
rule.regex = ptn
}

logger.WithField("authz", srv.AllowTable).Info("Read authorization table")
logger.WithField("authz", srv).Info("Read authorization table")
return &srv, nil
}

func (x *authzService) allowedTags(user string) ([]string, error) {
allowed, ok := x.AllowTable[user]
func (x *authzService) lookup(userID string) *authzUser {
user, ok := x.UserMap[userID]
if !ok {
return nil, fmt.Errorf("Not permitted")
for _, rule := range x.Rules {
if rule.regex.MatchString(userID) {
newUser := &authzUser{UserID: userID, rolePtr: rule.rolePtr}
x.UserMap[userID] = newUser
return newUser
}
}
}

return allowed, nil
return user
}
155 changes: 155 additions & 0 deletions authz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package main_test

import (
"testing"

main "github.com/m-mizutani/strix"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAuthzService(t *testing.T) {
raw := `{
"users": [
{"user_id": "[email protected]", "role":"blue"},
{"user_id": "[email protected]", "role":"orange"},
{"user_id": "[email protected]", "role":"orange"}
],
"roles": [
{"name":"blue", "allowed_tags":[]},
{"name":"orange", "allowed_tags":["spell.1"]}
],
"rules": [
{"user_regex":"^delta@", "role":"blue"},
{"user_regex":"@example.com$", "role":"orange"}
]
}`

authz, err := main.NewAuthzService([]byte(raw))
require.NoError(t, err)

// Test default users and roles
userA := main.AuthzServiceLookup(authz, "[email protected]")
assert.NotNil(t, userA)
assert.NotContains(t, main.AuthzUserAllowed(userA), "spell.1")

userB := main.AuthzServiceLookup(authz, "[email protected]")
assert.NotNil(t, userB)
assert.Contains(t, main.AuthzUserAllowed(userB), "spell.1")

userC := main.AuthzServiceLookup(authz, "[email protected]")
assert.NotNil(t, userC)
assert.Contains(t, main.AuthzUserAllowed(userC), "spell.1")

// Test rules
userD1 := main.AuthzServiceLookup(authz, "[email protected]")
assert.NotNil(t, userD1)
assert.Equal(t, 0, len(main.AuthzUserAllowed(userD1)))

userD2 := main.AuthzServiceLookup(authz, "[email protected]")
assert.NotNil(t, userD2)
assert.Equal(t, 0, len(main.AuthzUserAllowed(userD2)))

userD3 := main.AuthzServiceLookup(authz, "[email protected]")
assert.Nil(t, userD3)

userE1 := main.AuthzServiceLookup(authz, "[email protected]")
assert.NotNil(t, userE1)
assert.Contains(t, main.AuthzUserAllowed(userE1), "spell.1")

userF1 := main.AuthzServiceLookup(authz, "[email protected]")
assert.Nil(t, userF1)
userF2 := main.AuthzServiceLookup(authz, "[email protected]")
assert.Nil(t, userF2)
}

func TestAuthzServiceInvalidJSON(t *testing.T) {
raw := `{
"users": [
{"user_id": "[email protected]", "role":"blue"}
],
"roles": [
{"name":"blue", "allowed_tags":[]}
],
"rules": [
{"user_regex":"^delta@", "role":"blue"}
],
}` // ^^^ invalid comma

_, err := main.NewAuthzService([]byte(raw))
require.Error(t, err)
}

func TestAuthzServiceDuplicatedRole(t *testing.T) {
raw := `{
"users": [
{"user_id": "[email protected]", "role":"blue"}
],
"roles": [
{"name":"blue", "allowed_tags":[]},
{"name":"blue", "allowed_tags":["spell.1"]}
]
}`

_, err := main.NewAuthzService([]byte(raw))
assert.EqualError(t, err, "Role 'blue' is duplicated")
}

func TestAuthzServiceDuplicatedUser(t *testing.T) {
raw := `{
"users": [
{"user_id": "[email protected]", "role":"blue"},
{"user_id": "[email protected]", "role":"orange"}
],
"roles": [
{"name":"blue", "allowed_tags":[]},
{"name":"orange", "allowed_tags":["spell.1"]}
]
}`

_, err := main.NewAuthzService([]byte(raw))
assert.EqualError(t, err, "User '[email protected]' is duplicated")
}

func TestAuthzServiceUserRoleNotFound(t *testing.T) {
raw := `{
"users": [
{"user_id": "[email protected]", "role":"blue"},
{"user_id": "[email protected]", "role":"orange"}
],
"roles": [
{"name":"blue", "allowed_tags":[]}
]
}`

_, err := main.NewAuthzService([]byte(raw))
assert.EqualError(t, err, "Role 'orange' of User '[email protected]' is not found")
}

func TestAuthzServiceRuleRoleNotFound(t *testing.T) {
raw := `{
"roles": [
{"name":"blue", "allowed_tags":[]}
],
"rules": [
{"user_regex":"^delta@", "role":"orange"}
]
}`

_, err := main.NewAuthzService([]byte(raw))
assert.EqualError(t, err, "Role 'orange' of Rule '^delta@' is not found")
}

func TestAuthzServiceRuleInavlidRegex(t *testing.T) {
raw := `{
"roles": [
{"name":"orange", "allowed_tags":[]}
],
"rules": [
{"user_regex":"^[delta@", "role":"orange"}
]
}`

_, err := main.NewAuthzService([]byte(raw))
assert.EqualError(t, err, "Fail to compile regex of a rule: ^[delta@")
}
13 changes: 13 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

type AuthzUser authzUser

var NewAuthzService = newAuthzService

func AuthzServiceLookup(x *authzService, userID string) *AuthzUser {
return (*AuthzUser)(x.lookup(userID))
}
func AuthzUserAllowed(x *AuthzUser) []string {
authz := (*authzUser)(x)
return authz.rolePtr.AllowedTags
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/pkg/errors v0.8.1
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
github.com/urfave/cli v1.22.1
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect
Expand Down
2 changes: 1 addition & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func runServer(args arguments) error {
})

// Setup session manager
authz, err := newAuthzService(args.AuthzFilePath)
authz, err := newAuthzServiceFromFile(args.AuthzFilePath)
if err != nil {
return err
}
Expand Down

0 comments on commit dda74ac

Please sign in to comment.