-
Notifications
You must be signed in to change notification settings - Fork 17.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
28a3732
commit e3a4b4f
Showing
16 changed files
with
1,076 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package user | ||
|
||
// NextUserFunc is used in users iteration process. It receives *User for each user record. | ||
// If non-nil error is returned from NextUserFunc - iteration process is terminated. | ||
type NextUserFunc func(*User) error | ||
|
||
// NextGroupFunc is used in groups iteration process. It receives *Group for each group record. | ||
// If non-nil error is returned from NextGroupFunc - iteration process is terminated. | ||
type NextGroupFunc func(*Group) error | ||
|
||
// IterateUsers iterates over user entries. For each retrieved *User entry provided NextUserFunc is called. | ||
// | ||
// On UNIX, if CGO is enabled, getpwent(3) is used in the underlying implementation. Since getpwent(3) is not thread-safe, | ||
// locking is strongly advised. | ||
func IterateUsers(n NextUserFunc) error { | ||
return iterateUsers(n) | ||
} | ||
|
||
// IterateGroups iterates over group entries. For each retrieved *Group entry provided NextGroupFunc is called. | ||
// | ||
// On UNIX, if CGO is enabled, getgrent(3) is used in the underlying implementation. Since getgrent(3) is not thread-safe, | ||
// locking is strongly advised. | ||
func IterateGroups(n NextGroupFunc) error { | ||
return iterateGroups(n) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
//go:build (aix || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris) && cgo && !osusergo | ||
// +build aix dragonfly freebsd !android,linux netbsd openbsd solaris | ||
// +build cgo | ||
// +build !osusergo | ||
|
||
package user | ||
|
||
// On darwin, there seems to be some issues when using getpwent(3) | ||
// and getgrent(3). Until the issues are fixed, it is not recommended | ||
// relying on these libc library calls. As such, cgo version of | ||
// users and groups iterators should be disabled on darwin. | ||
// https://developer.apple.com/forums/thread/689613 | ||
|
||
/* | ||
#include <unistd.h> | ||
#include <sys/types.h> | ||
#include <pwd.h> | ||
#include <grp.h> | ||
#include <stdlib.h> | ||
#include <stdio.h> | ||
#include <errno.h> | ||
static void resetErrno(){ | ||
errno = 0; | ||
} | ||
*/ | ||
import "C" | ||
|
||
// usersHelper defines the methods used in users iteration process within | ||
// iterateUsers. This interface allows testing iterateUsers functionality. | ||
// iterate_test_fgetent.go file defines test related struct that implements | ||
// usersHelper. | ||
type usersHelper interface { | ||
// set sets up internal state before iteration | ||
set() | ||
|
||
// get sequentially returns a passwd structure which is later processed into *User entry | ||
get() (*C.struct_passwd, error) | ||
|
||
// end cleans up internal state after iteration is done | ||
end() | ||
} | ||
|
||
type iterateUsersHelper struct{} | ||
|
||
func (i iterateUsersHelper) set() { | ||
C.setpwent() | ||
} | ||
|
||
func (i iterateUsersHelper) get() (*C.struct_passwd, error) { | ||
var result *C.struct_passwd | ||
result, err := C.getpwent() | ||
return result, err | ||
} | ||
|
||
func (i iterateUsersHelper) end() { | ||
C.endpwent() | ||
} | ||
|
||
// This helper is used to retrieve users via c library call. A global | ||
// variable which implements usersHelper interface is needed in order to | ||
// separate testing logic from production. Since cgo can not be used directly | ||
// in tests, iterate_test_fgetent.go file provides iterateUsersHelperTest | ||
// structure which implements usersHelper interface and can substitute | ||
// default userIterator value. | ||
var userIterator usersHelper = iterateUsersHelper{} | ||
|
||
// iterateUsers iterates over users database via getpwent(3). If fn returns non | ||
// nil error, then iteration is terminated. A nil result from getpwent means | ||
// there were no more entries, or an error occurred, as such, iteration is | ||
// terminated, and if error was encountered it is returned. | ||
// | ||
// Since iterateUsers uses getpwent(3), which is not thread safe, iterateUsers | ||
// can not bet used concurrently. If concurrent usage is required, it is | ||
// recommended to use locking mechanism such as sync.Mutex when calling | ||
// iterateUsers from multiple goroutines. | ||
func iterateUsers(fn NextUserFunc) error { | ||
userIterator.set() | ||
defer userIterator.end() | ||
for { | ||
var result *C.struct_passwd | ||
C.resetErrno() | ||
result, err := userIterator.get() | ||
|
||
// If result is nil - getpwent iterated through entire users database or there was an error | ||
if result == nil { | ||
return err | ||
} | ||
|
||
if err = fn(buildUser(result)); err != nil { | ||
// User provided non-nil error means that iteration should be terminated | ||
return err | ||
} | ||
} | ||
} | ||
|
||
// groupsHelper defines the methods used in groups iteration process within iterateGroups. This interface allows testing | ||
// iterateGroups functionality. iterate_test_fgetent.go file defines test related struct that implements groupsHelper. | ||
type groupsHelper interface { | ||
// set sets up internal state before iteration | ||
set() | ||
|
||
// get sequentially returns a group structure which is later processed into *Group entry | ||
get() (*C.struct_group, error) | ||
|
||
// end cleans up internal state after iteration is done | ||
end() | ||
} | ||
|
||
type iterateGroupsHelper struct{} | ||
|
||
func (i iterateGroupsHelper) set() { | ||
C.setgrent() | ||
} | ||
|
||
func (i iterateGroupsHelper) get() (*C.struct_group, error) { | ||
var result *C.struct_group | ||
result, err := C.getgrent() | ||
return result, err | ||
} | ||
|
||
func (i iterateGroupsHelper) end() { | ||
C.endgrent() | ||
} | ||
|
||
// This helper is used to retrieve groups via c library call. A global | ||
// variable which implements groupsHelper interface is needed in order to | ||
// separate testing logic from production. Since cgo can not be used directly | ||
// in tests, iterate_test_fgetent.go file provides iterateGroupsHelperTest | ||
// structure which implements groupsHelper interface and can substitute | ||
// default groupIterator value. | ||
var groupIterator groupsHelper = iterateGroupsHelper{} | ||
|
||
// iterateGroups iterates over groups database via getgrent(3). If fn returns | ||
// non nil error, then iteration is terminated. A nil result from getgrent means | ||
// there were no more entries, or an error occurred, as such, iteration is | ||
// terminated, and if error was encountered it is returned. | ||
// | ||
// Since iterateGroups uses getgrent(3), which is not thread safe, iterateGroups | ||
// can not bet used concurrently. If concurrent usage is required, it is | ||
// recommended to use locking mechanism such as sync.Mutex when calling | ||
// iterateGroups from multiple goroutines. | ||
func iterateGroups(fn NextGroupFunc) error { | ||
groupIterator.set() | ||
defer groupIterator.end() | ||
for { | ||
var result *C.struct_group | ||
C.resetErrno() | ||
result, err := groupIterator.get() | ||
|
||
// If result is nil - getgrent iterated through entire groups database or there was an error | ||
if result == nil { | ||
return err | ||
} | ||
|
||
if err = fn(buildGroup(result)); err != nil { | ||
// User provided non-nil error means that iteration should be terminated | ||
return err | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
//go:build ((darwin || freebsd || openbsd || netbsd) && cgo && !osusergo) || windows | ||
|
||
package user | ||
|
||
import ( | ||
"errors" | ||
"testing" | ||
) | ||
|
||
// As BSDs (including darwin) do not support fgetpwent(3)/fgetgrent(3), attempt | ||
// to check if at least 1 user/group record can be retrieved. | ||
// On Windows, it is not possible to easily mock registry. Checking if at | ||
// least one user and group can be retrieved via iteration will suffice. | ||
|
||
var _stopErr = errors.New("terminate iteration") | ||
|
||
func TestIterateUser(t *testing.T) { | ||
gotAtLeastOne := false | ||
err := iterateUsers(func(user *User) error { | ||
if *user == (User{}) { | ||
t.Errorf("parsed user is empty: %+v", user) | ||
} | ||
gotAtLeastOne = true | ||
return _stopErr | ||
}) | ||
|
||
if err != _stopErr { | ||
t.Errorf("iterating users: %w", err) | ||
} | ||
|
||
if !gotAtLeastOne { | ||
t.Errorf("no users were iterated") | ||
} | ||
} | ||
|
||
func TestIterateGroup(t *testing.T) { | ||
gotAtLeastOne := false | ||
err := iterateGroups(func(group *Group) error { | ||
if *group == (Group{}) { | ||
t.Errorf("parsed group is empty: %+v", group) | ||
} | ||
gotAtLeastOne = true | ||
return _stopErr | ||
}) | ||
|
||
if err != _stopErr { | ||
t.Errorf("iterating groups: %w", err) | ||
} | ||
|
||
if !gotAtLeastOne { | ||
t.Errorf("no groups were iterated") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
//go:build (aix || dragonfly || (!android && linux) || solaris) && cgo && !osusergo | ||
// +build aix dragonfly !android,linux solaris | ||
// +build cgo | ||
// +build !osusergo | ||
|
||
package user | ||
|
||
import ( | ||
"reflect" | ||
"syscall" | ||
"testing" | ||
) | ||
|
||
// This file is used for testing cgo based unix implementation of users and | ||
// groups iterators. Only unix based systems which support fgetpwent(3) and | ||
// fgetgrent(3) can run tests from this file. | ||
|
||
func TestIterateUser(t *testing.T) { | ||
var wantsUsers = []*User{ | ||
{Uid: "0", Gid: "0", Username: "root", Name: "System Administrator", HomeDir: "/var/root"}, | ||
{Uid: "1", Gid: "1", Username: "daemon", Name: "System Services", HomeDir: "/var/root"}, | ||
{Uid: "4", Gid: "4", Username: "_uucp", Name: "Unix to Unix Copy Protocol", HomeDir: "/var/spool/uucp"}, | ||
{Uid: "13", Gid: "13", Username: "_taskgated", Name: "Task Gate Daemon", HomeDir: "/var/empty"}, | ||
{Uid: "24", Gid: "24", Username: "_networkd", Name: "Network Services", HomeDir: "/var/networkd"}, | ||
{Uid: "25", Gid: "25", Username: "_installassistant", Name: "Install Assistant", HomeDir: "/var/empty"}, | ||
{Uid: "26", Gid: "26", Username: "_lp", Name: "Printing Services", HomeDir: "/var/spool/cups"}, | ||
{Uid: "27", Gid: "27", Username: "_postfix", Name: "Postfix Mail Server", HomeDir: "/var/spool/postfix"}, | ||
} | ||
|
||
userIterator = &iterateUsersHelperTest{} | ||
|
||
// Test that users are retrieved in same order as defined | ||
gotUsers := make([]*User, 0, len(wantsUsers)) | ||
err := iterateUsers(func(user *User) error { | ||
gotUsers = append(gotUsers, user) | ||
return nil | ||
}) | ||
|
||
if err != syscall.ENOENT { | ||
t.Errorf("iterating users: %v", err) | ||
} | ||
|
||
if !reflect.DeepEqual(wantsUsers, gotUsers) { | ||
t.Errorf("iterate users result is incorrect: got: %+v, want: %+v", gotUsers, wantsUsers) | ||
} | ||
} | ||
|
||
func TestIterateGroup(t *testing.T) { | ||
var wantsGroups = []*Group{ | ||
{Gid: "0", Name: "wheel"}, | ||
{Gid: "1", Name: "daemon"}, | ||
{Gid: "2", Name: "kmem"}, | ||
{Gid: "3", Name: "sys"}, | ||
{Gid: "5", Name: "operator"}, | ||
{Gid: "6", Name: "mail"}, | ||
{Gid: "4", Name: "tty"}, | ||
{Gid: "7", Name: "bin"}, | ||
{Gid: "8", Name: "procview"}, | ||
{Gid: "9", Name: "procmod"}, | ||
{Gid: "10", Name: "owner"}, | ||
{Gid: "12", Name: "everyone"}, | ||
} | ||
|
||
// Use testdata fixture | ||
groupIterator = &iterateGroupsHelperTest{} | ||
|
||
// Test that groups are retrieved in same order as defined | ||
gotGroups := make([]*Group, 0, len(wantsGroups)) | ||
err := iterateGroups(func(g *Group) error { | ||
gotGroups = append(gotGroups, g) | ||
return nil | ||
}) | ||
|
||
if err != syscall.ENOENT { | ||
t.Errorf("iterating groups: %v", err) | ||
} | ||
|
||
if !reflect.DeepEqual(wantsGroups, gotGroups) { | ||
t.Errorf("iterate groups result is incorrect: got: %+v, want: %+v", gotGroups, wantsGroups) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package user_test | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"os/user" | ||
) | ||
|
||
func ExampleIterateUsers() { | ||
stopErr := errors.New("stop iterating") | ||
// Get first 20 users | ||
users := make([]*user.User, 0, 20) | ||
i := 0 | ||
err := user.IterateUsers(func(user *user.User) error { | ||
users = append(users, user) | ||
i++ | ||
|
||
// Once we return non-nil error - iteration process stops | ||
if i >= 20 { | ||
return stopErr | ||
} | ||
|
||
// As long as error is nil, IterateUsers will iterate over users database | ||
return nil | ||
}) | ||
|
||
if err != stopErr && err != nil { | ||
fmt.Printf("error encountered while iterating users database: %v", err) | ||
} | ||
|
||
// Here users slice can be used to do something with collected users. | ||
} | ||
|
||
func ExampleIterateGroups() { | ||
stopErr := errors.New("stop iterating") | ||
// Get first 20 groups | ||
groups := make([]*user.Group, 0, 20) | ||
i := 0 | ||
err := user.IterateGroups(func(group *user.Group) error { | ||
groups = append(groups, group) | ||
i++ | ||
|
||
// Once we return non-nil error - iteration process stops | ||
if i >= 20 { | ||
return stopErr | ||
} | ||
|
||
// As long as error is nil, IterateGroups will iterate over groups database | ||
return nil | ||
}) | ||
|
||
if err != stopErr && err != nil { | ||
fmt.Printf("error encountered while iterating groups database: %v", err) | ||
} | ||
|
||
// Here groups slice can be used to do something with collected groups. | ||
} |
Oops, something went wrong.