diff options
Diffstat (limited to 'src/os/user')
-rw-r--r-- | src/os/user/cgo_lookup_unix.go | 273 | ||||
-rw-r--r-- | src/os/user/cgo_unix_test.go | 24 | ||||
-rw-r--r-- | src/os/user/getgrouplist_darwin.go | 54 | ||||
-rw-r--r-- | src/os/user/getgrouplist_unix.go | 42 | ||||
-rw-r--r-- | src/os/user/listgroups_aix.go | 13 | ||||
-rw-r--r-- | src/os/user/listgroups_solaris.go | 17 | ||||
-rw-r--r-- | src/os/user/listgroups_unix.go | 49 | ||||
-rw-r--r-- | src/os/user/lookup.go | 63 | ||||
-rw-r--r-- | src/os/user/lookup_android.go | 25 | ||||
-rw-r--r-- | src/os/user/lookup_plan9.go | 61 | ||||
-rw-r--r-- | src/os/user/lookup_stubs.go | 88 | ||||
-rw-r--r-- | src/os/user/lookup_unix.go | 197 | ||||
-rw-r--r-- | src/os/user/lookup_unix_test.go | 276 | ||||
-rw-r--r-- | src/os/user/lookup_windows.go | 385 | ||||
-rw-r--r-- | src/os/user/user.go | 90 | ||||
-rw-r--r-- | src/os/user/user_test.go | 158 |
16 files changed, 1815 insertions, 0 deletions
diff --git a/src/os/user/cgo_lookup_unix.go b/src/os/user/cgo_lookup_unix.go new file mode 100644 index 0000000..3307f79 --- /dev/null +++ b/src/os/user/cgo_lookup_unix.go @@ -0,0 +1,273 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris +// +build cgo,!osusergo + +package user + +import ( + "fmt" + "strconv" + "strings" + "syscall" + "unsafe" +) + +/* +#cgo solaris CFLAGS: -D_POSIX_PTHREAD_SEMANTICS +#include <unistd.h> +#include <sys/types.h> +#include <pwd.h> +#include <grp.h> +#include <stdlib.h> + +static int mygetpwuid_r(int uid, struct passwd *pwd, + char *buf, size_t buflen, struct passwd **result) { + return getpwuid_r(uid, pwd, buf, buflen, result); +} + +static int mygetpwnam_r(const char *name, struct passwd *pwd, + char *buf, size_t buflen, struct passwd **result) { + return getpwnam_r(name, pwd, buf, buflen, result); +} + +static int mygetgrgid_r(int gid, struct group *grp, + char *buf, size_t buflen, struct group **result) { + return getgrgid_r(gid, grp, buf, buflen, result); +} + +static int mygetgrnam_r(const char *name, struct group *grp, + char *buf, size_t buflen, struct group **result) { + return getgrnam_r(name, grp, buf, buflen, result); +} +*/ +import "C" + +func current() (*User, error) { + return lookupUnixUid(syscall.Getuid()) +} + +func lookupUser(username string) (*User, error) { + var pwd C.struct_passwd + var result *C.struct_passwd + nameC := make([]byte, len(username)+1) + copy(nameC, username) + + buf := alloc(userBuffer) + defer buf.free() + + err := retryWithBuffer(buf, func() syscall.Errno { + // mygetpwnam_r is a wrapper around getpwnam_r to avoid + // passing a size_t to getpwnam_r, because for unknown + // reasons passing a size_t to getpwnam_r doesn't work on + // Solaris. + return syscall.Errno(C.mygetpwnam_r((*C.char)(unsafe.Pointer(&nameC[0])), + &pwd, + (*C.char)(buf.ptr), + C.size_t(buf.size), + &result)) + }) + if err != nil { + return nil, fmt.Errorf("user: lookup username %s: %v", username, err) + } + if result == nil { + return nil, UnknownUserError(username) + } + return buildUser(&pwd), err +} + +func lookupUserId(uid string) (*User, error) { + i, e := strconv.Atoi(uid) + if e != nil { + return nil, e + } + return lookupUnixUid(i) +} + +func lookupUnixUid(uid int) (*User, error) { + var pwd C.struct_passwd + var result *C.struct_passwd + + buf := alloc(userBuffer) + defer buf.free() + + err := retryWithBuffer(buf, func() syscall.Errno { + // mygetpwuid_r is a wrapper around getpwuid_r to avoid using uid_t + // because C.uid_t(uid) for unknown reasons doesn't work on linux. + return syscall.Errno(C.mygetpwuid_r(C.int(uid), + &pwd, + (*C.char)(buf.ptr), + C.size_t(buf.size), + &result)) + }) + if err != nil { + return nil, fmt.Errorf("user: lookup userid %d: %v", uid, err) + } + if result == nil { + return nil, UnknownUserIdError(uid) + } + return buildUser(&pwd), nil +} + +func buildUser(pwd *C.struct_passwd) *User { + u := &User{ + Uid: strconv.FormatUint(uint64(pwd.pw_uid), 10), + Gid: strconv.FormatUint(uint64(pwd.pw_gid), 10), + Username: C.GoString(pwd.pw_name), + Name: C.GoString(pwd.pw_gecos), + HomeDir: C.GoString(pwd.pw_dir), + } + // 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 +} + +func lookupGroup(groupname string) (*Group, error) { + var grp C.struct_group + var result *C.struct_group + + buf := alloc(groupBuffer) + defer buf.free() + cname := make([]byte, len(groupname)+1) + copy(cname, groupname) + + err := retryWithBuffer(buf, func() syscall.Errno { + return syscall.Errno(C.mygetgrnam_r((*C.char)(unsafe.Pointer(&cname[0])), + &grp, + (*C.char)(buf.ptr), + C.size_t(buf.size), + &result)) + }) + if err != nil { + return nil, fmt.Errorf("user: lookup groupname %s: %v", groupname, err) + } + if result == nil { + return nil, UnknownGroupError(groupname) + } + return buildGroup(&grp), nil +} + +func lookupGroupId(gid string) (*Group, error) { + i, e := strconv.Atoi(gid) + if e != nil { + return nil, e + } + return lookupUnixGid(i) +} + +func lookupUnixGid(gid int) (*Group, error) { + var grp C.struct_group + var result *C.struct_group + + buf := alloc(groupBuffer) + defer buf.free() + + err := retryWithBuffer(buf, func() syscall.Errno { + // mygetgrgid_r is a wrapper around getgrgid_r to avoid using gid_t + // because C.gid_t(gid) for unknown reasons doesn't work on linux. + return syscall.Errno(C.mygetgrgid_r(C.int(gid), + &grp, + (*C.char)(buf.ptr), + C.size_t(buf.size), + &result)) + }) + if err != nil { + return nil, fmt.Errorf("user: lookup groupid %d: %v", gid, err) + } + if result == nil { + return nil, UnknownGroupIdError(strconv.Itoa(gid)) + } + return buildGroup(&grp), nil +} + +func buildGroup(grp *C.struct_group) *Group { + g := &Group{ + Gid: strconv.Itoa(int(grp.gr_gid)), + Name: C.GoString(grp.gr_name), + } + return g +} + +type bufferKind C.int + +const ( + userBuffer = bufferKind(C._SC_GETPW_R_SIZE_MAX) + groupBuffer = bufferKind(C._SC_GETGR_R_SIZE_MAX) +) + +func (k bufferKind) initialSize() C.size_t { + sz := C.sysconf(C.int(k)) + if sz == -1 { + // DragonFly and FreeBSD do not have _SC_GETPW_R_SIZE_MAX. + // Additionally, not all Linux systems have it, either. For + // example, the musl libc returns -1. + return 1024 + } + if !isSizeReasonable(int64(sz)) { + // Truncate. If this truly isn't enough, retryWithBuffer will error on the first run. + return maxBufferSize + } + return C.size_t(sz) +} + +type memBuffer struct { + ptr unsafe.Pointer + size C.size_t +} + +func alloc(kind bufferKind) *memBuffer { + sz := kind.initialSize() + return &memBuffer{ + ptr: C.malloc(sz), + size: sz, + } +} + +func (mb *memBuffer) resize(newSize C.size_t) { + mb.ptr = C.realloc(mb.ptr, newSize) + mb.size = newSize +} + +func (mb *memBuffer) free() { + C.free(mb.ptr) +} + +// retryWithBuffer repeatedly calls f(), increasing the size of the +// buffer each time, until f succeeds, fails with a non-ERANGE error, +// or the buffer exceeds a reasonable limit. +func retryWithBuffer(buf *memBuffer, f func() syscall.Errno) error { + for { + errno := f() + if errno == 0 { + return nil + } else if errno != syscall.ERANGE { + return errno + } + newSize := buf.size * 2 + if !isSizeReasonable(int64(newSize)) { + return fmt.Errorf("internal buffer exceeds %d bytes", maxBufferSize) + } + buf.resize(newSize) + } +} + +const maxBufferSize = 1 << 20 + +func isSizeReasonable(sz int64) bool { + return sz > 0 && sz <= maxBufferSize +} + +// Because we can't use cgo in tests: +func structPasswdForNegativeTest() C.struct_passwd { + sp := C.struct_passwd{} + sp.pw_uid = 1<<32 - 2 + sp.pw_gid = 1<<32 - 3 + return sp +} diff --git a/src/os/user/cgo_unix_test.go b/src/os/user/cgo_unix_test.go new file mode 100644 index 0000000..1d341aa --- /dev/null +++ b/src/os/user/cgo_unix_test.go @@ -0,0 +1,24 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris +// +build cgo,!osusergo + +package user + +import ( + "testing" +) + +// Issue 22739 +func TestNegativeUid(t *testing.T) { + sp := structPasswdForNegativeTest() + u := buildUser(&sp) + if g, w := u.Uid, "4294967294"; g != w { + t.Errorf("Uid = %q; want %q", g, w) + } + if g, w := u.Gid, "4294967293"; g != w { + t.Errorf("Gid = %q; want %q", g, w) + } +} diff --git a/src/os/user/getgrouplist_darwin.go b/src/os/user/getgrouplist_darwin.go new file mode 100644 index 0000000..e8fe26c --- /dev/null +++ b/src/os/user/getgrouplist_darwin.go @@ -0,0 +1,54 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build cgo,!osusergo + +package user + +/* +#include <unistd.h> +#include <sys/types.h> +#include <stdlib.h> + +static int mygetgrouplist(const char* user, gid_t group, gid_t* groups, int* ngroups) { + int* buf = malloc(*ngroups * sizeof(int)); + int rv = getgrouplist(user, (int) group, buf, ngroups); + int i; + if (rv == 0) { + for (i = 0; i < *ngroups; i++) { + groups[i] = (gid_t) buf[i]; + } + } + free(buf); + return rv; +} +*/ +import "C" +import ( + "fmt" + "unsafe" +) + +func getGroupList(name *C.char, userGID C.gid_t, gids *C.gid_t, n *C.int) C.int { + return C.mygetgrouplist(name, userGID, gids, n) +} + +// groupRetry retries getGroupList with an increasingly large size for n. The +// result is stored in gids. +func groupRetry(username string, name []byte, userGID C.gid_t, gids *[]C.gid_t, n *C.int) error { + *n = C.int(256 * 2) + for { + *gids = make([]C.gid_t, *n) + rv := getGroupList((*C.char)(unsafe.Pointer(&name[0])), userGID, &(*gids)[0], n) + if rv >= 0 { + // n is set correctly + break + } + if *n > maxGroups { + return fmt.Errorf("user: %q is a member of more than %d groups", username, maxGroups) + } + *n = *n * C.int(2) + } + return nil +} diff --git a/src/os/user/getgrouplist_unix.go b/src/os/user/getgrouplist_unix.go new file mode 100644 index 0000000..9685414 --- /dev/null +++ b/src/os/user/getgrouplist_unix.go @@ -0,0 +1,42 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build dragonfly freebsd !android,linux netbsd openbsd +// +build cgo,!osusergo + +package user + +/* +#include <unistd.h> +#include <sys/types.h> +#include <grp.h> + +static int mygetgrouplist(const char* user, gid_t group, gid_t* groups, int* ngroups) { + return getgrouplist(user, group, groups, ngroups); +} +*/ +import "C" +import ( + "fmt" + "unsafe" +) + +func getGroupList(name *C.char, userGID C.gid_t, gids *C.gid_t, n *C.int) C.int { + return C.mygetgrouplist(name, userGID, gids, n) +} + +// groupRetry retries getGroupList with much larger size for n. The result is +// stored in gids. +func groupRetry(username string, name []byte, userGID C.gid_t, gids *[]C.gid_t, n *C.int) error { + // More than initial buffer, but now n contains the correct size. + if *n > maxGroups { + return fmt.Errorf("user: %q is a member of more than %d groups", username, maxGroups) + } + *gids = make([]C.gid_t, *n) + rv := getGroupList((*C.char)(unsafe.Pointer(&name[0])), userGID, &(*gids)[0], n) + if rv == -1 { + return fmt.Errorf("user: list groups for %s failed", username) + } + return nil +} diff --git a/src/os/user/listgroups_aix.go b/src/os/user/listgroups_aix.go new file mode 100644 index 0000000..17de3e9 --- /dev/null +++ b/src/os/user/listgroups_aix.go @@ -0,0 +1,13 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build cgo,!osusergo + +package user + +import "fmt" + +func listGroups(u *User) ([]string, error) { + return nil, fmt.Errorf("user: list groups for %s: not supported on AIX", u.Username) +} diff --git a/src/os/user/listgroups_solaris.go b/src/os/user/listgroups_solaris.go new file mode 100644 index 0000000..f3cbf6c --- /dev/null +++ b/src/os/user/listgroups_solaris.go @@ -0,0 +1,17 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build cgo,!osusergo + +// Even though this file requires no C, it is used to provide a +// listGroup stub because all the other Solaris calls work. Otherwise, +// this stub will conflict with the lookup_stubs.go fallback. + +package user + +import "fmt" + +func listGroups(u *User) ([]string, error) { + return nil, fmt.Errorf("user: list groups for %s: not supported on Solaris", u.Username) +} diff --git a/src/os/user/listgroups_unix.go b/src/os/user/listgroups_unix.go new file mode 100644 index 0000000..70f7af7 --- /dev/null +++ b/src/os/user/listgroups_unix.go @@ -0,0 +1,49 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build dragonfly darwin freebsd !android,linux netbsd openbsd +// +build cgo,!osusergo + +package user + +import ( + "fmt" + "strconv" + "unsafe" +) + +/* +#include <unistd.h> +#include <sys/types.h> +*/ +import "C" + +const maxGroups = 2048 + +func listGroups(u *User) ([]string, error) { + ug, err := strconv.Atoi(u.Gid) + if err != nil { + return nil, fmt.Errorf("user: list groups for %s: invalid gid %q", u.Username, u.Gid) + } + userGID := C.gid_t(ug) + nameC := make([]byte, len(u.Username)+1) + copy(nameC, u.Username) + + n := C.int(256) + gidsC := make([]C.gid_t, n) + rv := getGroupList((*C.char)(unsafe.Pointer(&nameC[0])), userGID, &gidsC[0], &n) + if rv == -1 { + // Mac is the only Unix that does not set n properly when rv == -1, so + // we need to use different logic for Mac vs. the other OS's. + if err := groupRetry(u.Username, nameC, userGID, &gidsC, &n); err != nil { + return nil, err + } + } + gidsC = gidsC[:n] + gids := make([]string, 0, n) + for _, g := range gidsC[:n] { + gids = append(gids, strconv.Itoa(int(g))) + } + return gids, nil +} diff --git a/src/os/user/lookup.go b/src/os/user/lookup.go new file mode 100644 index 0000000..b36b7c0 --- /dev/null +++ b/src/os/user/lookup.go @@ -0,0 +1,63 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package user + +import "sync" + +// Current returns the current user. +// +// The first call will cache the current user information. +// Subsequent calls will return the cached value and will not reflect +// changes to the current user. +func Current() (*User, error) { + cache.Do(func() { cache.u, cache.err = current() }) + if cache.err != nil { + return nil, cache.err + } + u := *cache.u // copy + return &u, nil +} + +// cache of the current user +var cache struct { + sync.Once + u *User + err error +} + +// Lookup looks up a user by username. If the user cannot be found, the +// returned error is of type UnknownUserError. +func Lookup(username string) (*User, error) { + if u, err := Current(); err == nil && u.Username == username { + return u, err + } + return lookupUser(username) +} + +// LookupId looks up a user by userid. If the user cannot be found, the +// returned error is of type UnknownUserIdError. +func LookupId(uid string) (*User, error) { + if u, err := Current(); err == nil && u.Uid == uid { + return u, err + } + return lookupUserId(uid) +} + +// LookupGroup looks up a group by name. If the group cannot be found, the +// returned error is of type UnknownGroupError. +func LookupGroup(name string) (*Group, error) { + return lookupGroup(name) +} + +// LookupGroupId looks up a group by groupid. If the group cannot be found, the +// returned error is of type UnknownGroupIdError. +func LookupGroupId(gid string) (*Group, error) { + return lookupGroupId(gid) +} + +// GroupIds returns the list of group IDs that the user is a member of. +func (u *User) GroupIds() ([]string, error) { + return listGroups(u) +} diff --git a/src/os/user/lookup_android.go b/src/os/user/lookup_android.go new file mode 100644 index 0000000..8ca30b8 --- /dev/null +++ b/src/os/user/lookup_android.go @@ -0,0 +1,25 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build android + +package user + +import "errors" + +func lookupUser(string) (*User, error) { + return nil, errors.New("user: Lookup not implemented on android") +} + +func lookupUserId(string) (*User, error) { + return nil, errors.New("user: LookupId not implemented on android") +} + +func lookupGroup(string) (*Group, error) { + return nil, errors.New("user: LookupGroup not implemented on android") +} + +func lookupGroupId(string) (*Group, error) { + return nil, errors.New("user: LookupGroupId not implemented on android") +} diff --git a/src/os/user/lookup_plan9.go b/src/os/user/lookup_plan9.go new file mode 100644 index 0000000..33ae3a6 --- /dev/null +++ b/src/os/user/lookup_plan9.go @@ -0,0 +1,61 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package user + +import ( + "fmt" + "os" + "syscall" +) + +// Partial os/user support on Plan 9. +// Supports Current(), but not Lookup()/LookupId(). +// The latter two would require parsing /adm/users. +const ( + userFile = "/dev/user" +) + +func init() { + groupImplemented = false +} + +func current() (*User, error) { + ubytes, err := os.ReadFile(userFile) + if err != nil { + return nil, fmt.Errorf("user: %s", err) + } + + uname := string(ubytes) + + u := &User{ + Uid: uname, + Gid: uname, + Username: uname, + Name: uname, + HomeDir: os.Getenv("home"), + } + + return u, nil +} + +func lookupUser(username string) (*User, error) { + return nil, syscall.EPLAN9 +} + +func lookupUserId(uid string) (*User, error) { + return nil, syscall.EPLAN9 +} + +func lookupGroup(groupname string) (*Group, error) { + return nil, syscall.EPLAN9 +} + +func lookupGroupId(string) (*Group, error) { + return nil, syscall.EPLAN9 +} + +func listGroups(*User) ([]string, error) { + return nil, syscall.EPLAN9 +} diff --git a/src/os/user/lookup_stubs.go b/src/os/user/lookup_stubs.go new file mode 100644 index 0000000..178d814 --- /dev/null +++ b/src/os/user/lookup_stubs.go @@ -0,0 +1,88 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !cgo,!windows,!plan9 android osusergo,!windows,!plan9 + +package user + +import ( + "errors" + "fmt" + "os" + "runtime" + "strconv" +) + +func init() { + groupImplemented = false +} + +func current() (*User, error) { + uid := currentUID() + // $USER and /etc/passwd may disagree; prefer the latter if we can get it. + // See issue 27524 for more information. + u, err := lookupUserId(uid) + if err == nil { + return u, nil + } + + homeDir, _ := os.UserHomeDir() + u = &User{ + Uid: uid, + Gid: currentGID(), + Username: os.Getenv("USER"), + Name: "", // ignored + HomeDir: homeDir, + } + // On Android, return a dummy user instead of failing. + switch runtime.GOOS { + case "android": + if u.Uid == "" { + u.Uid = "1" + } + if u.Username == "" { + u.Username = "android" + } + } + // cgo isn't available, but if we found the minimum information + // without it, use it: + if u.Uid != "" && u.Username != "" && u.HomeDir != "" { + return u, nil + } + var missing string + if u.Username == "" { + missing = "$USER" + } + if u.HomeDir == "" { + if missing != "" { + missing += ", " + } + missing += "$HOME" + } + return u, fmt.Errorf("user: Current requires cgo or %s set in environment", missing) +} + +func listGroups(*User) ([]string, error) { + if runtime.GOOS == "android" || runtime.GOOS == "aix" { + return nil, fmt.Errorf("user: GroupIds not implemented on %s", runtime.GOOS) + } + return nil, errors.New("user: GroupIds requires cgo") +} + +func currentUID() string { + if id := os.Getuid(); id >= 0 { + return strconv.Itoa(id) + } + // Note: Windows returns -1, but this file isn't used on + // Windows anyway, so this empty return path shouldn't be + // used. + return "" +} + +func currentGID() string { + if id := os.Getgid(); id >= 0 { + return strconv.Itoa(id) + } + return "" +} diff --git a/src/os/user/lookup_unix.go b/src/os/user/lookup_unix.go new file mode 100644 index 0000000..0890cd8 --- /dev/null +++ b/src/os/user/lookup_unix.go @@ -0,0 +1,197 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build aix darwin dragonfly freebsd js,wasm !android,linux netbsd openbsd solaris +// +build !cgo osusergo + +package user + +import ( + "bufio" + "bytes" + "errors" + "io" + "os" + "strconv" + "strings" +) + +const groupFile = "/etc/group" +const userFile = "/etc/passwd" + +var colon = []byte{':'} + +func init() { + groupImplemented = false +} + +// 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) + for bs.Scan() { + line := bs.Bytes() + // 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 + } + v, err = fn(line) + if v != nil || err != nil { + return + } + } + 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 + } else if v != nil { + return v.(*Group), nil + } + return nil, UnknownGroupIdError(id) +} + +func findGroupName(name string, r io.Reader) (*Group, error) { + if v, err := readColonFile(r, matchGroupIndexValue(name, 0)); err != nil { + return nil, err + } else if v != nil { + return v.(*Group), nil + } + 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) { + f, err := os.Open(groupFile) + if err != nil { + return nil, err + } + defer f.Close() + return findGroupName(groupname, f) +} + +func lookupGroupId(id string) (*Group, error) { + f, err := os.Open(groupFile) + if err != nil { + return nil, err + } + defer f.Close() + return findGroupId(id, f) +} + +func lookupUser(username string) (*User, error) { + f, err := os.Open(userFile) + if err != nil { + return nil, err + } + 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) +} diff --git a/src/os/user/lookup_unix_test.go b/src/os/user/lookup_unix_test.go new file mode 100644 index 0000000..72d3b47 --- /dev/null +++ b/src/os/user/lookup_unix_test.go @@ -0,0 +1,276 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris +// +build !cgo + +package user + +import ( + "reflect" + "strings" + "testing" +) + +const testGroupFile = `# See the opendirectoryd(8) man page for additional +# information about Open Directory. +## +nobody:*:-2: +nogroup:*:-1: +wheel:*:0:root +emptyid:*::root +invalidgid:*:notanumber:root ++plussign:*:20:root +-minussign:*:21:root + +daemon:*:1:root + indented:*:7: +# comment:*:4:found + # comment:*:4:found +kmem:*:2:root +` + +var groupTests = []struct { + in string + name string + gid string +}{ + {testGroupFile, "nobody", "-2"}, + {testGroupFile, "kmem", "2"}, + {testGroupFile, "notinthefile", ""}, + {testGroupFile, "comment", ""}, + {testGroupFile, "plussign", ""}, + {testGroupFile, "+plussign", ""}, + {testGroupFile, "-minussign", ""}, + {testGroupFile, "minussign", ""}, + {testGroupFile, "emptyid", ""}, + {testGroupFile, "invalidgid", ""}, + {testGroupFile, "indented", "7"}, + {testGroupFile, "# comment", ""}, + {"", "emptyfile", ""}, +} + +func TestFindGroupName(t *testing.T) { + for _, tt := range groupTests { + got, err := findGroupName(tt.name, strings.NewReader(tt.in)) + if tt.gid == "" { + if err == nil { + t.Errorf("findGroupName(%s): got nil error, expected err", tt.name) + continue + } + switch terr := err.(type) { + case UnknownGroupError: + if terr.Error() != "group: unknown group "+tt.name { + t.Errorf("findGroupName(%s): got %v, want %v", tt.name, terr, tt.name) + } + default: + t.Errorf("findGroupName(%s): got unexpected error %v", tt.name, terr) + } + } else { + if err != nil { + t.Fatalf("findGroupName(%s): got unexpected error %v", tt.name, err) + } + if got.Gid != tt.gid { + t.Errorf("findGroupName(%s): got gid %v, want %s", tt.name, got.Gid, tt.gid) + } + if got.Name != tt.name { + t.Errorf("findGroupName(%s): got name %s, want %s", tt.name, got.Name, tt.name) + } + } + } +} + +var groupIdTests = []struct { + in string + gid string + name string +}{ + {testGroupFile, "-2", "nobody"}, + {testGroupFile, "2", "kmem"}, + {testGroupFile, "notinthefile", ""}, + {testGroupFile, "comment", ""}, + {testGroupFile, "7", "indented"}, + {testGroupFile, "4", ""}, + {testGroupFile, "20", ""}, // row starts with a plus + {testGroupFile, "21", ""}, // row starts with a minus + {"", "emptyfile", ""}, +} + +func TestFindGroupId(t *testing.T) { + for _, tt := range groupIdTests { + got, err := findGroupId(tt.gid, strings.NewReader(tt.in)) + if tt.name == "" { + if err == nil { + t.Errorf("findGroupId(%s): got nil error, expected err", tt.gid) + continue + } + switch terr := err.(type) { + case UnknownGroupIdError: + if terr.Error() != "group: unknown groupid "+tt.gid { + t.Errorf("findGroupId(%s): got %v, want %v", tt.name, terr, tt.name) + } + default: + t.Errorf("findGroupId(%s): got unexpected error %v", tt.name, terr) + } + } else { + if err != nil { + t.Fatalf("findGroupId(%s): got unexpected error %v", tt.name, err) + } + if got.Gid != tt.gid { + t.Errorf("findGroupId(%s): got gid %v, want %s", tt.name, got.Gid, tt.gid) + } + if got.Name != tt.name { + t.Errorf("findGroupId(%s): got name %s, want %s", tt.name, got.Name, tt.name) + } + } + } +} + +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) + } + } + } +} diff --git a/src/os/user/lookup_windows.go b/src/os/user/lookup_windows.go new file mode 100644 index 0000000..f65773c --- /dev/null +++ b/src/os/user/lookup_windows.go @@ -0,0 +1,385 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package user + +import ( + "fmt" + "internal/syscall/windows" + "internal/syscall/windows/registry" + "syscall" + "unsafe" +) + +func isDomainJoined() (bool, error) { + var domain *uint16 + var status uint32 + err := syscall.NetGetJoinInformation(nil, &domain, &status) + if err != nil { + return false, err + } + syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain))) + return status == syscall.NetSetupDomainName, nil +} + +func lookupFullNameDomain(domainAndUser string) (string, error) { + return syscall.TranslateAccountName(domainAndUser, + syscall.NameSamCompatible, syscall.NameDisplay, 50) +} + +func lookupFullNameServer(servername, username string) (string, error) { + s, e := syscall.UTF16PtrFromString(servername) + if e != nil { + return "", e + } + u, e := syscall.UTF16PtrFromString(username) + if e != nil { + return "", e + } + var p *byte + e = syscall.NetUserGetInfo(s, u, 10, &p) + if e != nil { + return "", e + } + defer syscall.NetApiBufferFree(p) + i := (*syscall.UserInfo10)(unsafe.Pointer(p)) + return windows.UTF16PtrToString(i.FullName), nil +} + +func lookupFullName(domain, username, domainAndUser string) (string, error) { + joined, err := isDomainJoined() + if err == nil && joined { + name, err := lookupFullNameDomain(domainAndUser) + if err == nil { + return name, nil + } + } + name, err := lookupFullNameServer(domain, username) + if err == nil { + return name, nil + } + // domain worked neither as a domain nor as a server + // could be domain server unavailable + // pretend username is fullname + return username, nil +} + +// getProfilesDirectory retrieves the path to the root directory +// where user profiles are stored. +func getProfilesDirectory() (string, error) { + n := uint32(100) + for { + b := make([]uint16, n) + e := windows.GetProfilesDirectory(&b[0], &n) + if e == nil { + return syscall.UTF16ToString(b), nil + } + if e != syscall.ERROR_INSUFFICIENT_BUFFER { + return "", e + } + if n <= uint32(len(b)) { + return "", e + } + } +} + +// lookupUsernameAndDomain obtains the username and domain for usid. +func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, e error) { + username, domain, t, e := usid.LookupAccount("") + if e != nil { + return "", "", e + } + if t != syscall.SidTypeUser { + return "", "", fmt.Errorf("user: should be user account type, not %d", t) + } + return username, domain, nil +} + +// findHomeDirInRegistry finds the user home path based on the uid. +func findHomeDirInRegistry(uid string) (dir string, e error) { + k, e := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE) + if e != nil { + return "", e + } + defer k.Close() + dir, _, e = k.GetStringValue("ProfileImagePath") + if e != nil { + return "", e + } + return dir, nil +} + +// lookupGroupName accepts the name of a group and retrieves the group SID. +func lookupGroupName(groupname string) (string, error) { + sid, _, t, e := syscall.LookupSID("", groupname) + if e != nil { + return "", e + } + // https://msdn.microsoft.com/en-us/library/cc245478.aspx#gt_0387e636-5654-4910-9519-1f8326cf5ec0 + // SidTypeAlias should also be treated as a group type next to SidTypeGroup + // and SidTypeWellKnownGroup: + // "alias object -> resource group: A group object..." + // + // Tests show that "Administrators" can be considered of type SidTypeAlias. + if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias { + return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t) + } + return sid.String() +} + +// listGroupsForUsernameAndDomain accepts username and domain and retrieves +// a SID list of the local groups where this user is a member. +func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) { + // Check if both the domain name and user should be used. + var query string + joined, err := isDomainJoined() + if err == nil && joined && len(domain) != 0 { + query = domain + `\` + username + } else { + query = username + } + q, err := syscall.UTF16PtrFromString(query) + if err != nil { + return nil, err + } + var p0 *byte + var entriesRead, totalEntries uint32 + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa370655(v=vs.85).aspx + // NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0 + // elements which hold the names of local groups where the user participates. + // The list does not follow any sorting order. + // + // If no groups can be found for this user, NetUserGetLocalGroups() should + // always return the SID of a single group called "None", which + // also happens to be the primary group for the local user. + err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries) + if err != nil { + return nil, err + } + defer syscall.NetApiBufferFree(p0) + if entriesRead == 0 { + return nil, fmt.Errorf("listGroupsForUsernameAndDomain: NetUserGetLocalGroups() returned an empty list for domain: %s, username: %s", domain, username) + } + entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead] + var sids []string + for _, entry := range entries { + if entry.Name == nil { + continue + } + sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name)) + if err != nil { + return nil, err + } + sids = append(sids, sid) + } + return sids, nil +} + +func newUser(uid, gid, dir, username, domain string) (*User, error) { + domainAndUser := domain + `\` + username + name, e := lookupFullName(domain, username, domainAndUser) + if e != nil { + return nil, e + } + u := &User{ + Uid: uid, + Gid: gid, + Username: domainAndUser, + Name: name, + HomeDir: dir, + } + return u, nil +} + +func current() (*User, error) { + t, e := syscall.OpenCurrentProcessToken() + if e != nil { + return nil, e + } + defer t.Close() + u, e := t.GetTokenUser() + if e != nil { + return nil, e + } + pg, e := t.GetTokenPrimaryGroup() + if e != nil { + return nil, e + } + uid, e := u.User.Sid.String() + if e != nil { + return nil, e + } + gid, e := pg.PrimaryGroup.String() + if e != nil { + return nil, e + } + dir, e := t.GetUserProfileDirectory() + if e != nil { + return nil, e + } + username, domain, e := lookupUsernameAndDomain(u.User.Sid) + if e != nil { + return nil, e + } + return newUser(uid, gid, dir, username, domain) +} + +// lookupUserPrimaryGroup obtains the primary group SID for a user using this method: +// https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for +// The method follows this formula: domainRID + "-" + primaryGroupRID +func lookupUserPrimaryGroup(username, domain string) (string, error) { + // get the domain RID + sid, _, t, e := syscall.LookupSID("", domain) + if e != nil { + return "", e + } + if t != syscall.SidTypeDomain { + return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t) + } + domainRID, e := sid.String() + if e != nil { + return "", e + } + // If the user has joined a domain use the RID of the default primary group + // called "Domain Users": + // https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems + // SID: S-1-5-21domain-513 + // + // The correct way to obtain the primary group of a domain user is + // probing the user primaryGroupID attribute in the server Active Directory: + // https://msdn.microsoft.com/en-us/library/ms679375(v=vs.85).aspx + // + // Note that the primary group of domain users should not be modified + // on Windows for performance reasons, even if it's possible to do that. + // The .NET Developer's Guide to Directory Services Programming - Page 409 + // https://books.google.bg/books?id=kGApqjobEfsC&lpg=PA410&ots=p7oo-eOQL7&dq=primary%20group%20RID&hl=bg&pg=PA409#v=onepage&q&f=false + joined, err := isDomainJoined() + if err == nil && joined { + return domainRID + "-513", nil + } + // For non-domain users call NetUserGetInfo() with level 4, which + // in this case would not have any network overhead. + // The primary group should not change from RID 513 here either + // but the group will be called "None" instead: + // https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/ + // "Group 'None' (RID: 513)" + u, e := syscall.UTF16PtrFromString(username) + if e != nil { + return "", e + } + d, e := syscall.UTF16PtrFromString(domain) + if e != nil { + return "", e + } + var p *byte + e = syscall.NetUserGetInfo(d, u, 4, &p) + if e != nil { + return "", e + } + defer syscall.NetApiBufferFree(p) + i := (*windows.UserInfo4)(unsafe.Pointer(p)) + return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil +} + +func newUserFromSid(usid *syscall.SID) (*User, error) { + username, domain, e := lookupUsernameAndDomain(usid) + if e != nil { + return nil, e + } + gid, e := lookupUserPrimaryGroup(username, domain) + if e != nil { + return nil, e + } + uid, e := usid.String() + if e != nil { + return nil, e + } + // If this user has logged in at least once their home path should be stored + // in the registry under the specified SID. References: + // https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx + // https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles + // + // The registry is the most reliable way to find the home path as the user + // might have decided to move it outside of the default location, + // (e.g. C:\users). Reference: + // https://answers.microsoft.com/en-us/windows/forum/windows_7-security/how-do-i-set-a-home-directory-outside-cusers-for-a/aed68262-1bf4-4a4d-93dc-7495193a440f + dir, e := findHomeDirInRegistry(uid) + if e != nil { + // If the home path does not exist in the registry, the user might + // have not logged in yet; fall back to using getProfilesDirectory(). + // Find the username based on a SID and append that to the result of + // getProfilesDirectory(). The domain is not relevant here. + dir, e = getProfilesDirectory() + if e != nil { + return nil, e + } + dir += `\` + username + } + return newUser(uid, gid, dir, username, domain) +} + +func lookupUser(username string) (*User, error) { + sid, _, t, e := syscall.LookupSID("", username) + if e != nil { + return nil, e + } + if t != syscall.SidTypeUser { + return nil, fmt.Errorf("user: should be user account type, not %d", t) + } + return newUserFromSid(sid) +} + +func lookupUserId(uid string) (*User, error) { + sid, e := syscall.StringToSid(uid) + if e != nil { + return nil, e + } + return newUserFromSid(sid) +} + +func lookupGroup(groupname string) (*Group, error) { + sid, err := lookupGroupName(groupname) + if err != nil { + return nil, err + } + return &Group{Name: groupname, Gid: sid}, nil +} + +func lookupGroupId(gid string) (*Group, error) { + sid, err := syscall.StringToSid(gid) + if err != nil { + return nil, err + } + groupname, _, t, err := sid.LookupAccount("") + if err != nil { + return nil, err + } + if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias { + return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t) + } + return &Group{Name: groupname, Gid: gid}, nil +} + +func listGroups(user *User) ([]string, error) { + sid, err := syscall.StringToSid(user.Uid) + if err != nil { + return nil, err + } + username, domain, err := lookupUsernameAndDomain(sid) + if err != nil { + return nil, err + } + sids, err := listGroupsForUsernameAndDomain(username, domain) + if err != nil { + return nil, err + } + // Add the primary group of the user to the list if it is not already there. + // This is done only to comply with the POSIX concept of a primary group. + for _, sid := range sids { + if sid == user.Gid { + return sids, nil + } + } + return append(sids, user.Gid), nil +} diff --git a/src/os/user/user.go b/src/os/user/user.go new file mode 100644 index 0000000..c1b8101 --- /dev/null +++ b/src/os/user/user.go @@ -0,0 +1,90 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package user allows user account lookups by name or id. + +For most Unix systems, this package has two internal implementations of +resolving user and group ids to names. One is written in pure Go and +parses /etc/passwd and /etc/group. The other is cgo-based and relies on +the standard C library (libc) routines such as getpwuid_r and getgrnam_r. + +When cgo is available, cgo-based (libc-backed) code is used by default. +This can be overridden by using osusergo build tag, which enforces +the pure Go implementation. +*/ +package user + +import ( + "strconv" +) + +var ( + userImplemented = true // set to false by lookup_stubs.go's init + groupImplemented = true // set to false by lookup_stubs.go's init +) + +// User represents a user account. +type User struct { + // Uid is the user ID. + // On POSIX systems, this is a decimal number representing the uid. + // On Windows, this is a security identifier (SID) in a string format. + // On Plan 9, this is the contents of /dev/user. + Uid string + // Gid is the primary group ID. + // On POSIX systems, this is a decimal number representing the gid. + // On Windows, this is a SID in a string format. + // On Plan 9, this is the contents of /dev/user. + Gid string + // Username is the login name. + Username string + // Name is the user's real or display name. + // It might be blank. + // On POSIX systems, this is the first (or only) entry in the GECOS field + // list. + // On Windows, this is the user's display name. + // On Plan 9, this is the contents of /dev/user. + Name string + // HomeDir is the path to the user's home directory (if they have one). + HomeDir string +} + +// Group represents a grouping of users. +// +// On POSIX systems Gid contains a decimal number representing the group ID. +type Group struct { + Gid string // group ID + Name string // group name +} + +// UnknownUserIdError is returned by LookupId when a user cannot be found. +type UnknownUserIdError int + +func (e UnknownUserIdError) Error() string { + return "user: unknown userid " + strconv.Itoa(int(e)) +} + +// UnknownUserError is returned by Lookup when +// a user cannot be found. +type UnknownUserError string + +func (e UnknownUserError) Error() string { + return "user: unknown user " + string(e) +} + +// UnknownGroupIdError is returned by LookupGroupId when +// a group cannot be found. +type UnknownGroupIdError string + +func (e UnknownGroupIdError) Error() string { + return "group: unknown groupid " + string(e) +} + +// UnknownGroupError is returned by LookupGroup when +// a group cannot be found. +type UnknownGroupError string + +func (e UnknownGroupError) Error() string { + return "group: unknown group " + string(e) +} diff --git a/src/os/user/user_test.go b/src/os/user/user_test.go new file mode 100644 index 0000000..8c4c817 --- /dev/null +++ b/src/os/user/user_test.go @@ -0,0 +1,158 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package user + +import ( + "runtime" + "testing" +) + +func checkUser(t *testing.T) { + t.Helper() + if !userImplemented { + t.Skip("user: not implemented; skipping tests") + } +} + +func TestCurrent(t *testing.T) { + u, err := Current() + if err != nil { + t.Fatalf("Current: %v (got %#v)", err, u) + } + if u.HomeDir == "" { + t.Errorf("didn't get a HomeDir") + } + if u.Username == "" { + t.Errorf("didn't get a username") + } +} + +func BenchmarkCurrent(b *testing.B) { + for i := 0; i < b.N; i++ { + Current() + } +} + +func compare(t *testing.T, want, got *User) { + if want.Uid != got.Uid { + t.Errorf("got Uid=%q; want %q", got.Uid, want.Uid) + } + if want.Username != got.Username { + t.Errorf("got Username=%q; want %q", got.Username, want.Username) + } + if want.Name != got.Name { + t.Errorf("got Name=%q; want %q", got.Name, want.Name) + } + if want.HomeDir != got.HomeDir { + t.Errorf("got HomeDir=%q; want %q", got.HomeDir, want.HomeDir) + } + if want.Gid != got.Gid { + t.Errorf("got Gid=%q; want %q", got.Gid, want.Gid) + } +} + +func TestLookup(t *testing.T) { + checkUser(t) + + if runtime.GOOS == "plan9" { + t.Skipf("Lookup not implemented on %q", runtime.GOOS) + } + + want, err := Current() + if err != nil { + 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) + if err != nil { + t.Fatalf("Lookup: %v", err) + } + compare(t, want, got) +} + +func TestLookupId(t *testing.T) { + checkUser(t) + + if runtime.GOOS == "plan9" { + t.Skipf("LookupId not implemented on %q", runtime.GOOS) + } + + want, err := Current() + if err != nil { + t.Fatalf("Current: %v", err) + } + got, err := LookupId(want.Uid) + if err != nil { + t.Fatalf("LookupId: %v", err) + } + compare(t, want, got) +} + +func checkGroup(t *testing.T) { + t.Helper() + if !groupImplemented { + t.Skip("user: group not implemented; skipping test") + } +} + +func TestLookupGroup(t *testing.T) { + checkGroup(t) + user, err := Current() + if err != nil { + t.Fatalf("Current(): %v", err) + } + + g1, err := LookupGroupId(user.Gid) + if err != nil { + // NOTE(rsc): Maybe the group isn't defined. That's fine. + // On my OS X laptop, rsc logs in with group 5000 even + // though there's no name for group 5000. Such is Unix. + t.Logf("LookupGroupId(%q): %v", user.Gid, err) + return + } + if g1.Gid != user.Gid { + t.Errorf("LookupGroupId(%q).Gid = %s; want %s", user.Gid, g1.Gid, user.Gid) + } + + g2, err := LookupGroup(g1.Name) + if err != nil { + t.Fatalf("LookupGroup(%q): %v", g1.Name, err) + } + if g1.Gid != g2.Gid || g1.Name != g2.Name { + t.Errorf("LookupGroup(%q) = %+v; want %+v", g1.Name, g2, g1) + } +} + +func TestGroupIds(t *testing.T) { + checkGroup(t) + if runtime.GOOS == "aix" { + t.Skip("skipping GroupIds, see golang.org/issue/30563") + } + if runtime.GOOS == "solaris" || runtime.GOOS == "illumos" { + t.Skip("skipping GroupIds, see golang.org/issue/14709") + } + user, err := Current() + if err != nil { + t.Fatalf("Current(): %v", err) + } + gids, err := user.GroupIds() + if err != nil { + t.Fatalf("%+v.GroupIds(): %v", user, err) + } + if !containsID(gids, user.Gid) { + t.Errorf("%+v.GroupIds() = %v; does not contain user GID %s", user, gids, user.Gid) + } +} + +func containsID(ids []string, id string) bool { + for _, x := range ids { + if x == id { + return true + } + } + return false +} |