• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2021 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5//go:build ((darwin || dragonfly || freebsd || (js && wasm) || wasip1 || (!android && linux) || netbsd || openbsd || solaris) && ((!cgo && !darwin) || osusergo)) || aix || illumos
6
7package user
8
9import (
10	"bufio"
11	"bytes"
12	"errors"
13	"fmt"
14	"io"
15	"os"
16	"strconv"
17)
18
19func listGroupsFromReader(u *User, r io.Reader) ([]string, error) {
20	if u.Username == "" {
21		return nil, errors.New("user: list groups: empty username")
22	}
23	primaryGid, err := strconv.Atoi(u.Gid)
24	if err != nil {
25		return nil, fmt.Errorf("user: list groups for %s: invalid gid %q", u.Username, u.Gid)
26	}
27
28	userCommas := []byte("," + u.Username + ",")  // ,john,
29	userFirst := userCommas[1:]                   // john,
30	userLast := userCommas[:len(userCommas)-1]    // ,john
31	userOnly := userCommas[1 : len(userCommas)-1] // john
32
33	// Add primary Gid first.
34	groups := []string{u.Gid}
35
36	rd := bufio.NewReader(r)
37	done := false
38	for !done {
39		line, err := rd.ReadBytes('\n')
40		if err != nil {
41			if err == io.EOF {
42				done = true
43			} else {
44				return groups, err
45			}
46		}
47
48		// Look for username in the list of users. If user is found,
49		// append the GID to the groups slice.
50
51		// There's no spec for /etc/passwd or /etc/group, but we try to follow
52		// the same rules as the glibc parser, which allows comments and blank
53		// space at the beginning of a line.
54		line = bytes.TrimSpace(line)
55		if len(line) == 0 || line[0] == '#' ||
56			// If you search for a gid in a row where the group
57			// name (the first field) starts with "+" or "-",
58			// glibc fails to find the record, and so should we.
59			line[0] == '+' || line[0] == '-' {
60			continue
61		}
62
63		// Format of /etc/group is
64		// 	groupname:password:GID:user_list
65		// for example
66		// 	wheel:x:10:john,paul,jack
67		//	tcpdump:x:72:
68		listIdx := bytes.LastIndexByte(line, ':')
69		if listIdx == -1 || listIdx == len(line)-1 {
70			// No commas, or empty group list.
71			continue
72		}
73		if bytes.Count(line[:listIdx], colon) != 2 {
74			// Incorrect number of colons.
75			continue
76		}
77		list := line[listIdx+1:]
78		// Check the list for user without splitting or copying.
79		if !(bytes.Equal(list, userOnly) || bytes.HasPrefix(list, userFirst) || bytes.HasSuffix(list, userLast) || bytes.Contains(list, userCommas)) {
80			continue
81		}
82
83		// groupname:password:GID
84		parts := bytes.Split(line[:listIdx], colon)
85		if len(parts) != 3 || len(parts[0]) == 0 {
86			continue
87		}
88		gid := string(parts[2])
89		// Make sure it's numeric and not the same as primary GID.
90		numGid, err := strconv.Atoi(gid)
91		if err != nil || numGid == primaryGid {
92			continue
93		}
94
95		groups = append(groups, gid)
96	}
97
98	return groups, nil
99}
100
101func listGroups(u *User) ([]string, error) {
102	f, err := os.Open(groupFile)
103	if err != nil {
104		return nil, err
105	}
106	defer f.Close()
107
108	return listGroupsFromReader(u, f)
109}
110