]> Cypherpunks repositories - gostls13.git/commitdiff
os/user: implement go native GroupIds
authorKir Kolyshkin <kolyshkin@gmail.com>
Thu, 24 Jun 2021 03:46:11 +0000 (20:46 -0700)
committerIan Lance Taylor <iant@golang.org>
Tue, 21 Sep 2021 23:11:47 +0000 (23:11 +0000)
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>

src/os/user/cgo_listgroups_unix.go [new file with mode: 0644]
src/os/user/listgroups_aix.go [deleted file]
src/os/user/listgroups_illumos.go [deleted file]
src/os/user/listgroups_stub.go [new file with mode: 0644]
src/os/user/listgroups_unix.go
src/os/user/listgroups_unix_test.go [new file with mode: 0644]
src/os/user/lookup_stubs.go
src/os/user/lookup_unix.go
src/os/user/lookup_unix_test.go
src/os/user/user.go

diff --git a/src/os/user/cgo_listgroups_unix.go b/src/os/user/cgo_listgroups_unix.go
new file mode 100644 (file)
index 0000000..38aa765
--- /dev/null
@@ -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 (file)
index fbc1deb..0000000
+++ /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 (file)
index e783b26..0000000
+++ /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 (file)
index 0000000..a066c6d
--- /dev/null
@@ -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")
+}
index 38aa7653b054f6b2cb101b76865b4cc7aa75017d..fa2df4931c152e76144fcb30041947c8e3bc770e 100644 (file)
-// 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 (file)
index 0000000..a9f79ec
--- /dev/null
@@ -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)
+       }
+}
index d8e3d4866a5eb0b4e1e3a01f8d1afaa19cc81707..efaa92923d2d1b8611288d020712692de39175c2 100644 (file)
@@ -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 @@ func current() (*User, error) {
        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)
index dffea4a885410b93c84b5687daa39bbd44c52ce7..ac4f1502af8f1793427545233d9bf39d7b06ff63 100644 (file)
@@ -18,16 +18,7 @@ import (
        "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)
index 060cfe186f5ced2d288611965300d1163bda5571..05d23567c383719cf766de23bd2890a9a6b19d5f 100644 (file)
@@ -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 @@ var groupTests = []struct {
        {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))
index 4e1b5b34070da9ae02f8050ddbaa55f7edea6244..0307d2ad6a12713e2e1339d92a5a0800adb621c0 100644 (file)
@@ -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.
 */