From e985c6e58ffb0f00311b6f7a33e3e82c56bee967 Mon Sep 17 00:00:00 2001 From: Andrew Dunham Date: Sun, 21 Apr 2024 22:00:28 -0400 Subject: [PATCH] ssh/tailssh: try fetching group IDs for user with the 'id' command Since the tailscaled binaries that we distribute are static and don't link cgo, we previously wouldn't fetch group IDs that are returned via NSS. Try shelling out to the 'id' command, similar to how we call 'getent', to detect such cases. Updates #11682 Signed-off-by: Andrew Dunham Change-Id: I9bdc938bd76c71bc130d44a97cc2233064d64799 --- ssh/tailssh/user.go | 10 ++------ util/osuser/group_ids.go | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 util/osuser/group_ids.go diff --git a/ssh/tailssh/user.go b/ssh/tailssh/user.go index b27d605ed..33ebb4db7 100644 --- a/ssh/tailssh/user.go +++ b/ssh/tailssh/user.go @@ -34,14 +34,7 @@ type userMeta struct { // GroupIds returns the list of group IDs that the user is a member of. func (u *userMeta) GroupIds() ([]string, error) { - if runtime.GOOS == "linux" && distro.Get() == distro.Gokrazy { - // Gokrazy is a single-user appliance with ~no userspace. - // There aren't users to look up (no /etc/passwd, etc) - // so rather than fail below, just hardcode root. - // TODO(bradfitz): fix os/user upstream instead? - return []string{"0"}, nil - } - return u.User.GroupIds() + return osuser.GetGroupIds(&u.User) } // userLookup is like os/user.Lookup but it returns a *userMeta wrapper @@ -51,6 +44,7 @@ func userLookup(username string) (*userMeta, error) { if err != nil { return nil, err } + return &userMeta{User: *u, loginShellCached: s}, nil } diff --git a/util/osuser/group_ids.go b/util/osuser/group_ids.go new file mode 100644 index 000000000..a08472598 --- /dev/null +++ b/util/osuser/group_ids.go @@ -0,0 +1,50 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package osuser + +import ( + "context" + "fmt" + "os/exec" + "os/user" + "runtime" + "strings" + "time" + + "tailscale.com/version/distro" +) + +// GetGroupIds returns the list of group IDs that the user is a member of, or +// an error. It will first try to use the 'id' command to get the group IDs, +// and if that fails, it will fall back to the user.GroupIds method. +func GetGroupIds(user *user.User) ([]string, error) { + if runtime.GOOS != "linux" { + return user.GroupIds() + } + + if distro.Get() == distro.Gokrazy { + // Gokrazy is a single-user appliance with ~no userspace. + // There aren't users to look up (no /etc/passwd, etc) + // so rather than fail below, just hardcode root. + // TODO(bradfitz): fix os/user upstream instead? + return []string{"0"}, nil + } + + if ids, err := getGroupIdsWithId(user.Username); err == nil { + return ids, nil + } + return user.GroupIds() +} + +func getGroupIdsWithId(usernameOrUID string) ([]string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "id", "-Gz", usernameOrUID) + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("running 'id' command: %w", err) + } + return strings.Split(string(out), "\x00"), nil +}