Skip to content

Commit 70eed7b

Browse files
committed
Add users/groups iteration functionality to os/user
Go standard library has os/user package which allows to lookup user or group records via either user/group name or id. This commit extends capabilities of os/user package by introducing iteration functionality for users and groups. Users and groups iteration functionality might be useful in cases where a full or partial list of all available users/groups is required in an application.
1 parent 28a3732 commit 70eed7b

16 files changed

+1062
-3
lines changed

src/os/user/iterate.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package user
2+
3+
// NextUserFunc is used in users iteration process. It receives *User for each user record.
4+
// If non-nil error is returned from NextUserFunc - iteration process is terminated.
5+
type NextUserFunc func(*User) error
6+
7+
// NextGroupFunc is used in groups iteration process. It receives *Group for each group record.
8+
// If non-nil error is returned from NextGroupFunc - iteration process is terminated.
9+
type NextGroupFunc func(*Group) error
10+
11+
// IterateUsers iterates over user entries. For each retrieved *User entry provided NextUserFunc is called.
12+
//
13+
// On UNIX, if CGO is enabled, getpwent(3) is used in the underlying implementation. Since getpwent(3) is not thread-safe,
14+
// locking is strongly advised.
15+
func IterateUsers(n NextUserFunc) error {
16+
return iterateUsers(n)
17+
}
18+
19+
// IterateGroups iterates over group entries. For each retrieved *Group entry provided NextGroupFunc is called.
20+
//
21+
// On UNIX, if CGO is enabled, getgrent(3) is used in the underlying implementation. Since getgrent(3) is not thread-safe,
22+
// locking is strongly advised.
23+
func IterateGroups(n NextGroupFunc) error {
24+
return iterateGroups(n)
25+
}

src/os/user/iterate_cgo.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//go:build (aix || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || darwin) && cgo && !osusergo
2+
// +build aix dragonfly freebsd !android,linux netbsd openbsd solaris darwin
3+
// +build cgo
4+
// +build !osusergo
5+
6+
package user
7+
8+
/*
9+
#include <unistd.h>
10+
#include <sys/types.h>
11+
#include <pwd.h>
12+
#include <grp.h>
13+
#include <stdlib.h>
14+
#include <stdio.h>
15+
#include <errno.h>
16+
17+
static void resetErrno(){
18+
errno = 0;
19+
}
20+
*/
21+
import "C"
22+
23+
// usersHelper defines the methods used in users iteration process within
24+
// iterateUsers. This interface allows testing iterateUsers functionality.
25+
// iterate_test_fgetent.go file defines test related struct that implements
26+
// usersHelper.
27+
type usersHelper interface {
28+
// set sets up internal state before iteration
29+
set()
30+
31+
// get sequentially returns a passwd structure which is later processed into *User entry
32+
get() (*C.struct_passwd, error)
33+
34+
// end cleans up internal state after iteration is done
35+
end()
36+
}
37+
38+
type iterateUsersHelper struct{}
39+
40+
func (i iterateUsersHelper) set() {
41+
C.setpwent()
42+
}
43+
44+
func (i iterateUsersHelper) get() (*C.struct_passwd, error) {
45+
var result *C.struct_passwd
46+
result, err := C.getpwent()
47+
return result, err
48+
}
49+
50+
func (i iterateUsersHelper) end() {
51+
C.endpwent()
52+
}
53+
54+
// This helper is used to retrieve users via c library call. A global
55+
// variable which implements usersHelper interface is needed in order to
56+
// separate testing logic from production. Since cgo can not be used directly
57+
// in tests, iterate_test_fgetent.go file provides iterateUsersHelperTest
58+
// structure which implements usersHelper interface and can substitute
59+
// default userIterator value.
60+
var userIterator usersHelper = iterateUsersHelper{}
61+
62+
// iterateUsers iterates over users database via getpwent(3). If fn returns non
63+
// nil error, then iteration is terminated. A nil result from getpwent means
64+
// there were no more entries, or an error occurred, as such, iteration is
65+
// terminated, and if error was encountered it is returned.
66+
//
67+
// Since iterateUsers uses getpwent(3), which is not thread safe, iterateUsers
68+
// can not bet used concurrently. If concurrent usage is required, it is
69+
// recommended to use locking mechanism such as sync.Mutex when calling
70+
// iterateUsers from multiple goroutines.
71+
func iterateUsers(fn NextUserFunc) error {
72+
userIterator.set()
73+
defer userIterator.end()
74+
for {
75+
var result *C.struct_passwd
76+
C.resetErrno()
77+
result, err := userIterator.get()
78+
79+
// If result is nil - getpwent iterated through entire users database or there was an error
80+
if result == nil {
81+
return err
82+
}
83+
84+
if err = fn(buildUser(result)); err != nil {
85+
// User provided non-nil error means that iteration should be terminated
86+
return err
87+
}
88+
}
89+
}
90+
91+
// groupsHelper defines the methods used in groups iteration process within iterateGroups. This interface allows testing
92+
// iterateGroups functionality. iterate_test_fgetent.go file defines test related struct that implements groupsHelper.
93+
type groupsHelper interface {
94+
// set sets up internal state before iteration
95+
set()
96+
97+
// get sequentially returns a group structure which is later processed into *Group entry
98+
get() (*C.struct_group, error)
99+
100+
// end cleans up internal state after iteration is done
101+
end()
102+
}
103+
104+
type iterateGroupsHelper struct{}
105+
106+
func (i iterateGroupsHelper) set() {
107+
C.setgrent()
108+
}
109+
110+
func (i iterateGroupsHelper) get() (*C.struct_group, error) {
111+
var result *C.struct_group
112+
result, err := C.getgrent()
113+
return result, err
114+
}
115+
116+
func (i iterateGroupsHelper) end() {
117+
C.endgrent()
118+
}
119+
120+
// This helper is used to retrieve groups via c library call. A global
121+
// variable which implements groupsHelper interface is needed in order to
122+
// separate testing logic from production. Since cgo can not be used directly
123+
// in tests, iterate_test_fgetent.go file provides iterateGroupsHelperTest
124+
// structure which implements groupsHelper interface and can substitute
125+
// default groupIterator value.
126+
var groupIterator groupsHelper = iterateGroupsHelper{}
127+
128+
// iterateGroups iterates over groups database via getgrent(3). If fn returns
129+
// non nil error, then iteration is terminated. A nil result from getgrent means
130+
// there were no more entries, or an error occurred, as such, iteration is
131+
// terminated, and if error was encountered it is returned.
132+
//
133+
// Since iterateGroups uses getgrent(3), which is not thread safe, iterateGroups
134+
// can not bet used concurrently. If concurrent usage is required, it is
135+
// recommended to use locking mechanism such as sync.Mutex when calling
136+
// iterateGroups from multiple goroutines.
137+
func iterateGroups(fn NextGroupFunc) error {
138+
groupIterator.set()
139+
defer groupIterator.end()
140+
for {
141+
var result *C.struct_group
142+
C.resetErrno()
143+
result, err := groupIterator.get()
144+
145+
// If result is nil - getgrent iterated through entire groups database or there was an error
146+
if result == nil {
147+
return err
148+
}
149+
150+
if err = fn(buildGroup(result)); err != nil {
151+
// User provided non-nil error means that iteration should be terminated
152+
return err
153+
}
154+
}
155+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//go:build ((darwin || freebsd || openbsd || netbsd) && cgo && !osusergo) || windows
2+
3+
package user
4+
5+
import (
6+
"errors"
7+
"testing"
8+
)
9+
10+
// As BSDs (including darwin) do not support fgetpwent(3)/fgetgrent(3), attempt
11+
// to check if at least 1 user/group record can be retrieved.
12+
// On Windows, it is not possible to easily mock registry. Checking if at
13+
// least one user and group can be retrieved via iteration will suffice.
14+
15+
var _stopErr = errors.New("terminate iteration")
16+
17+
func TestIterateUser(t *testing.T) {
18+
gotAtLeastOne := false
19+
err := iterateUsers(func(user *User) error {
20+
if *user == (User{}) {
21+
t.Errorf("parsed user is empty: %+v", user)
22+
}
23+
gotAtLeastOne = true
24+
return _stopErr
25+
})
26+
27+
if err != _stopErr {
28+
t.Errorf("iterating users: %w", err)
29+
}
30+
31+
if !gotAtLeastOne {
32+
t.Errorf("no users were iterated")
33+
}
34+
}
35+
36+
func TestIterateGroup(t *testing.T) {
37+
gotAtLeastOne := false
38+
err := iterateGroups(func(group *Group) error {
39+
if *group == (Group{}) {
40+
t.Errorf("parsed group is empty: %+v", group)
41+
}
42+
gotAtLeastOne = true
43+
return _stopErr
44+
})
45+
46+
if err != _stopErr {
47+
t.Errorf("iterating groups: %w", err)
48+
}
49+
50+
if !gotAtLeastOne {
51+
t.Errorf("no groups were iterated")
52+
}
53+
}

src/os/user/iterate_cgo_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//go:build (aix || dragonfly || (!android && linux) || solaris) && cgo && !osusergo
2+
// +build aix dragonfly !android,linux solaris
3+
// +build cgo
4+
// +build !osusergo
5+
6+
package user
7+
8+
import (
9+
"reflect"
10+
"syscall"
11+
"testing"
12+
)
13+
14+
// This file is used for testing cgo based unix implementation of users and
15+
// groups iterators. Only unix based systems which support fgetpwent(3) and
16+
// fgetgrent(3) can run tests from this file.
17+
18+
func TestIterateUser(t *testing.T) {
19+
var wantsUsers = []*User{
20+
{Uid: "0", Gid: "0", Username: "root", Name: "System Administrator", HomeDir: "/var/root"},
21+
{Uid: "1", Gid: "1", Username: "daemon", Name: "System Services", HomeDir: "/var/root"},
22+
{Uid: "4", Gid: "4", Username: "_uucp", Name: "Unix to Unix Copy Protocol", HomeDir: "/var/spool/uucp"},
23+
{Uid: "13", Gid: "13", Username: "_taskgated", Name: "Task Gate Daemon", HomeDir: "/var/empty"},
24+
{Uid: "24", Gid: "24", Username: "_networkd", Name: "Network Services", HomeDir: "/var/networkd"},
25+
{Uid: "25", Gid: "25", Username: "_installassistant", Name: "Install Assistant", HomeDir: "/var/empty"},
26+
{Uid: "26", Gid: "26", Username: "_lp", Name: "Printing Services", HomeDir: "/var/spool/cups"},
27+
{Uid: "27", Gid: "27", Username: "_postfix", Name: "Postfix Mail Server", HomeDir: "/var/spool/postfix"},
28+
}
29+
30+
userIterator = &iterateUsersHelperTest{}
31+
32+
// Test that users are retrieved in same order as defined
33+
gotUsers := make([]*User, 0, len(wantsUsers))
34+
err := iterateUsers(func(user *User) error {
35+
gotUsers = append(gotUsers, user)
36+
return nil
37+
})
38+
39+
if err != syscall.ENOENT {
40+
t.Errorf("iterating users: %v", err)
41+
}
42+
43+
if !reflect.DeepEqual(wantsUsers, gotUsers) {
44+
t.Errorf("iterate users result is incorrect: got: %+v, want: %+v", gotUsers, wantsUsers)
45+
}
46+
}
47+
48+
func TestIterateGroup(t *testing.T) {
49+
var wantsGroups = []*Group{
50+
{Gid: "0", Name: "wheel"},
51+
{Gid: "1", Name: "daemon"},
52+
{Gid: "2", Name: "kmem"},
53+
{Gid: "3", Name: "sys"},
54+
{Gid: "5", Name: "operator"},
55+
{Gid: "6", Name: "mail"},
56+
{Gid: "4", Name: "tty"},
57+
{Gid: "7", Name: "bin"},
58+
{Gid: "8", Name: "procview"},
59+
{Gid: "9", Name: "procmod"},
60+
{Gid: "10", Name: "owner"},
61+
{Gid: "12", Name: "everyone"},
62+
}
63+
64+
// Use testdata fixture
65+
groupIterator = &iterateGroupsHelperTest{}
66+
67+
// Test that groups are retrieved in same order as defined
68+
gotGroups := make([]*Group, 0, len(wantsGroups))
69+
err := iterateGroups(func(g *Group) error {
70+
gotGroups = append(gotGroups, g)
71+
return nil
72+
})
73+
74+
if err != syscall.ENOENT {
75+
t.Errorf("iterating groups: %v", err)
76+
}
77+
78+
if !reflect.DeepEqual(wantsGroups, gotGroups) {
79+
t.Errorf("iterate groups result is incorrect: got: %+v, want: %+v", gotGroups, wantsGroups)
80+
}
81+
}

src/os/user/iterate_example_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package user_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os/user"
7+
)
8+
9+
func ExampleIterateUsers() {
10+
stopErr := errors.New("stop iterating")
11+
// Get first 20 users
12+
users := make([]*user.User, 0, 20)
13+
i := 0
14+
err := user.IterateUsers(func(user *user.User) error {
15+
users = append(users, user)
16+
i++
17+
18+
// Once we return non-nil error - iteration process stops
19+
if i >= 20 {
20+
return stopErr
21+
}
22+
23+
// As long as error is nil, IterateUsers will iterate over users database
24+
return nil
25+
})
26+
27+
if err != stopErr && err != nil {
28+
fmt.Printf("error encountered while iterating users database: %v", err)
29+
}
30+
31+
// Here users slice can be used to do something with collected users.
32+
}
33+
34+
func ExampleIterateGroups() {
35+
stopErr := errors.New("stop iterating")
36+
// Get first 20 groups
37+
groups := make([]*user.Group, 0, 20)
38+
i := 0
39+
err := user.IterateGroups(func(group *user.Group) error {
40+
groups = append(groups, group)
41+
i++
42+
43+
// Once we return non-nil error - iteration process stops
44+
if i >= 20 {
45+
return stopErr
46+
}
47+
48+
// As long as error is nil, IterateGroups will iterate over groups database
49+
return nil
50+
})
51+
52+
if err != stopErr && err != nil {
53+
fmt.Printf("error encountered while iterating groups database: %v", err)
54+
}
55+
56+
// Here groups slice can be used to do something with collected groups.
57+
}

0 commit comments

Comments
 (0)