diff --git a/docs/pages/server-access/guides/host-user-creation.mdx b/docs/pages/server-access/guides/host-user-creation.mdx index cdd1fe23fd3d2..5b176db93dd65 100644 --- a/docs/pages/server-access/guides/host-user-creation.mdx +++ b/docs/pages/server-access/guides/host-user-creation.mdx @@ -86,6 +86,13 @@ will be disabled. Roles that do not match the Node will not be checked. + + +When multiple roles contain `host_sudoers` entries, the sudoers file +will have the entries written to it ordered by role name + + + If a role includes a `deny` rule that sets `host_sudoers` to `'*'`, the user will have all sudoers entries removed when accessing matching Nodes, otherwise `deny` rules are matched literally when filtering: diff --git a/lib/services/role.go b/lib/services/role.go index 39d4f3a47d7e8..14d0ffdc886de 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -2707,9 +2707,17 @@ func (set RoleSet) EnhancedRecordingSet() map[string]bool { // a role disallows host user creation func (set RoleSet) HostUsers(s types.Server) (*HostUsersInfo, error) { groups := make(map[string]struct{}) - sudoers := make(map[string]struct{}) + var sudoers []string serverLabels := s.GetAllLabels() - for _, role := range set { + + roleSet := make([]types.Role, len(set)) + copy(roleSet, set) + slices.SortStableFunc(roleSet, func(a types.Role, b types.Role) bool { + return strings.Compare(a.GetName(), b.GetName()) == -1 + }) + + seenSudoers := make(map[string]struct{}) + for _, role := range roleSet { result, _, err := MatchLabels(role.GetNodeLabels(types.Allow), serverLabels) if err != nil { return nil, trace.Wrap(err) @@ -2728,10 +2736,16 @@ func (set RoleSet) HostUsers(s types.Server) (*HostUsersInfo, error) { groups[group] = struct{}{} } for _, sudoer := range role.GetHostSudoers(types.Allow) { - sudoers[sudoer] = struct{}{} + if _, ok := seenSudoers[sudoer]; ok { + continue + } + seenSudoers[sudoer] = struct{}{} + sudoers = append(sudoers, sudoer) } } - for _, role := range set { + + var finalSudoers []string + for _, role := range roleSet { result, _, err := MatchLabels(role.GetNodeLabels(types.Deny), serverLabels) if err != nil { return nil, trace.Wrap(err) @@ -2742,18 +2756,25 @@ func (set RoleSet) HostUsers(s types.Server) (*HostUsersInfo, error) { for _, group := range role.GetHostGroups(types.Deny) { delete(groups, group) } - for _, sudoer := range role.GetHostSudoers(types.Deny) { - if sudoer == "*" { - sudoers = nil - break + + outer: + for _, sudoer := range sudoers { + for _, deniedSudoer := range role.GetHostSudoers(types.Deny) { + if deniedSudoer == "*" { + finalSudoers = nil + break outer + } + if sudoer != deniedSudoer { + finalSudoers = append(finalSudoers, sudoer) + } } - delete(sudoers, sudoer) } + sudoers = finalSudoers } return &HostUsersInfo{ Groups: utils.StringsSliceFromSet(groups), - Sudoers: utils.StringsSliceFromSet(sudoers), + Sudoers: sudoers, }, nil } diff --git a/lib/services/role_test.go b/lib/services/role_test.go index 6651d5903fde6..ab999f6156f95 100644 --- a/lib/services/role_test.go +++ b/lib/services/role_test.go @@ -5955,6 +5955,9 @@ func TestHostUsers_HostSudoers(t *testing.T) { test: "multiple roles, one not matching", sudoers: []string{"sudoers entry 1", "sudoers entry 2"}, roles: NewRoleSet(&types.RoleV6{ + Metadata: types.Metadata{ + Name: "a", + }, Spec: types.RoleSpecV6{ Options: types.RoleOptions{ CreateHostUser: types.NewBoolOption(true), @@ -5965,6 +5968,9 @@ func TestHostUsers_HostSudoers(t *testing.T) { }, }, }, &types.RoleV6{ + Metadata: types.Metadata{ + Name: "b", + }, Spec: types.RoleSpecV6{ Options: types.RoleOptions{ CreateHostUser: types.NewBoolOption(true), @@ -6060,6 +6066,108 @@ func TestHostUsers_HostSudoers(t *testing.T) { }, }, }, + { + test: "multiple roles, order preserved by role name", + sudoers: []string{"sudoers entry 1", "sudoers entry 2", "sudoers entry 3", "sudoers entry 4"}, + roles: NewRoleSet(&types.RoleV6{ + Metadata: types.Metadata{ + Name: "a", + }, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + CreateHostUser: types.NewBoolOption(true), + }, + Allow: types.RoleConditions{ + NodeLabels: types.Labels{"success": []string{"abc"}}, + HostSudoers: []string{"sudoers entry 1"}, + }, + }, + }, &types.RoleV6{ + Metadata: types.Metadata{ + Name: "c", + }, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + CreateHostUser: types.NewBoolOption(true), + }, + Allow: types.RoleConditions{ + NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}}, + HostSudoers: []string{"sudoers entry 4", "sudoers entry 1"}, + }, + }, + }, &types.RoleV6{ + Metadata: types.Metadata{ + Name: "b", + }, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + CreateHostUser: types.NewBoolOption(true), + }, + Allow: types.RoleConditions{ + NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}}, + HostSudoers: []string{"sudoers entry 2", "sudoers entry 3"}, + }, + }, + }), + server: &types.ServerV2{ + Metadata: types.Metadata{ + Labels: map[string]string{ + "success": "abc", + }, + }, + }, + }, + { + test: "duplication handled", + sudoers: []string{"sudoers entry 2"}, + roles: NewRoleSet(&types.RoleV6{ + Metadata: types.Metadata{ + Name: "a", + }, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + CreateHostUser: types.NewBoolOption(true), + }, + Allow: types.RoleConditions{ + NodeLabels: types.Labels{"success": []string{"abc"}}, + HostSudoers: []string{"sudoers entry 1"}, + }, + }, + }, &types.RoleV6{ // DENY sudoers entry 1 + Metadata: types.Metadata{ + Name: "d", + }, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + CreateHostUser: types.NewBoolOption(true), + }, + Deny: types.RoleConditions{ + NodeLabels: types.Labels{"success": []string{"abc"}}, + HostSudoers: []string{"sudoers entry 1"}, + }, + }, + }, &types.RoleV6{ // duplicate sudoers entry 1 case also gets removed + Metadata: types.Metadata{ + Name: "c", + }, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + CreateHostUser: types.NewBoolOption(true), + }, + Allow: types.RoleConditions{ + NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}}, + HostSudoers: []string{"sudoers entry 1", "sudoers entry 2"}, + }, + }, + }), + server: &types.ServerV2{ + Metadata: types.Metadata{ + Labels: map[string]string{ + "success": "abc", + }, + }, + }, + }, } { t.Run(tc.test, func(t *testing.T) { info, err := tc.roles.HostUsers(tc.server)