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 +}