blob: fa2df4931c152e76144fcb30041947c8e3bc770e [file] [log] [blame]
// 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)
}