os/user: implement go native GroupIds
Currently, GroupIds (a method that returns supplementary group IDs
for a user) is not implemented when cgo is not available, or osusergo
build tag is set, or the underlying OS lacks getgrouplist(3).
This adds a native Go implementation of GroupIds (which parses
/etc/group) for such cases, together with some tests.
This implementation is used:
- when cgo is not available;
- when osusergo build tag is set;
- on AIX (which lacks getgrouplist(3));
- on Illumos (which only recently added getgrouplist(3)).
This commit moves listgroups_unix.go to cgo_listgroups_unix.go, and adds
listgroups_unix.go which implements the feature.
NOTE the +build equivalent of go:build expression in listgroups_unix.go
is not provided as it is going to be bulky. Go 1.17 already prefers
go:build over +build, and no longer fail if a file contains go:build
without +build, so the absence of +build is not a problem even with Go
1.17, and this code is targeted for Go 1.18.
Updates #14709
Updates #30563
Change-Id: Icc95cda97ee3bcb03ef028b16eab7d3faba9ffab
Reviewed-on: https://go-review.googlesource.com/c/go/+/330753
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Tobias Klauser <tobias.klauser@gmail.com>
Run-TryBot: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
diff --git a/src/os/user/cgo_listgroups_unix.go b/src/os/user/cgo_listgroups_unix.go
new file mode 100644
index 0000000..38aa765
--- /dev/null
+++ b/src/os/user/cgo_listgroups_unix.go
@@ -0,0 +1,51 @@
+// 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.
+
+//go:build (dragonfly || darwin || freebsd || (!android && linux) || netbsd || openbsd || (solaris && !illumos)) && cgo && !osusergo
+// +build dragonfly darwin freebsd !android,linux netbsd openbsd solaris,!illumos
+// +build cgo
+// +build !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/listgroups_aix.go b/src/os/user/listgroups_aix.go
deleted file mode 100644
index fbc1deb..0000000
--- a/src/os/user/listgroups_aix.go
+++ /dev/null
@@ -1,20 +0,0 @@
-// 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.
-
-//go:build cgo && !osusergo
-// +build cgo,!osusergo
-
-package user
-
-import "fmt"
-
-// Not implemented on AIX, see golang.org/issue/30563.
-
-func init() {
- groupListImplemented = false
-}
-
-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_illumos.go b/src/os/user/listgroups_illumos.go
deleted file mode 100644
index e783b26..0000000
--- a/src/os/user/listgroups_illumos.go
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2021 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.
-
-//go:build cgo && !osusergo
-// +build cgo,!osusergo
-
-// Even though this file requires no C, it is used to provide a
-// listGroup stub because all the other illumos calls work. Otherwise,
-// this stub will conflict with the lookup_stubs.go fallback.
-
-package user
-
-import "fmt"
-
-// Not implemented on illumos, see golang.org/issue/14709.
-
-func init() {
- groupListImplemented = false
-}
-
-func listGroups(u *User) ([]string, error) {
- return nil, fmt.Errorf("user: list groups for %s: not supported on illumos", u.Username)
-}
diff --git a/src/os/user/listgroups_stub.go b/src/os/user/listgroups_stub.go
new file mode 100644
index 0000000..a066c6d
--- /dev/null
+++ b/src/os/user/listgroups_stub.go
@@ -0,0 +1,20 @@
+// Copyright 2021 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.
+
+//go:build android || (js && !wasm)
+// +build android js,!wasm
+
+package user
+
+import (
+ "errors"
+)
+
+func init() {
+ groupListImplemented = false
+}
+
+func listGroups(*User) ([]string, error) {
+ return nil, errors.New("user: list groups not implemented")
+}
diff --git a/src/os/user/listgroups_unix.go b/src/os/user/listgroups_unix.go
index 38aa765..fa2df49 100644
--- a/src/os/user/listgroups_unix.go
+++ b/src/os/user/listgroups_unix.go
@@ -1,51 +1,113 @@
-// Copyright 2016 The Go Authors. All rights reserved.
+// Copyright 2021 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.
-//go:build (dragonfly || darwin || freebsd || (!android && linux) || netbsd || openbsd || (solaris && !illumos)) && cgo && !osusergo
-// +build dragonfly darwin freebsd !android,linux netbsd openbsd solaris,!illumos
-// +build cgo
-// +build !osusergo
+//go:build ((darwin || dragonfly || freebsd || (js && wasm) || (!android && linux) || netbsd || openbsd || solaris) && (!cgo || osusergo)) || aix || illumos
package user
import (
+ "bufio"
+ "bytes"
+ "errors"
"fmt"
+ "io"
+ "os"
"strconv"
- "unsafe"
)
-/*
-#include <unistd.h>
-#include <sys/types.h>
-*/
-import "C"
+const groupFile = "/etc/group"
-const maxGroups = 2048
+var colon = []byte{':'}
-func listGroups(u *User) ([]string, error) {
- ug, err := strconv.Atoi(u.Gid)
+func listGroupsFromReader(u *User, r io.Reader) ([]string, error) {
+ if u.Username == "" {
+ return nil, errors.New("user: list groups: empty username")
+ }
+ primaryGid, 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
+ userCommas := []byte("," + u.Username + ",") // ,john,
+ userFirst := userCommas[1:] // john,
+ userLast := userCommas[:len(userCommas)-1] // ,john
+ userOnly := userCommas[1 : len(userCommas)-1] // john
+
+ // Add primary Gid first.
+ groups := []string{u.Gid}
+
+ rd := bufio.NewReader(r)
+ done := false
+ for !done {
+ line, err := rd.ReadBytes('\n')
+ if err != nil {
+ if err == io.EOF {
+ done = true
+ } else {
+ return groups, err
+ }
}
+
+ // Look for username in the list of users. If user is found,
+ // append the GID to the groups slice.
+
+ // 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] == '#' ||
+ // If you search for a gid in a row where the group
+ // name (the first field) starts with "+" or "-",
+ // glibc fails to find the record, and so should we.
+ line[0] == '+' || line[0] == '-' {
+ continue
+ }
+
+ // Format of /etc/group is
+ // groupname:password:GID:user_list
+ // for example
+ // wheel:x:10:john,paul,jack
+ // tcpdump:x:72:
+ listIdx := bytes.LastIndexByte(line, ':')
+ if listIdx == -1 || listIdx == len(line)-1 {
+ // No commas, or empty group list.
+ continue
+ }
+ if bytes.Count(line[:listIdx], colon) != 2 {
+ // Incorrect number of colons.
+ continue
+ }
+ list := line[listIdx+1:]
+ // Check the list for user without splitting or copying.
+ if !(bytes.Equal(list, userOnly) || bytes.HasPrefix(list, userFirst) || bytes.HasSuffix(list, userLast) || bytes.Contains(list, userCommas)) {
+ continue
+ }
+
+ // groupname:password:GID
+ parts := bytes.Split(line[:listIdx], colon)
+ if len(parts) != 3 || len(parts[0]) == 0 {
+ continue
+ }
+ gid := string(parts[2])
+ // Make sure it's numeric and not the same as primary GID.
+ numGid, err := strconv.Atoi(gid)
+ if err != nil || numGid == primaryGid {
+ continue
+ }
+
+ groups = append(groups, gid)
}
- gidsC = gidsC[:n]
- gids := make([]string, 0, n)
- for _, g := range gidsC[:n] {
- gids = append(gids, strconv.Itoa(int(g)))
+
+ return groups, nil
+}
+
+func listGroups(u *User) ([]string, error) {
+ f, err := os.Open(groupFile)
+ if err != nil {
+ return nil, err
}
- return gids, nil
+ defer f.Close()
+
+ return listGroupsFromReader(u, f)
}
diff --git a/src/os/user/listgroups_unix_test.go b/src/os/user/listgroups_unix_test.go
new file mode 100644
index 0000000..a9f79ec
--- /dev/null
+++ b/src/os/user/listgroups_unix_test.go
@@ -0,0 +1,107 @@
+// Copyright 2021 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.
+
+//go:build ((darwin || dragonfly || freebsd || (js && wasm) || (!android && linux) || netbsd || openbsd || solaris) && (!cgo || osusergo)) || aix || illumos
+
+package user
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+ "testing"
+)
+
+var 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
+# Next line is invalid (empty group name)
+:*:22:root
+
+daemon:*:1:root
+ indented:*:7:root
+# comment:*:4:found
+ # comment:*:4:found
+kmem:*:2:root
+manymembers:x:777:jill,jody,john,jack,jov,user777
+` + largeGroup()
+
+func largeGroup() (res string) {
+ var b strings.Builder
+ b.WriteString("largegroup:x:1000:user1")
+ for i := 2; i <= 7500; i++ {
+ fmt.Fprintf(&b, ",user%d", i)
+ }
+ return b.String()
+}
+
+var listGroupsTests = []struct {
+ // input
+ in string
+ user string
+ gid string
+ // output
+ gids []string
+ err bool
+}{
+ {in: testGroupFile, user: "root", gid: "0", gids: []string{"0", "1", "2", "7"}},
+ {in: testGroupFile, user: "jill", gid: "33", gids: []string{"33", "777"}},
+ {in: testGroupFile, user: "jody", gid: "34", gids: []string{"34", "777"}},
+ {in: testGroupFile, user: "john", gid: "35", gids: []string{"35", "777"}},
+ {in: testGroupFile, user: "jov", gid: "37", gids: []string{"37", "777"}},
+ {in: testGroupFile, user: "user777", gid: "7", gids: []string{"7", "777", "1000"}},
+ {in: testGroupFile, user: "user1111", gid: "1111", gids: []string{"1111", "1000"}},
+ {in: testGroupFile, user: "user1000", gid: "1000", gids: []string{"1000"}},
+ {in: testGroupFile, user: "user7500", gid: "7500", gids: []string{"1000", "7500"}},
+ {in: testGroupFile, user: "no-such-user", gid: "2345", gids: []string{"2345"}},
+ {in: "", user: "no-such-user", gid: "2345", gids: []string{"2345"}},
+ // Error cases.
+ {in: "", user: "", gid: "2345", err: true},
+ {in: "", user: "joanna", gid: "bad", err: true},
+}
+
+func TestListGroups(t *testing.T) {
+ for _, tc := range listGroupsTests {
+ u := &User{Username: tc.user, Gid: tc.gid}
+ got, err := listGroupsFromReader(u, strings.NewReader(tc.in))
+ if tc.err {
+ if err == nil {
+ t.Errorf("listGroups(%q): got nil; want error", tc.user)
+ }
+ continue // no more checks
+ }
+ if err != nil {
+ t.Errorf("listGroups(%q): got %v error, want nil", tc.user, err)
+ continue // no more checks
+ }
+ checkSameIDs(t, got, tc.gids)
+ }
+}
+
+func checkSameIDs(t *testing.T, got, want []string) {
+ t.Helper()
+ if len(got) != len(want) {
+ t.Errorf("ID list mismatch: got %v; want %v", got, want)
+ return
+ }
+ sort.Strings(got)
+ sort.Strings(want)
+ mismatch := -1
+ for i, g := range want {
+ if got[i] != g {
+ mismatch = i
+ break
+ }
+ }
+ if mismatch != -1 {
+ t.Errorf("ID list mismatch (at index %d): got %v; want %v", mismatch, got, want)
+ }
+}
diff --git a/src/os/user/lookup_stubs.go b/src/os/user/lookup_stubs.go
index d8e3d48..efaa929 100644
--- a/src/os/user/lookup_stubs.go
+++ b/src/os/user/lookup_stubs.go
@@ -8,17 +8,12 @@
package user
import (
- "errors"
"fmt"
"os"
"runtime"
"strconv"
)
-func init() {
- groupListImplemented = false
-}
-
func current() (*User, error) {
uid := currentUID()
// $USER and /etc/passwd may disagree; prefer the latter if we can get it.
@@ -64,13 +59,6 @@
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)
diff --git a/src/os/user/lookup_unix.go b/src/os/user/lookup_unix.go
index dffea4a..ac4f150 100644
--- a/src/os/user/lookup_unix.go
+++ b/src/os/user/lookup_unix.go
@@ -18,16 +18,7 @@
"strings"
)
-const (
- groupFile = "/etc/group"
- userFile = "/etc/passwd"
-)
-
-var colon = []byte{':'}
-
-func init() {
- groupListImplemented = false
-}
+const userFile = "/etc/passwd"
// lineFunc returns a value, an error, or (nil, nil) to skip the row.
type lineFunc func(line []byte) (v interface{}, err error)
diff --git a/src/os/user/lookup_unix_test.go b/src/os/user/lookup_unix_test.go
index 060cfe1..05d2356 100644
--- a/src/os/user/lookup_unix_test.go
+++ b/src/os/user/lookup_unix_test.go
@@ -9,30 +9,11 @@
package user
import (
- "fmt"
"reflect"
"strings"
"testing"
)
-var 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
-` + largeGroup()
-
var groupTests = []struct {
in string
name string
@@ -51,19 +32,10 @@
{testGroupFile, "indented", "7"},
{testGroupFile, "# comment", ""},
{testGroupFile, "largegroup", "1000"},
+ {testGroupFile, "manymembers", "777"},
{"", "emptyfile", ""},
}
-// Generate a proper "largegroup" entry for testGroupFile string
-func largeGroup() (res string) {
- var b strings.Builder
- b.WriteString("largegroup:x:1000:user1")
- for i := 2; i <= 7500; i++ {
- fmt.Fprintf(&b, ",user%d", i)
- }
- return b.String()
-}
-
func TestFindGroupName(t *testing.T) {
for _, tt := range groupTests {
got, err := findGroupName(tt.name, strings.NewReader(tt.in))
diff --git a/src/os/user/user.go b/src/os/user/user.go
index 4e1b5b3..0307d2a 100644
--- a/src/os/user/user.go
+++ b/src/os/user/user.go
@@ -6,11 +6,13 @@
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.
+resolving user and group ids to names, and listing supplementary group IDs.
+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, getgrnam_r, and getgrouplist.
-When cgo is available, cgo-based (libc-backed) code is used by default.
+When cgo is available, and the required routines are implemented in libc
+for a particular platform, cgo-based (libc-backed) code is used.
This can be overridden by using osusergo build tag, which enforces
the pure Go implementation.
*/