feature: (#568) Separator allowed in usergroup line for trusted headers (#572)

This commit is contained in:
James Read 2025-04-22 14:35:49 +01:00 committed by GitHub
parent eb2721c023
commit 633e513697
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 145 additions and 52 deletions

View File

@ -25,8 +25,8 @@ func (p PermissionBits) Has(permission PermissionBits) bool {
// User respresents a person. // User respresents a person.
type AuthenticatedUser struct { type AuthenticatedUser struct {
Username string Username string
Usergroup string UsergroupLine string
Provider string Provider string
SID string SID string
@ -40,6 +40,36 @@ func (u *AuthenticatedUser) IsGuest() bool {
return u.Username == "guest" && u.Provider == "system" return u.Username == "guest" && u.Provider == "system"
} }
func (u *AuthenticatedUser) parseUsergroupLine(sep string) []string {
ret := []string{}
if sep != "" {
for _, v := range strings.Split(u.UsergroupLine, sep) {
trimmed := strings.TrimSpace(v)
if trimmed != "" {
ret = append(ret, trimmed)
}
}
} else {
ret = strings.Fields(u.UsergroupLine)
}
return ret
}
func (u *AuthenticatedUser) matchesUsergroupAcl(matchUsergroups []string, sep string) bool {
groupList := u.parseUsergroupLine(sep)
for _, group := range groupList {
if slices.Contains(matchUsergroups, group) {
log.Debugf("Usergroup %v found in %+v (len: %v)", group, groupList, len(groupList))
return true
}
}
return false
}
func logAclNotMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) { func logAclNotMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
if cfg.LogDebugOptions.AclNotMatched { if cfg.LogDebugOptions.AclNotMatched {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
@ -101,7 +131,7 @@ func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"actionTitle": action.Title, "actionTitle": action.Title,
"username": user.Username, "username": user.Username,
"usergroup": user.Usergroup, "usergroupLine": user.UsergroupLine,
"relevantAcls": len(relevantAcls), "relevantAcls": len(relevantAcls),
"requiredPermission": requiredPermission, "requiredPermission": requiredPermission,
}).Debugf("ACL check - %v", aclFunction) }).Debugf("ACL check - %v", aclFunction)
@ -162,7 +192,7 @@ func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser
if ok { if ok {
ret = &AuthenticatedUser{} ret = &AuthenticatedUser{}
ret.Username = getMetadataKeyOrEmpty(md, "username") ret.Username = getMetadataKeyOrEmpty(md, "username")
ret.Usergroup = getMetadataKeyOrEmpty(md, "usergroup") ret.UsergroupLine = getMetadataKeyOrEmpty(md, "usergroup")
ret.Provider = getMetadataKeyOrEmpty(md, "provider") ret.Provider = getMetadataKeyOrEmpty(md, "provider")
buildUserAcls(cfg, ret) buildUserAcls(cfg, ret)
@ -173,10 +203,10 @@ func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser
} }
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"username": ret.Username, "username": ret.Username,
"usergroup": ret.Usergroup, "usergroupLine": ret.UsergroupLine,
"provider": ret.Provider, "provider": ret.Provider,
"acls": ret.Acls, "acls": ret.Acls,
}).Debugf("UserFromContext") }).Debugf("UserFromContext")
return ret return ret
@ -185,7 +215,7 @@ func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser
func UserGuest(cfg *config.Config) *AuthenticatedUser { func UserGuest(cfg *config.Config) *AuthenticatedUser {
ret := &AuthenticatedUser{} ret := &AuthenticatedUser{}
ret.Username = "guest" ret.Username = "guest"
ret.Usergroup = "guest" ret.UsergroupLine = "guest"
ret.Provider = "system" ret.Provider = "system"
buildUserAcls(cfg, ret) buildUserAcls(cfg, ret)
@ -195,9 +225,9 @@ func UserGuest(cfg *config.Config) *AuthenticatedUser {
func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser { func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser {
ret := &AuthenticatedUser{ ret := &AuthenticatedUser{
Username: username, Username: username,
Usergroup: "system", UsergroupLine: "system",
Provider: "system", Provider: "system",
} }
buildUserAcls(cfg, ret) buildUserAcls(cfg, ret)
@ -212,8 +242,7 @@ func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
continue continue
} }
// handle multiple usergroups - groups will be separated by a space if user.matchesUsergroupAcl(acl.MatchUsergroups, cfg.AuthHttpHeaderUserGroupSep) {
if hasGroupsMatch(acl.MatchUsergroups, user.Usergroup) {
user.Acls = append(user.Acls, acl.Name) user.Acls = append(user.Acls, acl.Name)
continue continue
} }
@ -222,15 +251,6 @@ func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
user.EffectivePolicy = getEffectivePolicy(cfg, user) user.EffectivePolicy = getEffectivePolicy(cfg, user)
} }
func hasGroupsMatch(matchUsergroups []string, usergroup string) bool {
for _, group := range strings.Fields(usergroup) {
if slices.Contains(matchUsergroups, group) {
return true
}
}
return false
}
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool { func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool {
if !slices.Contains(user.Acls, acl.Name) { if !slices.Contains(user.Acls, acl.Name) {
// If the user does not have this ACL, then it is not relevant // If the user does not have this ACL, then it is not relevant

View File

@ -1,37 +1,111 @@
package acl package acl
import "testing" import (
"github.com/stretchr/testify/assert"
"testing"
)
func Test_hasGroupsMatch(t *testing.T) { func Test_hasGroupsMatch(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
matchUsergroups []string aclMatchUsergroups []string
usergroup string usergroupLine string
want bool matches bool
sep string
}{ }{
{ {
name: "No groups match", name: "No groups match",
matchUsergroups: []string{"group1", "group2"}, aclMatchUsergroups: []string{"group1", "group2"},
usergroup: "group3", usergroupLine: "group3",
matches: false,
}, },
{ {
name: "Exact match", name: "Exact match",
matchUsergroups: []string{"group1", "group2"}, aclMatchUsergroups: []string{"group1", "group2"},
usergroup: "group1", usergroupLine: "group1",
want: true, matches: true,
}, },
{ {
name: "Multiple groups match", name: "Multiple groups match",
matchUsergroups: []string{"group1", "group2"}, aclMatchUsergroups: []string{"group1", "group2"},
usergroup: "group1 group2", usergroupLine: "group1 group2",
want: true, matches: true,
},
{
name: "Comma-separated groups match",
aclMatchUsergroups: []string{"group1", "group2", "group3"},
usergroupLine: "group1, group2",
matches: true,
sep: ",",
},
{
name: "Comma-separated groups with default separator does not match",
aclMatchUsergroups: []string{"group1"},
usergroupLine: "group1, group2",
matches: false,
sep: "",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := hasGroupsMatch(tt.matchUsergroups, tt.usergroup); got != tt.want { user := &AuthenticatedUser{
t.Errorf("hasGroupsMatch() = %v, want %v", got, tt.want) Username: "testuser",
UsergroupLine: tt.usergroupLine,
}
if matches := user.matchesUsergroupAcl(tt.aclMatchUsergroups, tt.sep); matches != tt.matches {
t.Errorf("AuthenticatedUser.matchesUsergroupAcl() = %v, want %v for usergroups %v", matches, tt.matches, tt.aclMatchUsergroups)
} }
}) })
} }
} }
func Test_parseUsergroupLine(t *testing.T) {
tests := []struct {
name string
usergroupLine string
expectedGroups []string
sep string
}{
{
name: "Default separator (space)",
usergroupLine: "group1 group2",
expectedGroups: []string{"group1", "group2"},
},
{
name: "Comma-separated groups",
usergroupLine: "group1 , group2",
expectedGroups: []string{"group1", "group2"},
sep: ",",
},
{
name: "Multiple spaces",
usergroupLine: "group1 , group2 , group3",
expectedGroups: []string{"group1", "group2", "group3"},
sep: ",",
},
{
name: "Empty usergroup line",
usergroupLine: "",
expectedGroups: []string{},
},
{
name: "Empty group names",
usergroupLine: "|group1| | group3|",
expectedGroups: []string{"group1", "group3"},
sep: "|",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user := &AuthenticatedUser{
Username: "testuser",
UsergroupLine: tt.usergroupLine,
}
assert.Equal(t, tt.expectedGroups, user.parseUsergroupLine(tt.sep))
})
}
}

View File

@ -124,6 +124,7 @@ type Config struct {
AuthJwtPubKeyPath string // will read pub key from file on disk AuthJwtPubKeyPath string // will read pub key from file on disk
AuthHttpHeaderUsername string AuthHttpHeaderUsername string
AuthHttpHeaderUserGroup string AuthHttpHeaderUserGroup string
AuthHttpHeaderUserGroupSep string
AuthLocalUsers AuthLocalUsersConfig AuthLocalUsers AuthLocalUsersConfig
AuthLoginUrl string AuthLoginUrl string
AuthRequireGuestsToLogin bool AuthRequireGuestsToLogin bool

View File

@ -6,12 +6,12 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"errors" "errors"
"fmt"
"net/mail" "net/mail"
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"fmt"
) )
var ( var (

View File

@ -186,8 +186,8 @@ func TestUnsetRequiredArgument(t *testing.T) {
Shell: "echo 'Your name is: {{ name }}'", Shell: "echo 'Your name is: {{ name }}'",
Arguments: []config.ActionArgument{ Arguments: []config.ActionArgument{
{ {
Name: "name", Name: "name",
Type: "ascii", Type: "ascii",
}, },
}, },
} }
@ -206,12 +206,12 @@ func TestUnusedArgumentStillPassesTypeSafetyCheck(t *testing.T) {
Shell: "echo 'Your name is: {{ name }}'", Shell: "echo 'Your name is: {{ name }}'",
Arguments: []config.ActionArgument{ Arguments: []config.ActionArgument{
{ {
Name: "name", Name: "name",
Type: "ascii", Type: "ascii",
}, },
{ {
Name: "age", Name: "age",
Type: "int", Type: "int",
}, },
}, },
} }

View File

@ -316,7 +316,7 @@ func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *apiv1.GetDa
if len(res.Actions) == 0 { if len(res.Actions) == 0 {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"username": user.Username, "username": user.Username,
"usergroup": user.Usergroup, "usergroupLine": user.UsergroupLine,
"provider": user.Provider, "provider": user.Provider,
"acls": user.Acls, "acls": user.Acls,
"availableActions": len(cfg.Actions), "availableActions": len(cfg.Actions),
@ -377,14 +377,12 @@ func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *apiv1.WhoAmIRequest) (*apiv
res := &apiv1.WhoAmIResponse{ res := &apiv1.WhoAmIResponse{
AuthenticatedUser: user.Username, AuthenticatedUser: user.Username,
Usergroup: user.Usergroup, Usergroup: user.UsergroupLine,
Provider: user.Provider, Provider: user.Provider,
Sid: user.SID, Sid: user.SID,
Acls: user.Acls, Acls: user.Acls,
} }
log.Warnf("usergroup: %v", user.Usergroup)
return res, nil return res, nil
} }