Skip to content

Commit

Permalink
feat(dex): Allow multiple roles for one user (#1687)
Browse files Browse the repository at this point in the history
- Currently we are treating the role of a user as one value but in the
future user's might be a part of multiple roles. We need to add both of
these roles to the context and then check the permissions for both of
the roles
  • Loading branch information
gsandok authored Jun 24, 2024
1 parent 556d5bf commit a0502e9
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 33 deletions.
23 changes: 13 additions & 10 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/freiheit-com/kuberpult/pkg/grpc"
"github.com/freiheit-com/kuberpult/pkg/logger"
Expand Down Expand Up @@ -99,7 +100,7 @@ func (x *DummyGrpcContextReader) ReadUserFromGrpcContext(ctx context.Context) (*
Email: "[email protected]",
Name: "userName",
DexAuthContext: &DexAuthContext{
Role: x.Role,
Role: []string{x.Role},
},
}
return user, nil
Expand Down Expand Up @@ -148,14 +149,16 @@ func (x *DexGrpcContextReader) ReadUserFromGrpcContext(ctx context.Context) (*Us
if len(rolesInHeader) == 0 {
return useDexDefaultRole(ctx, x.DexDefaultRoleEnabled, u)
}

userRole, err := Decode64(rolesInHeader[0])

if err != nil {
return nil, grpc.AuthError(ctx, fmt.Errorf("extract: non-base64 in author-role in grpc context %s", userRole))
var userRole []string
for _, role := range rolesInHeader {
newRole, err := Decode64(role)
if err != nil {
return nil, grpc.AuthError(ctx, fmt.Errorf("extract: non-base64 in author-role in grpc context %s", userRole))
}
userRole = append(userRole, strings.Split(newRole, ",")...)
}

if userRole == "" {
if len(userRole) == 0 {
return useDexDefaultRole(ctx, x.DexDefaultRoleEnabled, u)
}
u.DexAuthContext = &DexAuthContext{
Expand All @@ -168,7 +171,7 @@ func (x *DexGrpcContextReader) ReadUserFromGrpcContext(ctx context.Context) (*Us
func useDexDefaultRole(ctx context.Context, dexDefaultRoleEnabled bool, u *User) (*User, error) {
if dexDefaultRoleEnabled {
u.DexAuthContext = &DexAuthContext{
Role: "default",
Role: []string{"default"},
}
logger.FromContext(ctx).Warn("role undefined but dex is enabled. Default user role enabled. Proceeding with default role.")
return u, nil
Expand Down Expand Up @@ -201,7 +204,7 @@ func ReadUserFromHttpHeader(ctx context.Context, r *http.Request) (*User, error)
Email: headerEmail,
Name: headerName,
DexAuthContext: &DexAuthContext{
Role: headerRole,
Role: strings.Split(headerRole, ","),
},
}, nil
}
Expand All @@ -220,7 +223,7 @@ func WriteUserToHttpHeader(r *http.Request, user User) {
// WriteUserRoleToHttpHeader writes the user role into http headers
// it is used for requests like /release and managing locks which are delegated from frontend-service to cd-service
func WriteUserRoleToHttpHeader(r *http.Request, role string) {
r.Header.Set(HeaderUserRole, Encode64(role))
r.Header.Add(HeaderUserRole, Encode64(role))
}

func GetUserOrDefault(u *User, defaultUser User) User {
Expand Down
2 changes: 1 addition & 1 deletion pkg/auth/dex.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import (
// Extracted information from JWT/Cookie.
type DexAuthContext struct {
// The user role extracted from the Cookie.
Role string
Role []string
}

// Dex App Client.
Expand Down
13 changes: 8 additions & 5 deletions pkg/auth/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,18 +281,21 @@ func CheckUserPermissions(rbacConfig RBACConfig, user *User, env, team, envGroup
if rbacConfig.Policy == nil {
return errors.New("the desired action can not be performed because Dex is enabled without any RBAC policies")
}
permissionsWanted := fmt.Sprintf(PermissionTemplate, user.DexAuthContext.Role, action, pEnvGroup, pEnv, pApplication)
_, permissionsExist := rbacConfig.Policy.Permissions[permissionsWanted]
if permissionsExist {
return nil
for _, role := range user.DexAuthContext.Role {
permissionsWanted := fmt.Sprintf(PermissionTemplate, role, action, pEnvGroup, pEnv, pApplication)
_, permissionsExist := rbacConfig.Policy.Permissions[permissionsWanted]
if permissionsExist {
return nil
}
}

}
}
}
// The permission is not found. Return an error.
return PermissionError{
User: user.Name,
Role: user.DexAuthContext.Role,
Role: strings.Join(user.DexAuthContext.Role, ", "),
Action: action,
Environment: env,
Team: team,
Expand Down
38 changes: 32 additions & 6 deletions pkg/auth/rbac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,17 +141,43 @@ func TestCheckUserPermissions(t *testing.T) {
}{
{
Name: "Check user permission works as expected",
user: &User{DexAuthContext: &DexAuthContext{Role: "Developer"}},
user: &User{DexAuthContext: &DexAuthContext{Role: []string{"Developer"}}},
env: "production",
envGroup: "production",
application: "app1",
action: PermissionCreateLock,
rbacConfig: RBACConfig{DexEnabled: true, Policy: &RBACPolicies{Permissions: map[string]Permission{"p,role:Developer,CreateLock,production:production,app1,allow": {Role: "Developer"}}}},
team: "",
},
{
Name: "Check user permission works as expected with multiple roles",
user: &User{DexAuthContext: &DexAuthContext{Role: []string{"Developer", "Manager"}}},
env: "production",
envGroup: "production",
application: "app1",
action: PermissionCreateLock,
rbacConfig: RBACConfig{DexEnabled: true, Policy: &RBACPolicies{Permissions: map[string]Permission{"p,role:Developer,CreateLock,production:production,app1,allow": {Role: "Developer"}}}},
team: "",
},
{
Name: "Check user permission with multiple roles but no permissions",
user: &User{DexAuthContext: &DexAuthContext{Role: []string{"visitor", "Manager"}}},
env: "production",
envGroup: "production",
application: "app1",
action: PermissionCreateLock,
rbacConfig: RBACConfig{DexEnabled: true, Policy: &RBACPolicies{Permissions: map[string]Permission{"p,role:Developer,CreateLock,production:production,app1,allow": {Role: "Developer"}}}},
team: "random-team",
WantError: PermissionError{
Role: "visitor, Manager",
Action: "CreateLock",
Environment: "production",
Team: "random-team",
},
},
{
Name: "Environment independent works as expected",
user: &User{Name: "user", DexAuthContext: &DexAuthContext{Role: "Developer"}},
user: &User{Name: "user", DexAuthContext: &DexAuthContext{Role: []string{"Developer"}}},
env: "production",
envGroup: "production",
application: "app1",
Expand All @@ -161,7 +187,7 @@ func TestCheckUserPermissions(t *testing.T) {
},
{
Name: "User does not have permission: wrong environment/group",
user: &User{DexAuthContext: &DexAuthContext{Role: "Developer"}},
user: &User{DexAuthContext: &DexAuthContext{Role: []string{"Developer"}}},
env: "production",
envGroup: "staging",
application: "app1",
Expand All @@ -177,7 +203,7 @@ func TestCheckUserPermissions(t *testing.T) {
},
{
Name: "User does not have permission: wrong app",
user: &User{DexAuthContext: &DexAuthContext{Role: "Developer"}},
user: &User{DexAuthContext: &DexAuthContext{Role: []string{"Developer"}}},
env: "production",
envGroup: "production",
application: "app2",
Expand All @@ -193,7 +219,7 @@ func TestCheckUserPermissions(t *testing.T) {
},
{
Name: "There are no policies specified",
user: &User{DexAuthContext: &DexAuthContext{Role: "Developer"}},
user: &User{DexAuthContext: &DexAuthContext{Role: []string{"Developer"}}},
env: "production",
envGroup: "production",
application: "app2",
Expand Down Expand Up @@ -276,7 +302,7 @@ func TestCheckUserPermissionsWildcards(t *testing.T) {
}{
{
Name: "Check user permission works for all wildcard combinations",
user: &User{DexAuthContext: &DexAuthContext{Role: "Developer"}},
user: &User{DexAuthContext: &DexAuthContext{Role: []string{"Developer"}}},
env: "production",
envGroup: "production",
application: "app1",
Expand Down
2 changes: 1 addition & 1 deletion pkg/testutil/testutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func MakeTestContextDexEnabledUser(role string) context.Context {
u := auth.User{
Email: "[email protected]",
Name: "test tester",
DexAuthContext: &auth.DexAuthContext{Role: role},
DexAuthContext: &auth.DexAuthContext{Role: []string{role}},
}
ctx := auth.WriteUserToContext(context.Background(), u)
ctx = metadata.NewIncomingContext(ctx, metadata.New(map[string]string{
Expand Down
6 changes: 4 additions & 2 deletions services/frontend-service/pkg/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,9 @@ func (p *Auth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx = auth.WriteUserToContext(ctx, combinedUser)
ctx = auth.WriteUserToGrpcContext(ctx, combinedUser)
if user != nil && user.DexAuthContext != nil {
ctx = auth.WriteUserRoleToGrpcContext(ctx, user.DexAuthContext.Role)
for _, role := range user.DexAuthContext.Role {
ctx = auth.WriteUserRoleToGrpcContext(ctx, role)
}
}
p.HttpServer.ServeHTTP(w, r.WithContext(ctx))
return nil
Expand All @@ -603,7 +605,7 @@ func getUserFromDex(w http.ResponseWriter, req *http.Request, clientID, baseURL
logger.FromContext(httpCtx).Info("could not decode user role")
return nil
}
return &auth.DexAuthContext{Role: headerRole}
return &auth.DexAuthContext{Role: strings.Split(headerRole, ",")}
}

// GrpcProxy passes through gRPC messages to another server.
Expand Down
24 changes: 16 additions & 8 deletions services/frontend-service/pkg/interceptors/interceptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,18 @@ func GoogleIAPInterceptor(
httpHandler(w, req)
}

func AddRoleToContext(httpCtx context.Context, w http.ResponseWriter, req *http.Request, userGroup string, policy *auth.RBACPolicies) context.Context {
func AddRoleToContext(w http.ResponseWriter, req *http.Request, roles []string) context.Context {
auth.WriteUserRoleToHttpHeader(req, strings.Join(roles, ","))
return auth.WriteUserRoleToGrpcContext(req.Context(), strings.Join(roles, ","))
}

func CreateRoleString(userGroup string, roles []string, policy *auth.RBACPolicies) []string {
for _, policyGroup := range policy.Groups {
if policyGroup.Group == userGroup {
auth.WriteUserRoleToHttpHeader(req, policyGroup.Role)
httpCtx = auth.WriteUserRoleToGrpcContext(req.Context(), policyGroup.Role)
roles = append(roles, policyGroup.Role)
}
}
return httpCtx
return roles
}

// DexLoginInterceptor intercepts HTTP calls to the frontend service.
Expand Down Expand Up @@ -184,23 +188,27 @@ func GetContextFromDex(w http.ResponseWriter, req *http.Request, clientID, baseU
return req.Context(), err
}
httpCtx := req.Context()
var roles []string
// switch case to handle multiple types of claims that can be extracted from the Dex Response
switch val := claims["groups"].(type) {
case []interface{}:
for _, group := range val {
groupName := strings.Trim(group.(string), "\"")
httpCtx = AddRoleToContext(httpCtx, w, req, groupName, DexRbacPolicy)
roles = CreateRoleString(groupName, roles, DexRbacPolicy)
}
case []string:
httpCtx = AddRoleToContext(httpCtx, w, req, strings.Join(val, ","), DexRbacPolicy)
roles = CreateRoleString(strings.Join(val, ","), roles, DexRbacPolicy)
case string:
httpCtx = AddRoleToContext(httpCtx, w, req, val, DexRbacPolicy)
roles = CreateRoleString(val, roles, DexRbacPolicy)
}

if claims["email"].(string) != "" {
httpCtx = AddRoleToContext(httpCtx, w, req, claims["email"].(string), DexRbacPolicy)
roles = CreateRoleString(claims["email"].(string), roles, DexRbacPolicy)
} else if claims["groups"] == nil {
return nil, fmt.Errorf("unable to parse token with expected fields for DEX login")
}
if len(roles) != 0 {
httpCtx = AddRoleToContext(w, req, roles)
}
return httpCtx, nil
}

0 comments on commit a0502e9

Please sign in to comment.