| // 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 ( |
| "bufio" |
| "bytes" |
| "errors" |
| "fmt" |
| "io" |
| "os" |
| "strconv" |
| ) |
| |
| const groupFile = "/etc/group" |
| |
| var colon = []byte{':'} |
| |
| 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) |
| } |
| |
| 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) |
| } |
| |
| return groups, nil |
| } |
| |
| func listGroups(u *User) ([]string, error) { |
| f, err := os.Open(groupFile) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() |
| |
| return listGroupsFromReader(u, f) |
| } |