Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split idtools to an internal package and package to be moved #49087

Merged
merged 2 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Split internal idtools functionality
Separare idtools functionality that is used internally from the
functionlality used by importers. The `pkg/idtools` package is now
much smaller and more generic.

Signed-off-by: Derek McGowan <derek@mcg.dev>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
  • Loading branch information
dmcgowan committed Jan 7, 2025
commit 9c368a93b690735bccfdb7c371371f6ec324d81e
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package idtools // import "github.com/docker/docker/pkg/idtools"
package usergroup

import (
"fmt"
Expand Down
73 changes: 73 additions & 0 deletions internal/usergroup/add_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package usergroup

import (
"os"
"os/exec"
"os/user"
"syscall"
"testing"

"github.com/docker/docker/pkg/idtools"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)

const (
tempUser = "tempuser"
)

func TestNewIDMappings(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
_, _, err := AddNamespaceRangesUser(tempUser)
assert.Check(t, err)
defer delUser(t, tempUser)

tempUser, err := user.Lookup(tempUser)
assert.Check(t, err)

idMapping, err := LoadIdentityMapping(tempUser.Username)
assert.Check(t, err)

rootUID, rootGID, err := idtools.GetRootUIDGID(idMapping.UIDMaps, idMapping.GIDMaps)
assert.Check(t, err)

dirName, err := os.MkdirTemp("", "mkdirall")
assert.Check(t, err, "Couldn't create temp directory")
defer os.RemoveAll(dirName)

err = idtools.MkdirAllAndChown(dirName, 0o700, idtools.Identity{UID: rootUID, GID: rootGID})
assert.Check(t, err, "Couldn't change ownership of file path. Got error")
cmd := exec.Command("ls", "-la", dirName)
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{Uid: uint32(rootUID), Gid: uint32(rootGID)},
}
out, err := cmd.CombinedOutput()
assert.Check(t, err, "Unable to access %s directory with user UID:%d and GID:%d:\n%s", dirName, rootUID, rootGID, string(out))
}

func TestLookupUserAndGroup(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
uid, gid, err := AddNamespaceRangesUser(tempUser)
assert.Check(t, err)
defer delUser(t, tempUser)

fetchedUser, err := LookupUser(tempUser)
assert.Check(t, err)

fetchedUserByID, err := LookupUID(uid)
assert.Check(t, err)
assert.Check(t, is.DeepEqual(fetchedUserByID, fetchedUser))

fetchedGroup, err := LookupGroup(tempUser)
assert.Check(t, err)

fetchedGroupByID, err := LookupGID(gid)
assert.Check(t, err)
assert.Check(t, is.DeepEqual(fetchedGroupByID, fetchedGroup))
}

func delUser(t *testing.T, name string) {
out, err := exec.Command("userdel", name).CombinedOutput()
assert.Check(t, err, out)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//go:build !linux

package idtools // import "github.com/docker/docker/pkg/idtools"
package usergroup

import "fmt"

Expand Down
10 changes: 10 additions & 0 deletions internal/usergroup/const_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package usergroup

const (
SeTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege"
)

const (
ContainerAdministratorSidString = "S-1-5-93-2-1"
ContainerUserSidString = "S-1-5-93-2-2"
)
thaJeztah marked this conversation as resolved.
Show resolved Hide resolved
188 changes: 188 additions & 0 deletions internal/usergroup/lookup_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//go:build !windows

package usergroup

import (
"bytes"
"fmt"
"io"
"os/exec"
"strconv"
"syscall"

"github.com/docker/docker/pkg/idtools"
"github.com/moby/sys/user"
)

// LookupUser uses traditional local system files lookup (from libcontainer/user) on a username,
// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
func LookupUser(name string) (user.User, error) {
// first try a local system files lookup using existing capabilities
usr, err := user.LookupUser(name)
if err == nil {
return usr, nil
}
// local files lookup failed; attempt to call `getent` to query configured passwd dbs
usr, err = getentUser(name)
if err != nil {
return user.User{}, err
}
return usr, nil
}

// LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid,
// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
func LookupUID(uid int) (user.User, error) {
// first try a local system files lookup using existing capabilities
usr, err := user.LookupUid(uid)
if err == nil {
return usr, nil
}
// local files lookup failed; attempt to call `getent` to query configured passwd dbs
return getentUser(strconv.Itoa(uid))
}

func getentUser(name string) (user.User, error) {
reader, err := callGetent("passwd", name)
if err != nil {
return user.User{}, err
}
users, err := user.ParsePasswd(reader)
if err != nil {
return user.User{}, err
}
if len(users) == 0 {
return user.User{}, fmt.Errorf("getent failed to find passwd entry for %q", name)
}
return users[0], nil
}

// LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name,
// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
func LookupGroup(name string) (user.Group, error) {
// first try a local system files lookup using existing capabilities
group, err := user.LookupGroup(name)
if err == nil {
return group, nil
}
// local files lookup failed; attempt to call `getent` to query configured group dbs
return getentGroup(name)
}

// LookupGID uses traditional local system files lookup (from libcontainer/user) on a group ID,
// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
func LookupGID(gid int) (user.Group, error) {
// first try a local system files lookup using existing capabilities
group, err := user.LookupGid(gid)
if err == nil {
return group, nil
}
// local files lookup failed; attempt to call `getent` to query configured group dbs
return getentGroup(strconv.Itoa(gid))
}

func getentGroup(name string) (user.Group, error) {
reader, err := callGetent("group", name)
if err != nil {
return user.Group{}, err
}
groups, err := user.ParseGroup(reader)
if err != nil {
return user.Group{}, err
}
if len(groups) == 0 {
return user.Group{}, fmt.Errorf("getent failed to find groups entry for %q", name)
}
return groups[0], nil
}

func callGetent(database, key string) (io.Reader, error) {
getentCmd, err := resolveBinary("getent")
// if no `getent` command within the execution environment, can't do anything else
if err != nil {
return nil, fmt.Errorf("unable to find getent command: %w", err)
}
command := exec.Command(getentCmd, database, key)
// we run getent within container filesystem, but without /dev so /dev/null is not available for exec to mock stdin
command.Stdin = io.NopCloser(bytes.NewReader(nil))
out, err := command.CombinedOutput()
if err != nil {
exitCode, errC := getExitCode(err)
if errC != nil {
return nil, err
}
switch exitCode {
case 1:
return nil, fmt.Errorf("getent reported invalid parameters/database unknown")
case 2:
return nil, fmt.Errorf("getent unable to find entry %q in %s database", key, database)
case 3:
return nil, fmt.Errorf("getent database doesn't support enumeration")
default:
return nil, err
}
}
return bytes.NewReader(out), nil
}

// getExitCode returns the ExitStatus of the specified error if its type is
// exec.ExitError, returns 0 and an error otherwise.
func getExitCode(err error) (int, error) {
exitCode := 0
if exiterr, ok := err.(*exec.ExitError); ok {
if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok {
return procExit.ExitStatus(), nil
}
}
return exitCode, fmt.Errorf("failed to get exit code")
}

// LoadIdentityMapping takes a requested username and
// using the data from /etc/sub{uid,gid} ranges, creates the
// proper uid and gid remapping ranges for that user/group pair
func LoadIdentityMapping(name string) (idtools.IdentityMapping, error) {
usr, err := LookupUser(name)
if err != nil {
return idtools.IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err)
}

subuidRanges, err := lookupSubRangesFile("/etc/subuid", usr)
if err != nil {
return idtools.IdentityMapping{}, err
}
subgidRanges, err := lookupSubRangesFile("/etc/subgid", usr)
if err != nil {
return idtools.IdentityMapping{}, err
}

return idtools.IdentityMapping{
UIDMaps: subuidRanges,
GIDMaps: subgidRanges,
}, nil
}

func lookupSubRangesFile(path string, usr user.User) ([]idtools.IDMap, error) {
uidstr := strconv.Itoa(usr.Uid)
rangeList, err := user.ParseSubIDFileFilter(path, func(sid user.SubID) bool {
return sid.Name == usr.Name || sid.Name == uidstr
})
if err != nil {
return nil, err
}
if len(rangeList) == 0 {
return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name)
}

idMap := []idtools.IDMap{}

containerID := 0
for _, idrange := range rangeList {
idMap = append(idMap, idtools.IDMap{
ContainerID: containerID,
HostID: int(idrange.SubID),
Size: int(idrange.Count),
})
containerID = containerID + int(idrange.Count)
}
return idMap, nil
}
26 changes: 26 additions & 0 deletions internal/usergroup/lookup_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build !windows

package usergroup

import (
"testing"

"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)

func TestLookupUserAndGroupThatDoesNotExist(t *testing.T) {
fakeUser := "fakeuser"
_, err := LookupUser(fakeUser)
assert.Check(t, is.Error(err, `getent unable to find entry "fakeuser" in passwd database`))

_, err = LookupUID(-1)
assert.Check(t, is.ErrorContains(err, ""))

fakeGroup := "fakegroup"
_, err = LookupGroup(fakeGroup)
assert.Check(t, is.Error(err, `getent unable to find entry "fakegroup" in group database`))

_, err = LookupGID(-1)
assert.Check(t, is.ErrorContains(err, ""))
}
22 changes: 22 additions & 0 deletions internal/usergroup/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package usergroup

import (
"github.com/moby/sys/user"
)

const (
subuidFileName = "/etc/subuid"
subgidFileName = "/etc/subgid"
)

func parseSubuid(username string) ([]user.SubID, error) {
return user.ParseSubIDFileFilter(subuidFileName, func(sid user.SubID) bool {
return sid.Name == username
})
}

func parseSubgid(username string) ([]user.SubID, error) {
return user.ParseSubIDFileFilter(subgidFileName, func(sid user.SubID) bool {
return sid.Name == username
})
}
39 changes: 39 additions & 0 deletions internal/usergroup/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package usergroup

import (
"os"
"path/filepath"
"testing"

"github.com/moby/sys/user"
)

func TestParseSubidFileWithNewlinesAndComments(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "parsesubid")
if err != nil {
t.Fatal(err)
}
fnamePath := filepath.Join(tmpDir, "testsubuid")
fcontent := `tss:100000:65536
# empty default subuid/subgid file

dockremap:231072:65536`
if err := os.WriteFile(fnamePath, []byte(fcontent), 0o644); err != nil {
t.Fatal(err)
}
ranges, err := user.ParseSubIDFileFilter(fnamePath, func(sid user.SubID) bool {
return sid.Name == "dockremap"
})
if err != nil {
t.Fatal(err)
}
if len(ranges) != 1 {
t.Fatalf("wanted 1 element in ranges, got %d instead", len(ranges))
}
if ranges[0].SubID != 231072 {
t.Fatalf("wanted 231072, got %d instead", ranges[0].SubID)
}
if ranges[0].Count != 65536 {
t.Fatalf("wanted 65536, got %d instead", ranges[0].Count)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//go:build !windows

package idtools // import "github.com/docker/docker/pkg/idtools"
package usergroup

import (
"fmt"
Expand Down
Loading