Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 28 additions & 27 deletions integration/hostuser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ func requireRoot(t *testing.T) {
func TestRootHostUsersBackend(t *testing.T) {
requireRoot(t)
sudoersTestDir := t.TempDir()
backend := srv.HostUsersProvisioningBackend{
usersbk := srv.HostUsersProvisioningBackend{}
sudoersbk := srv.HostSudoersProvisioningBackend{
SudoersPath: sudoersTestDir,
HostUUID: "hostuuid",
}
Expand All @@ -66,37 +67,37 @@ func TestRootHostUsersBackend(t *testing.T) {
})

t.Run("Test CreateGroup", func(t *testing.T) {
err := backend.CreateGroup(testgroup, "")
err := usersbk.CreateGroup(testgroup, "")
require.NoError(t, err)
err = backend.CreateGroup(testgroup, "")
err = usersbk.CreateGroup(testgroup, "")
require.True(t, trace.IsAlreadyExists(err))
})

t.Run("Test CreateUser and group", func(t *testing.T) {
err := backend.CreateUser(testuser, []string{testgroup}, "", "")
err := usersbk.CreateUser(testuser, []string{testgroup}, "", "")
require.NoError(t, err)

tuser, err := backend.Lookup(testuser)
tuser, err := usersbk.Lookup(testuser)
require.NoError(t, err)

group, err := backend.LookupGroup(testgroup)
group, err := usersbk.LookupGroup(testgroup)
require.NoError(t, err)

tuserGids, err := tuser.GroupIds()
require.NoError(t, err)
require.Contains(t, tuserGids, group.Gid)

err = backend.CreateUser(testuser, []string{}, "", "")
err = usersbk.CreateUser(testuser, []string{}, "", "")
require.True(t, trace.IsAlreadyExists(err))

require.NoFileExists(t, filepath.Join("/home", testuser))
err = backend.CreateHomeDirectory(testuser, tuser.Uid, tuser.Gid)
err = usersbk.CreateHomeDirectory(testuser, tuser.Uid, tuser.Gid)
require.NoError(t, err)
require.FileExists(t, filepath.Join("/home", testuser, ".bashrc"))
})

t.Run("Test DeleteUser", func(t *testing.T) {
err := backend.DeleteUser(testuser)
err := usersbk.DeleteUser(testuser)
require.NoError(t, err)

_, err = user.Lookup(testuser)
Expand All @@ -107,15 +108,15 @@ func TestRootHostUsersBackend(t *testing.T) {
checkUsers := []string{"teleport-usera", "teleport-userb", "teleport-userc"}
t.Cleanup(func() {
for _, u := range checkUsers {
backend.DeleteUser(u)
usersbk.DeleteUser(u)
}
})
for _, u := range checkUsers {
err := backend.CreateUser(u, []string{}, "", "")
err := usersbk.CreateUser(u, []string{}, "", "")
require.NoError(t, err)
}

users, err := backend.GetAllUsers()
users, err := usersbk.GetAllUsers()
require.NoError(t, err)
require.Subset(t, users, append(checkUsers, "root"))
})
Expand All @@ -125,25 +126,25 @@ func TestRootHostUsersBackend(t *testing.T) {
t.Skip("visudo not found on path")
}
validSudoersEntry := []byte("root ALL=(ALL) ALL")
err := backend.CheckSudoers(validSudoersEntry)
err := sudoersbk.CheckSudoers(validSudoersEntry)
require.NoError(t, err)
invalidSudoersEntry := []byte("yipee i broke sudo!!!!")
err = backend.CheckSudoers(invalidSudoersEntry)
err = sudoersbk.CheckSudoers(invalidSudoersEntry)
require.Contains(t, err.Error(), "visudo: invalid sudoers file")
// test sudoers entry containing . or ~
require.NoError(t, backend.WriteSudoersFile("user.name", validSudoersEntry))
require.NoError(t, sudoersbk.WriteSudoersFile("user.name", validSudoersEntry))
_, err = os.Stat(filepath.Join(sudoersTestDir, "teleport-hostuuid-user_name"))
require.NoError(t, err)
require.NoError(t, backend.RemoveSudoersFile("user.name"))
require.NoError(t, sudoersbk.RemoveSudoersFile("user.name"))
_, err = os.Stat(filepath.Join(sudoersTestDir, "teleport-hostuuid-user_name"))
require.True(t, os.IsNotExist(err))
})

t.Run("Test CreateHomeDirectory does not follow symlinks", func(t *testing.T) {
err := backend.CreateUser(testuser, []string{testgroup}, "", "")
err := usersbk.CreateUser(testuser, []string{testgroup}, "", "")
require.NoError(t, err)

tuser, err := backend.Lookup(testuser)
tuser, err := usersbk.Lookup(testuser)
require.NoError(t, err)

require.NoError(t, os.WriteFile("/etc/skel/testfile", []byte("test\n"), 0o700))
Expand All @@ -156,7 +157,7 @@ func TestRootHostUsersBackend(t *testing.T) {
require.NoError(t, os.Symlink("/tmp/ignoreme", bashrcPath))
require.NoFileExists(t, "/tmp/ignoreme")

err = backend.CreateHomeDirectory(testuser, tuser.Uid, tuser.Gid)
err = usersbk.CreateHomeDirectory(testuser, tuser.Uid, tuser.Gid)
t.Cleanup(func() {
os.RemoveAll(filepath.Join("/home", testuser))
})
Expand Down Expand Up @@ -260,6 +261,7 @@ func TestRootHostUsers(t *testing.T) {
}
uuid := "host_uuid"
users := srv.NewHostUsers(context.Background(), presence, uuid)
sudoers := srv.NewHostSudoers(uuid)

sudoersPath := func(username, uuid string) string {
return fmt.Sprintf("/etc/sudoers.d/teleport-%s-%s", uuid, username)
Expand All @@ -271,26 +273,25 @@ func TestRootHostUsers(t *testing.T) {
})
closer, err := users.CreateUser(testuser,
&services.HostUsersInfo{
Sudoers: []string{"ALL=(ALL) ALL"},
Mode: types.CreateHostUserMode_HOST_USER_MODE_DROP,
Mode: types.CreateHostUserMode_HOST_USER_MODE_DROP,
})
require.NoError(t, err)
err = sudoers.WriteSudoers(testuser, []string{"ALL=(ALL) ALL"})
require.NoError(t, err)
_, err = os.Stat(sudoersPath(testuser, uuid))
require.NoError(t, err)

// delete the user and ensure the sudoers file got deleted
require.NoError(t, closer.Close())
require.NoError(t, sudoers.RemoveSudoers(testuser))
_, err = os.Stat(sudoersPath(testuser, uuid))
require.True(t, os.IsNotExist(err))

// ensure invalid sudoers entries dont get written
closer, err = users.CreateUser(testuser,
&services.HostUsersInfo{
Sudoers: []string{"badsudoers entry!!!"},
Mode: types.CreateHostUserMode_HOST_USER_MODE_DROP,
})
err = sudoers.WriteSudoers(testuser,
[]string{"badsudoers entry!!!"},
)
require.Error(t, err)
defer closer.Close()
_, err = os.Stat(sudoersPath(testuser, uuid))
require.True(t, os.IsNotExist(err))
})
Expand Down
93 changes: 60 additions & 33 deletions lib/services/access_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ type AccessChecker interface {
// a role disallows host user creation
HostUsers(types.Server) (*HostUsersInfo, error)

// HostSudoers returns host sudoers entries matching a server
HostSudoers(types.Server) ([]string, error)

// DesktopGroups returns the desktop groups a user is allowed to create or an access denied error if a role disallows desktop user creation
DesktopGroups(types.WindowsDesktop) ([]string, error)

Expand Down Expand Up @@ -833,8 +836,6 @@ func (a *accessChecker) DesktopGroups(s types.WindowsDesktop) ([]string, error)
type HostUsersInfo struct {
// Groups is the list of groups to include host users in
Groups []string
// Sudoers is a list of entries for a users sudoers file
Sudoers []string
// Mode determines if a host user should be deleted after a session
// ends or not.
Mode types.CreateHostUserMode
Expand All @@ -848,17 +849,9 @@ type HostUsersInfo struct {
// a role disallows host user creation
func (a *accessChecker) HostUsers(s types.Server) (*HostUsersInfo, error) {
groups := make(map[string]struct{})
var sudoers []string
var mode types.CreateHostUserMode

roleSet := make([]types.Role, len(a.RoleSet))
copy(roleSet, a.RoleSet)
slices.SortStableFunc(roleSet, func(a types.Role, b types.Role) int {
return strings.Compare(a.GetName(), b.GetName())
})

seenSudoers := make(map[string]struct{})
for _, role := range roleSet {
for _, role := range a.RoleSet {
result, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Traits, s, false)
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -894,6 +887,61 @@ func (a *accessChecker) HostUsers(s types.Server) (*HostUsersInfo, error) {
for _, group := range role.GetHostGroups(types.Allow) {
groups[group] = struct{}{}
}
}

for _, role := range a.RoleSet {
result, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Traits, s, false)
if err != nil {
return nil, trace.Wrap(err)
}
if !result {
continue
}
for _, group := range role.GetHostGroups(types.Deny) {
delete(groups, group)
}
}

traits := a.Traits()
var gid string
gidL := traits[constants.TraitHostUserGID]
if len(gidL) >= 1 {
gid = gidL[0]
}
var uid string
uidL := traits[constants.TraitHostUserUID]
if len(uidL) >= 1 {
uid = uidL[0]
}

return &HostUsersInfo{
Groups: utils.StringsSliceFromSet(groups),
Mode: mode,
UID: uid,
GID: gid,
}, nil
}

// HostSudoers returns host sudoers entries matching a server
func (a *accessChecker) HostSudoers(s types.Server) ([]string, error) {
var sudoers []string

roleSet := slices.Clone(a.RoleSet)
slices.SortFunc(roleSet, func(a types.Role, b types.Role) int {
return strings.Compare(a.GetName(), b.GetName())
})

seenSudoers := make(map[string]struct{})
for _, role := range roleSet {
result, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Traits, s, false)
if err != nil {
return nil, trace.Wrap(err)
}
// skip nodes that dont have matching labels
if !result {
continue
}

for _, sudoer := range role.GetHostSudoers(types.Allow) {
if _, ok := seenSudoers[sudoer]; ok {
continue
Expand All @@ -912,9 +960,6 @@ func (a *accessChecker) HostUsers(s types.Server) (*HostUsersInfo, error) {
if !result {
continue
}
for _, group := range role.GetHostGroups(types.Deny) {
delete(groups, group)
}

outer:
for _, sudoer := range sudoers {
Expand All @@ -931,25 +976,7 @@ func (a *accessChecker) HostUsers(s types.Server) (*HostUsersInfo, error) {
sudoers = finalSudoers
}

traits := a.Traits()
var gid string
gidL := traits[constants.TraitHostUserGID]
if len(gidL) >= 1 {
gid = gidL[0]
}
var uid string
uidL := traits[constants.TraitHostUserUID]
if len(uidL) >= 1 {
uid = uidL[0]
}

return &HostUsersInfo{
Groups: utils.StringsSliceFromSet(groups),
Sudoers: sudoers,
Mode: mode,
UID: uid,
GID: gid,
}, nil
return sudoers, nil
}

// AccessInfoFromLocalCertificate returns a new AccessInfo populated from the
Expand Down
Loading