Skip to content

Commit

Permalink
Add users/groups iteration functionality to os/user
Browse files Browse the repository at this point in the history
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
Mantelijo authored and mjonaitis1 committed Sep 8, 2021
1 parent 28a3732 commit e3a4b4f
Show file tree
Hide file tree
Showing 16 changed files with 1,076 additions and 3 deletions.
25 changes: 25 additions & 0 deletions src/os/user/iterate.go
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)
}
161 changes: 161 additions & 0 deletions src/os/user/iterate_cgo.go
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
}
}
}
53 changes: 53 additions & 0 deletions src/os/user/iterate_cgo_bsd_windows_test.go
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")
}
}
81 changes: 81 additions & 0 deletions src/os/user/iterate_cgo_test.go
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)
}
}
57 changes: 57 additions & 0 deletions src/os/user/iterate_example_test.go
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.
}
Loading

0 comments on commit e3a4b4f

Please sign in to comment.