Skip to content

Commit

Permalink
add unix, plan9, windows users groups iteration to os/user
Browse files Browse the repository at this point in the history
  • Loading branch information
mjonaitis1 committed Aug 23, 2021
1 parent ff1cd5d commit ca4f26b
Show file tree
Hide file tree
Showing 18 changed files with 1,111 additions and 81 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 is used in the underlying implementation. Since getpwent 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 is used in the underlying implementation. Since getgrent is not thread-safe,
// locking is strongly advised.
func IterateGroups(n NextGroupFunc) error {
return iterateGroups(n)
}
149 changes: 149 additions & 0 deletions src/os/user/iterate_cgo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//go:build (aix || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || darwin) && cgo && !osusergo
// +build aix dragonfly freebsd !android,linux netbsd openbsd solaris darwin
// +build cgo
// +build !osusergo

package user

/*
#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 iuh value.
var iuh 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 {
iuh.set()
defer iuh.end()
for {
var result *C.struct_passwd
C.resetErrno()
result, err := iuh.get()

// If result is nil - getpwent iterated through entire users database or there was an error
if result == nil {
return err
}

// User provided non-nil error means that iteration should be terminated
if err = fn(buildUser(result)); err != nil {
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 igh value.
var igh 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 {
igh.set()
defer igh.end()
for {
var result *C.struct_group
C.resetErrno()
result, err := igh.get()

// If result is nil - getgrent iterated through entire groups database or there was an error
if result == nil {
return err
}

// User provided non-nil error means that iteration should be terminated
if err = fn(buildGroup(result)); err != nil {
return err
}
}
}
43 changes: 43 additions & 0 deletions src/os/user/iterate_cgo_bsd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//go:build (darwin || freebsd || openbsd || netbsd) && cgo && !osusergo
// +build darwin freebsd openbsd netbsd
// +build cgo
// +build !osusergo

package user

import (
"errors"
"testing"
)

// As cgo implementation can not be tested on darwin or bsd based operating
// systems, since there is no support for fgetpwent/fgetgrent library calls,
// we will attempt to check if user/group record could be at least retrieved

var _stopErr = errors.New("terminate iteration")

func TestIterateUser(t *testing.T) {
err := iterateUsers(func(user *User) error {
if user.Username == "" && user.Gid == "" && user.Uid == "" && user.HomeDir == "" && user.Name == "" {
t.Errorf("parsed user is empty: %+v", user)
}
return _stopErr
})

if err != _stopErr {
t.Errorf("iterating users: %w", err)
}
}

func TestIterateGroup(t *testing.T) {
err := iterateGroups(func(group *Group) error {
if group.Name == "" && group.Gid == "" {
t.Errorf("parsed group is empty: %+v", group)
}
return _stopErr
})

if err != _stopErr {
t.Errorf("iterating groups: %w", err)
}
}
77 changes: 77 additions & 0 deletions src/os/user/iterate_cgo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//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"
)

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"},
}

iuh = &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 len(gotUsers) != len(wantsUsers) || !reflect.DeepEqual(wantsUsers, gotUsers) {
t.Errorf("could not parse all users correctly")
}
}

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
igh = &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 len(gotGroups) != len(wantsGroups) || !reflect.DeepEqual(wantsGroups, gotGroups) {
t.Errorf("could not parse all groups correctly")
}
}
54 changes: 54 additions & 0 deletions src/os/user/iterate_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package user

import (
"errors"
"fmt"
)

func ExampleIterateUsers() {
// Get first 20 users
users := make([]*User, 0, 20)
i := 0
err := IterateUsers(func(user *User) error {
users = append(users, user)
i++

// Once we return non-nil error - iteration process stops
if i >= 20 {
return errors.New("stop iterating")
}

// As long as error is nil, IterateUsers will iterate over users database
return nil
})

if 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() {
// Get first 20 groups
groups := make([]*Group, 0, 20)
i := 0
err := IterateGroups(func(group *Group) error {
groups = append(groups, group)
i++

// Once we return non-nil error - iteration process stops
if i >= 20 {
return errors.New("stop iterating")
}

// As long as error is nil, IterateGroups will iterate over groups database
return nil
})

if 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 ca4f26b

Please sign in to comment.