summaryrefslogtreecommitdiffhomepage
path: root/util/osuser/user.go
blob: 2de3da762739dd8bc3954e7e459a55dcba7cd9a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// Copyright (c) Tailscale Inc & contributors
// 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
}