| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- // Package osuser implements OS user lookup. It's a wrapper around os/user that
- // works on non-cgo builds.
- package osuser
- import (
- "context"
- "errors"
- "log"
- "os/exec"
- "os/user"
- "runtime"
- "strings"
- "time"
- "unicode/utf8"
- "tailscale.com/version/distro"
- )
- // LookupByUIDWithShell is like os/user.LookupId but handles a few edge cases
- // like gokrazy and non-cgo lookups, and returns the user shell. The user shell
- // lookup is best-effort and may be empty.
- func LookupByUIDWithShell(uid string) (u *user.User, shell string, err error) {
- return lookup(uid, user.LookupId, true)
- }
- // LookupByUsernameWithShell is like os/user.Lookup but handles a few edge
- // cases like gokrazy and non-cgo lookups, and returns the user shell. The user
- // shell lookup is best-effort and may be empty.
- func LookupByUsernameWithShell(username string) (u *user.User, shell string, err error) {
- return lookup(username, user.Lookup, true)
- }
- // LookupByUID is like os/user.LookupId but handles a few edge cases like
- // gokrazy and non-cgo lookups.
- func LookupByUID(uid string) (*user.User, error) {
- u, _, err := lookup(uid, user.LookupId, false)
- return u, err
- }
- // LookupByUsername is like os/user.Lookup but handles a few edge cases like
- // gokrazy and non-cgo lookups.
- func LookupByUsername(username string) (*user.User, error) {
- u, _, err := lookup(username, user.Lookup, false)
- return u, err
- }
- // lookupStd is either user.Lookup or user.LookupId.
- type lookupStd func(string) (*user.User, error)
- func lookup(usernameOrUID string, std lookupStd, wantShell bool) (*user.User, string, error) {
- // Skip getent entirely on Non-Unix platforms that won't ever have it.
- // (Using HasPrefix for "wasip1", anticipating that WASI support will
- // move beyond "preview 1" some day.)
- if runtime.GOOS == "windows" || runtime.GOOS == "js" || runtime.GOARCH == "wasm" || runtime.GOOS == "plan9" {
- var shell string
- if wantShell && runtime.GOOS == "plan9" {
- shell = "/bin/rc"
- }
- if runtime.GOOS == "plan9" {
- if u, err := user.Current(); err == nil {
- return u, shell, nil
- }
- }
- u, err := std(usernameOrUID)
- return u, shell, err
- }
- // No getent on Gokrazy. So hard-code the login shell.
- if distro.Get() == distro.Gokrazy {
- var shell string
- if wantShell {
- shell = "/tmp/serial-busybox/ash"
- }
- u, err := std(usernameOrUID)
- if err != nil {
- return &user.User{
- Uid: "0",
- Gid: "0",
- Username: "root",
- Name: "Gokrazy",
- HomeDir: "/",
- }, shell, nil
- }
- return u, shell, nil
- }
- if runtime.GOOS == "plan9" {
- return &user.User{
- Uid: "0",
- Gid: "0",
- Username: "glenda",
- Name: "Glenda",
- HomeDir: "/",
- }, "/bin/rc", nil
- }
- // Start with getent if caller wants to get the user shell.
- if wantShell {
- return userLookupGetent(usernameOrUID, std)
- }
- // If shell is not required, try os/user.Lookup* first and only use getent
- // if that fails. This avoids spawning a child process when os/user lookup
- // succeeds.
- if u, err := std(usernameOrUID); err == nil {
- return u, "", nil
- }
- return userLookupGetent(usernameOrUID, std)
- }
- func checkGetentInput(usernameOrUID string) bool {
- maxUid := 32
- if runtime.GOOS == "linux" {
- maxUid = 256
- }
- if len(usernameOrUID) > maxUid || len(usernameOrUID) == 0 {
- return false
- }
- for _, r := range usernameOrUID {
- if r < ' ' || r == 0x7f || r == utf8.RuneError { // TODO(bradfitz): more?
- return false
- }
- }
- return true
- }
- // userLookupGetent uses "getent" to look up users so that even with static
- // tailscaled binaries without cgo (as we distribute), we can still look up
- // PAM/NSS users which the standard library's os/user without cgo won't get
- // (because of no libc hooks). If "getent" fails, userLookupGetent falls back
- // to the standard library.
- func userLookupGetent(usernameOrUID string, std lookupStd) (*user.User, string, error) {
- // Do some basic validation before passing this string to "getent", even though
- // getent should do its own validation.
- if !checkGetentInput(usernameOrUID) {
- return nil, "", errors.New("invalid username or UID")
- }
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- out, err := exec.CommandContext(ctx, "getent", "passwd", usernameOrUID).Output()
- if err != nil {
- log.Printf("error calling getent for user %q: %v", usernameOrUID, err)
- u, err := std(usernameOrUID)
- return u, "", err
- }
- // output is "alice:x:1001:1001:Alice Smith,,,:/home/alice:/bin/bash"
- f := strings.SplitN(strings.TrimSpace(string(out)), ":", 10)
- for len(f) < 7 {
- f = append(f, "")
- }
- var mandatoryFields = []int{0, 2, 3, 5}
- for _, v := range mandatoryFields {
- if f[v] == "" {
- log.Printf("getent for user %q returned invalid output: %q", usernameOrUID, out)
- u, err := std(usernameOrUID)
- return u, "", err
- }
- }
- return &user.User{
- Username: f[0],
- Uid: f[2],
- Gid: f[3],
- Name: f[4],
- HomeDir: f[5],
- }, f[6], nil
- }
|