mirror of
https://github.com/golang/go
synced 2024-11-23 21:10:05 -07:00
os/user: add non-cgo versions of Lookup, LookupId
If you cross compile for a Unix target and call user.Lookup("root") or user.LookupId("0"), we'll try to read the answer out of /etc/passwd instead of returning an "unimplemented" error. The equivalent cgo function calls getpwuid_r in glibc, which may reach out to the NSS database or allow callers to register extensions. The pure Go implementation only reads from /etc/passwd. Change-Id: I56a302d634b15ba5097f9f0d6a758c68e486ba6d Reviewed-on: https://go-review.googlesource.com/37664 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
parent
4fc45ae879
commit
c2eb06193f
@ -15,7 +15,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
userImplemented = false
|
|
||||||
groupImplemented = false
|
groupImplemented = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,14 +45,6 @@ func current() (*User, error) {
|
|||||||
return u, fmt.Errorf("user: Current not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
return u, fmt.Errorf("user: Current not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupUser(username string) (*User, error) {
|
|
||||||
return nil, errors.New("user: Lookup requires cgo")
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupUserId(uid string) (*User, error) {
|
|
||||||
return nil, errors.New("user: LookupId requires cgo")
|
|
||||||
}
|
|
||||||
|
|
||||||
func listGroups(*User) ([]string, error) {
|
func listGroups(*User) ([]string, error) {
|
||||||
return nil, errors.New("user: GroupIds requires cgo")
|
return nil, errors.New("user: GroupIds requires cgo")
|
||||||
}
|
}
|
||||||
|
@ -10,12 +10,15 @@ package user
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const groupFile = "/etc/group"
|
const groupFile = "/etc/group"
|
||||||
|
const userFile = "/etc/passwd"
|
||||||
|
|
||||||
var colon = []byte{':'}
|
var colon = []byte{':'}
|
||||||
|
|
||||||
@ -23,54 +26,140 @@ func init() {
|
|||||||
groupImplemented = false
|
groupImplemented = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func findGroupId(id string, r io.Reader) (*Group, error) {
|
// lineFunc returns a value, an error, or (nil, nil) to skip the row.
|
||||||
|
type lineFunc func(line []byte) (v interface{}, err error)
|
||||||
|
|
||||||
|
// readColonFile parses r as an /etc/group or /etc/passwd style file, running
|
||||||
|
// fn for each row. readColonFile returns a value, an error, or (nil, nil) if
|
||||||
|
// the end of the file is reached without a match.
|
||||||
|
func readColonFile(r io.Reader, fn lineFunc) (v interface{}, err error) {
|
||||||
bs := bufio.NewScanner(r)
|
bs := bufio.NewScanner(r)
|
||||||
substr := []byte(":" + id)
|
|
||||||
for bs.Scan() {
|
for bs.Scan() {
|
||||||
lineBytes := bs.Bytes()
|
line := bs.Bytes()
|
||||||
if !bytes.Contains(lineBytes, substr) || bytes.Count(lineBytes, colon) < 3 {
|
// There's no spec for /etc/passwd or /etc/group, but we try to follow
|
||||||
|
// the same rules as the glibc parser, which allows comments and blank
|
||||||
|
// space at the beginning of a line.
|
||||||
|
line = bytes.TrimSpace(line)
|
||||||
|
if len(line) == 0 || line[0] == '#' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
text := strings.TrimSpace(removeComment(string(lineBytes)))
|
v, err = fn(line)
|
||||||
// wheel:*:0:root
|
if v != nil || err != nil {
|
||||||
parts := strings.SplitN(text, ":", 4)
|
return
|
||||||
if len(parts) < 4 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if parts[2] == id {
|
|
||||||
return &Group{Name: parts[0], Gid: parts[2]}, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := bs.Err(); err != nil {
|
return nil, bs.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchGroupIndexValue(value string, idx int) lineFunc {
|
||||||
|
var leadColon string
|
||||||
|
if idx > 0 {
|
||||||
|
leadColon = ":"
|
||||||
|
}
|
||||||
|
substr := []byte(leadColon + value + ":")
|
||||||
|
return func(line []byte) (v interface{}, err error) {
|
||||||
|
if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// wheel:*:0:root
|
||||||
|
parts := strings.SplitN(string(line), ":", 4)
|
||||||
|
if len(parts) < 4 || parts[0] == "" || parts[idx] != value ||
|
||||||
|
// If the file contains +foo and you search for "foo", glibc
|
||||||
|
// returns an "invalid argument" error. Similarly, if you search
|
||||||
|
// for a gid for a row where the group name starts with "+" or "-",
|
||||||
|
// glibc fails to find the record.
|
||||||
|
parts[0][0] == '+' || parts[0][0] == '-' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := strconv.Atoi(parts[2]); err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &Group{Name: parts[0], Gid: parts[2]}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findGroupId(id string, r io.Reader) (*Group, error) {
|
||||||
|
if v, err := readColonFile(r, matchGroupIndexValue(id, 2)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if v != nil {
|
||||||
|
return v.(*Group), nil
|
||||||
}
|
}
|
||||||
return nil, UnknownGroupIdError(id)
|
return nil, UnknownGroupIdError(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findGroupName(name string, r io.Reader) (*Group, error) {
|
func findGroupName(name string, r io.Reader) (*Group, error) {
|
||||||
bs := bufio.NewScanner(r)
|
if v, err := readColonFile(r, matchGroupIndexValue(name, 0)); err != nil {
|
||||||
substr := []byte(name + ":")
|
|
||||||
for bs.Scan() {
|
|
||||||
lineBytes := bs.Bytes()
|
|
||||||
if !bytes.Contains(lineBytes, substr) || bytes.Count(lineBytes, colon) < 3 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
text := strings.TrimSpace(removeComment(string(lineBytes)))
|
|
||||||
// wheel:*:0:root
|
|
||||||
parts := strings.SplitN(text, ":", 4)
|
|
||||||
if len(parts) < 4 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if parts[0] == name && parts[2] != "" {
|
|
||||||
return &Group{Name: parts[0], Gid: parts[2]}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := bs.Err(); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if v != nil {
|
||||||
|
return v.(*Group), nil
|
||||||
}
|
}
|
||||||
return nil, UnknownGroupError(name)
|
return nil, UnknownGroupError(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns a *User for a row if that row's has the given value at the
|
||||||
|
// given index.
|
||||||
|
func matchUserIndexValue(value string, idx int) lineFunc {
|
||||||
|
var leadColon string
|
||||||
|
if idx > 0 {
|
||||||
|
leadColon = ":"
|
||||||
|
}
|
||||||
|
substr := []byte(leadColon + value + ":")
|
||||||
|
return func(line []byte) (v interface{}, err error) {
|
||||||
|
if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 6 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// kevin:x:1005:1006::/home/kevin:/usr/bin/zsh
|
||||||
|
parts := strings.SplitN(string(line), ":", 7)
|
||||||
|
if len(parts) < 6 || parts[idx] != value || parts[0] == "" ||
|
||||||
|
parts[0][0] == '+' || parts[0][0] == '-' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := strconv.Atoi(parts[2]); err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if _, err := strconv.Atoi(parts[3]); err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
u := &User{
|
||||||
|
Username: parts[0],
|
||||||
|
Uid: parts[2],
|
||||||
|
Gid: parts[3],
|
||||||
|
Name: parts[4],
|
||||||
|
HomeDir: parts[5],
|
||||||
|
}
|
||||||
|
// The pw_gecos field isn't quite standardized. Some docs
|
||||||
|
// say: "It is expected to be a comma separated list of
|
||||||
|
// personal data where the first item is the full name of the
|
||||||
|
// user."
|
||||||
|
if i := strings.Index(u.Name, ","); i >= 0 {
|
||||||
|
u.Name = u.Name[:i]
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findUserId(uid string, r io.Reader) (*User, error) {
|
||||||
|
i, e := strconv.Atoi(uid)
|
||||||
|
if e != nil {
|
||||||
|
return nil, errors.New("user: invalid userid " + uid)
|
||||||
|
}
|
||||||
|
if v, err := readColonFile(r, matchUserIndexValue(uid, 2)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if v != nil {
|
||||||
|
return v.(*User), nil
|
||||||
|
}
|
||||||
|
return nil, UnknownUserIdError(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findUsername(name string, r io.Reader) (*User, error) {
|
||||||
|
if v, err := readColonFile(r, matchUserIndexValue(name, 0)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if v != nil {
|
||||||
|
return v.(*User), nil
|
||||||
|
}
|
||||||
|
return nil, UnknownUserError(name)
|
||||||
|
}
|
||||||
|
|
||||||
func lookupGroup(groupname string) (*Group, error) {
|
func lookupGroup(groupname string) (*Group, error) {
|
||||||
f, err := os.Open(groupFile)
|
f, err := os.Open(groupFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -89,11 +178,20 @@ func lookupGroupId(id string) (*Group, error) {
|
|||||||
return findGroupId(id, f)
|
return findGroupId(id, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeComment returns line, removing any '#' byte and any following
|
func lookupUser(username string) (*User, error) {
|
||||||
// bytes.
|
f, err := os.Open(userFile)
|
||||||
func removeComment(line string) string {
|
if err != nil {
|
||||||
if i := strings.Index(line, "#"); i != -1 {
|
return nil, err
|
||||||
return line[:i]
|
|
||||||
}
|
}
|
||||||
return line
|
defer f.Close()
|
||||||
|
return findUsername(username, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupUserId(uid string) (*User, error) {
|
||||||
|
f, err := os.Open(userFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return findUserId(uid, f)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -19,6 +20,9 @@ nobody:*:-2:
|
|||||||
nogroup:*:-1:
|
nogroup:*:-1:
|
||||||
wheel:*:0:root
|
wheel:*:0:root
|
||||||
emptyid:*::root
|
emptyid:*::root
|
||||||
|
invalidgid:*:notanumber:root
|
||||||
|
+plussign:*:20:root
|
||||||
|
-minussign:*:21:root
|
||||||
|
|
||||||
daemon:*:1:root
|
daemon:*:1:root
|
||||||
indented:*:7:
|
indented:*:7:
|
||||||
@ -36,7 +40,12 @@ var groupTests = []struct {
|
|||||||
{testGroupFile, "kmem", "2"},
|
{testGroupFile, "kmem", "2"},
|
||||||
{testGroupFile, "notinthefile", ""},
|
{testGroupFile, "notinthefile", ""},
|
||||||
{testGroupFile, "comment", ""},
|
{testGroupFile, "comment", ""},
|
||||||
|
{testGroupFile, "plussign", ""},
|
||||||
|
{testGroupFile, "+plussign", ""},
|
||||||
|
{testGroupFile, "-minussign", ""},
|
||||||
|
{testGroupFile, "minussign", ""},
|
||||||
{testGroupFile, "emptyid", ""},
|
{testGroupFile, "emptyid", ""},
|
||||||
|
{testGroupFile, "invalidgid", ""},
|
||||||
{testGroupFile, "indented", "7"},
|
{testGroupFile, "indented", "7"},
|
||||||
{testGroupFile, "# comment", ""},
|
{testGroupFile, "# comment", ""},
|
||||||
{"", "emptyfile", ""},
|
{"", "emptyfile", ""},
|
||||||
@ -83,6 +92,8 @@ var groupIdTests = []struct {
|
|||||||
{testGroupFile, "comment", ""},
|
{testGroupFile, "comment", ""},
|
||||||
{testGroupFile, "7", "indented"},
|
{testGroupFile, "7", "indented"},
|
||||||
{testGroupFile, "4", ""},
|
{testGroupFile, "4", ""},
|
||||||
|
{testGroupFile, "20", ""}, // row starts with a plus
|
||||||
|
{testGroupFile, "21", ""}, // row starts with a minus
|
||||||
{"", "emptyfile", ""},
|
{"", "emptyfile", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,3 +126,151 @@ func TestFindGroupId(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const testUserFile = ` # Example user file
|
||||||
|
root:x:0:0:root:/root:/bin/bash
|
||||||
|
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
|
||||||
|
bin:x:2:3:bin:/bin:/usr/sbin/nologin
|
||||||
|
indented:x:3:3:indented:/dev:/usr/sbin/nologin
|
||||||
|
sync:x:4:65534:sync:/bin:/bin/sync
|
||||||
|
negative:x:-5:60:games:/usr/games:/usr/sbin/nologin
|
||||||
|
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
|
||||||
|
allfields:x:6:12:mansplit,man2,man3,man4:/home/allfields:/usr/sbin/nologin
|
||||||
|
+plussign:x:8:10:man:/var/cache/man:/usr/sbin/nologin
|
||||||
|
-minussign:x:9:10:man:/var/cache/man:/usr/sbin/nologin
|
||||||
|
|
||||||
|
malformed:x:27:12 # more:colons:after:comment
|
||||||
|
|
||||||
|
struid:x:notanumber:12 # more:colons:after:comment
|
||||||
|
|
||||||
|
# commented:x:28:12:commented:/var/cache/man:/usr/sbin/nologin
|
||||||
|
# commentindented:x:29:12:commentindented:/var/cache/man:/usr/sbin/nologin
|
||||||
|
|
||||||
|
struid2:x:30:badgid:struid2name:/home/struid:/usr/sbin/nologin
|
||||||
|
`
|
||||||
|
|
||||||
|
var userIdTests = []struct {
|
||||||
|
in string
|
||||||
|
uid string
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{testUserFile, "-5", "negative"},
|
||||||
|
{testUserFile, "2", "bin"},
|
||||||
|
{testUserFile, "100", ""}, // not in the file
|
||||||
|
{testUserFile, "8", ""}, // plus sign, glibc doesn't find it
|
||||||
|
{testUserFile, "9", ""}, // minus sign, glibc doesn't find it
|
||||||
|
{testUserFile, "27", ""}, // malformed
|
||||||
|
{testUserFile, "28", ""}, // commented out
|
||||||
|
{testUserFile, "29", ""}, // commented out, indented
|
||||||
|
{testUserFile, "3", "indented"},
|
||||||
|
{testUserFile, "30", ""}, // the Gid is not valid, shouldn't match
|
||||||
|
{"", "1", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidUserId(t *testing.T) {
|
||||||
|
_, err := findUserId("notanumber", strings.NewReader(""))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("findUserId('notanumber'): got nil error")
|
||||||
|
}
|
||||||
|
if want := "user: invalid userid notanumber"; err.Error() != want {
|
||||||
|
t.Errorf("findUserId('notanumber'): got %v, want %s", err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupUserId(t *testing.T) {
|
||||||
|
for _, tt := range userIdTests {
|
||||||
|
got, err := findUserId(tt.uid, strings.NewReader(tt.in))
|
||||||
|
if tt.name == "" {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("findUserId(%s): got nil error, expected err", tt.uid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch terr := err.(type) {
|
||||||
|
case UnknownUserIdError:
|
||||||
|
if want := "user: unknown userid " + tt.uid; terr.Error() != want {
|
||||||
|
t.Errorf("findUserId(%s): got %v, want %v", tt.name, terr, want)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Errorf("findUserId(%s): got unexpected error %v", tt.name, terr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("findUserId(%s): got unexpected error %v", tt.name, err)
|
||||||
|
}
|
||||||
|
if got.Uid != tt.uid {
|
||||||
|
t.Errorf("findUserId(%s): got uid %v, want %s", tt.name, got.Uid, tt.uid)
|
||||||
|
}
|
||||||
|
if got.Username != tt.name {
|
||||||
|
t.Errorf("findUserId(%s): got name %s, want %s", tt.name, got.Username, tt.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupUserPopulatesAllFields(t *testing.T) {
|
||||||
|
u, err := findUsername("allfields", strings.NewReader(testUserFile))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := &User{
|
||||||
|
Username: "allfields",
|
||||||
|
Uid: "6",
|
||||||
|
Gid: "12",
|
||||||
|
Name: "mansplit",
|
||||||
|
HomeDir: "/home/allfields",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(u, want) {
|
||||||
|
t.Errorf("findUsername: got %#v, want %#v", u, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userTests = []struct {
|
||||||
|
in string
|
||||||
|
name string
|
||||||
|
uid string
|
||||||
|
}{
|
||||||
|
{testUserFile, "negative", "-5"},
|
||||||
|
{testUserFile, "bin", "2"},
|
||||||
|
{testUserFile, "notinthefile", ""},
|
||||||
|
{testUserFile, "indented", "3"},
|
||||||
|
{testUserFile, "plussign", ""},
|
||||||
|
{testUserFile, "+plussign", ""},
|
||||||
|
{testUserFile, "minussign", ""},
|
||||||
|
{testUserFile, "-minussign", ""},
|
||||||
|
{testUserFile, " indented", ""},
|
||||||
|
{testUserFile, "commented", ""},
|
||||||
|
{testUserFile, "commentindented", ""},
|
||||||
|
{testUserFile, "malformed", ""},
|
||||||
|
{testUserFile, "# commented", ""},
|
||||||
|
{"", "emptyfile", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupUser(t *testing.T) {
|
||||||
|
for _, tt := range userTests {
|
||||||
|
got, err := findUsername(tt.name, strings.NewReader(tt.in))
|
||||||
|
if tt.uid == "" {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("lookupUser(%s): got nil error, expected err", tt.uid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch terr := err.(type) {
|
||||||
|
case UnknownUserError:
|
||||||
|
if want := "user: unknown user " + tt.name; terr.Error() != want {
|
||||||
|
t.Errorf("lookupUser(%s): got %v, want %v", tt.name, terr, want)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Errorf("lookupUser(%s): got unexpected error %v", tt.name, terr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("lookupUser(%s): got unexpected error %v", tt.name, err)
|
||||||
|
}
|
||||||
|
if got.Uid != tt.uid {
|
||||||
|
t.Errorf("lookupUser(%s): got uid %v, want %s", tt.name, got.Uid, tt.uid)
|
||||||
|
}
|
||||||
|
if got.Username != tt.name {
|
||||||
|
t.Errorf("lookupUser(%s): got name %s, want %s", tt.name, got.Username, tt.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -70,6 +70,9 @@ func TestLookup(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Current: %v", err)
|
t.Fatalf("Current: %v", err)
|
||||||
}
|
}
|
||||||
|
// TODO: Lookup() has a fast path that calls Current() and returns if the
|
||||||
|
// usernames match, so this test does not exercise very much. It would be
|
||||||
|
// good to try and test finding a different user than the current user.
|
||||||
got, err := Lookup(want.Username)
|
got, err := Lookup(want.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Lookup: %v", err)
|
t.Fatalf("Lookup: %v", err)
|
||||||
|
Loading…
Reference in New Issue
Block a user