Skip to content

Commit 9c368a9

Browse files
committed
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 <[email protected]> Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent 8260f98 commit 9c368a9

13 files changed

Lines changed: 383 additions & 254 deletions
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package idtools // import "github.com/docker/docker/pkg/idtools"
1+
package usergroup
22

33
import (
44
"fmt"
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package usergroup
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"os/user"
7+
"syscall"
8+
"testing"
9+
10+
"github.com/docker/docker/pkg/idtools"
11+
"gotest.tools/v3/assert"
12+
is "gotest.tools/v3/assert/cmp"
13+
"gotest.tools/v3/skip"
14+
)
15+
16+
const (
17+
tempUser = "tempuser"
18+
)
19+
20+
func TestNewIDMappings(t *testing.T) {
21+
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
22+
_, _, err := AddNamespaceRangesUser(tempUser)
23+
assert.Check(t, err)
24+
defer delUser(t, tempUser)
25+
26+
tempUser, err := user.Lookup(tempUser)
27+
assert.Check(t, err)
28+
29+
idMapping, err := LoadIdentityMapping(tempUser.Username)
30+
assert.Check(t, err)
31+
32+
rootUID, rootGID, err := idtools.GetRootUIDGID(idMapping.UIDMaps, idMapping.GIDMaps)
33+
assert.Check(t, err)
34+
35+
dirName, err := os.MkdirTemp("", "mkdirall")
36+
assert.Check(t, err, "Couldn't create temp directory")
37+
defer os.RemoveAll(dirName)
38+
39+
err = idtools.MkdirAllAndChown(dirName, 0o700, idtools.Identity{UID: rootUID, GID: rootGID})
40+
assert.Check(t, err, "Couldn't change ownership of file path. Got error")
41+
cmd := exec.Command("ls", "-la", dirName)
42+
cmd.SysProcAttr = &syscall.SysProcAttr{
43+
Credential: &syscall.Credential{Uid: uint32(rootUID), Gid: uint32(rootGID)},
44+
}
45+
out, err := cmd.CombinedOutput()
46+
assert.Check(t, err, "Unable to access %s directory with user UID:%d and GID:%d:\n%s", dirName, rootUID, rootGID, string(out))
47+
}
48+
49+
func TestLookupUserAndGroup(t *testing.T) {
50+
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
51+
uid, gid, err := AddNamespaceRangesUser(tempUser)
52+
assert.Check(t, err)
53+
defer delUser(t, tempUser)
54+
55+
fetchedUser, err := LookupUser(tempUser)
56+
assert.Check(t, err)
57+
58+
fetchedUserByID, err := LookupUID(uid)
59+
assert.Check(t, err)
60+
assert.Check(t, is.DeepEqual(fetchedUserByID, fetchedUser))
61+
62+
fetchedGroup, err := LookupGroup(tempUser)
63+
assert.Check(t, err)
64+
65+
fetchedGroupByID, err := LookupGID(gid)
66+
assert.Check(t, err)
67+
assert.Check(t, is.DeepEqual(fetchedGroupByID, fetchedGroup))
68+
}
69+
70+
func delUser(t *testing.T, name string) {
71+
out, err := exec.Command("userdel", name).CombinedOutput()
72+
assert.Check(t, err, out)
73+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//go:build !linux
22

3-
package idtools // import "github.com/docker/docker/pkg/idtools"
3+
package usergroup
44

55
import "fmt"
66

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package usergroup
2+
3+
const (
4+
SeTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege"
5+
)
6+
7+
const (
8+
ContainerAdministratorSidString = "S-1-5-93-2-1"
9+
ContainerUserSidString = "S-1-5-93-2-2"
10+
)

internal/usergroup/lookup_unix.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//go:build !windows
2+
3+
package usergroup
4+
5+
import (
6+
"bytes"
7+
"fmt"
8+
"io"
9+
"os/exec"
10+
"strconv"
11+
"syscall"
12+
13+
"github.com/docker/docker/pkg/idtools"
14+
"github.com/moby/sys/user"
15+
)
16+
17+
// LookupUser uses traditional local system files lookup (from libcontainer/user) on a username,
18+
// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
19+
func LookupUser(name string) (user.User, error) {
20+
// first try a local system files lookup using existing capabilities
21+
usr, err := user.LookupUser(name)
22+
if err == nil {
23+
return usr, nil
24+
}
25+
// local files lookup failed; attempt to call `getent` to query configured passwd dbs
26+
usr, err = getentUser(name)
27+
if err != nil {
28+
return user.User{}, err
29+
}
30+
return usr, nil
31+
}
32+
33+
// LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid,
34+
// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
35+
func LookupUID(uid int) (user.User, error) {
36+
// first try a local system files lookup using existing capabilities
37+
usr, err := user.LookupUid(uid)
38+
if err == nil {
39+
return usr, nil
40+
}
41+
// local files lookup failed; attempt to call `getent` to query configured passwd dbs
42+
return getentUser(strconv.Itoa(uid))
43+
}
44+
45+
func getentUser(name string) (user.User, error) {
46+
reader, err := callGetent("passwd", name)
47+
if err != nil {
48+
return user.User{}, err
49+
}
50+
users, err := user.ParsePasswd(reader)
51+
if err != nil {
52+
return user.User{}, err
53+
}
54+
if len(users) == 0 {
55+
return user.User{}, fmt.Errorf("getent failed to find passwd entry for %q", name)
56+
}
57+
return users[0], nil
58+
}
59+
60+
// LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name,
61+
// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
62+
func LookupGroup(name string) (user.Group, error) {
63+
// first try a local system files lookup using existing capabilities
64+
group, err := user.LookupGroup(name)
65+
if err == nil {
66+
return group, nil
67+
}
68+
// local files lookup failed; attempt to call `getent` to query configured group dbs
69+
return getentGroup(name)
70+
}
71+
72+
// LookupGID uses traditional local system files lookup (from libcontainer/user) on a group ID,
73+
// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
74+
func LookupGID(gid int) (user.Group, error) {
75+
// first try a local system files lookup using existing capabilities
76+
group, err := user.LookupGid(gid)
77+
if err == nil {
78+
return group, nil
79+
}
80+
// local files lookup failed; attempt to call `getent` to query configured group dbs
81+
return getentGroup(strconv.Itoa(gid))
82+
}
83+
84+
func getentGroup(name string) (user.Group, error) {
85+
reader, err := callGetent("group", name)
86+
if err != nil {
87+
return user.Group{}, err
88+
}
89+
groups, err := user.ParseGroup(reader)
90+
if err != nil {
91+
return user.Group{}, err
92+
}
93+
if len(groups) == 0 {
94+
return user.Group{}, fmt.Errorf("getent failed to find groups entry for %q", name)
95+
}
96+
return groups[0], nil
97+
}
98+
99+
func callGetent(database, key string) (io.Reader, error) {
100+
getentCmd, err := resolveBinary("getent")
101+
// if no `getent` command within the execution environment, can't do anything else
102+
if err != nil {
103+
return nil, fmt.Errorf("unable to find getent command: %w", err)
104+
}
105+
command := exec.Command(getentCmd, database, key)
106+
// we run getent within container filesystem, but without /dev so /dev/null is not available for exec to mock stdin
107+
command.Stdin = io.NopCloser(bytes.NewReader(nil))
108+
out, err := command.CombinedOutput()
109+
if err != nil {
110+
exitCode, errC := getExitCode(err)
111+
if errC != nil {
112+
return nil, err
113+
}
114+
switch exitCode {
115+
case 1:
116+
return nil, fmt.Errorf("getent reported invalid parameters/database unknown")
117+
case 2:
118+
return nil, fmt.Errorf("getent unable to find entry %q in %s database", key, database)
119+
case 3:
120+
return nil, fmt.Errorf("getent database doesn't support enumeration")
121+
default:
122+
return nil, err
123+
}
124+
}
125+
return bytes.NewReader(out), nil
126+
}
127+
128+
// getExitCode returns the ExitStatus of the specified error if its type is
129+
// exec.ExitError, returns 0 and an error otherwise.
130+
func getExitCode(err error) (int, error) {
131+
exitCode := 0
132+
if exiterr, ok := err.(*exec.ExitError); ok {
133+
if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok {
134+
return procExit.ExitStatus(), nil
135+
}
136+
}
137+
return exitCode, fmt.Errorf("failed to get exit code")
138+
}
139+
140+
// LoadIdentityMapping takes a requested username and
141+
// using the data from /etc/sub{uid,gid} ranges, creates the
142+
// proper uid and gid remapping ranges for that user/group pair
143+
func LoadIdentityMapping(name string) (idtools.IdentityMapping, error) {
144+
usr, err := LookupUser(name)
145+
if err != nil {
146+
return idtools.IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err)
147+
}
148+
149+
subuidRanges, err := lookupSubRangesFile("/etc/subuid", usr)
150+
if err != nil {
151+
return idtools.IdentityMapping{}, err
152+
}
153+
subgidRanges, err := lookupSubRangesFile("/etc/subgid", usr)
154+
if err != nil {
155+
return idtools.IdentityMapping{}, err
156+
}
157+
158+
return idtools.IdentityMapping{
159+
UIDMaps: subuidRanges,
160+
GIDMaps: subgidRanges,
161+
}, nil
162+
}
163+
164+
func lookupSubRangesFile(path string, usr user.User) ([]idtools.IDMap, error) {
165+
uidstr := strconv.Itoa(usr.Uid)
166+
rangeList, err := user.ParseSubIDFileFilter(path, func(sid user.SubID) bool {
167+
return sid.Name == usr.Name || sid.Name == uidstr
168+
})
169+
if err != nil {
170+
return nil, err
171+
}
172+
if len(rangeList) == 0 {
173+
return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name)
174+
}
175+
176+
idMap := []idtools.IDMap{}
177+
178+
containerID := 0
179+
for _, idrange := range rangeList {
180+
idMap = append(idMap, idtools.IDMap{
181+
ContainerID: containerID,
182+
HostID: int(idrange.SubID),
183+
Size: int(idrange.Count),
184+
})
185+
containerID = containerID + int(idrange.Count)
186+
}
187+
return idMap, nil
188+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//go:build !windows
2+
3+
package usergroup
4+
5+
import (
6+
"testing"
7+
8+
"gotest.tools/v3/assert"
9+
is "gotest.tools/v3/assert/cmp"
10+
)
11+
12+
func TestLookupUserAndGroupThatDoesNotExist(t *testing.T) {
13+
fakeUser := "fakeuser"
14+
_, err := LookupUser(fakeUser)
15+
assert.Check(t, is.Error(err, `getent unable to find entry "fakeuser" in passwd database`))
16+
17+
_, err = LookupUID(-1)
18+
assert.Check(t, is.ErrorContains(err, ""))
19+
20+
fakeGroup := "fakegroup"
21+
_, err = LookupGroup(fakeGroup)
22+
assert.Check(t, is.Error(err, `getent unable to find entry "fakegroup" in group database`))
23+
24+
_, err = LookupGID(-1)
25+
assert.Check(t, is.ErrorContains(err, ""))
26+
}

internal/usergroup/parser.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package usergroup
2+
3+
import (
4+
"github.com/moby/sys/user"
5+
)
6+
7+
const (
8+
subuidFileName = "/etc/subuid"
9+
subgidFileName = "/etc/subgid"
10+
)
11+
12+
func parseSubuid(username string) ([]user.SubID, error) {
13+
return user.ParseSubIDFileFilter(subuidFileName, func(sid user.SubID) bool {
14+
return sid.Name == username
15+
})
16+
}
17+
18+
func parseSubgid(username string) ([]user.SubID, error) {
19+
return user.ParseSubIDFileFilter(subgidFileName, func(sid user.SubID) bool {
20+
return sid.Name == username
21+
})
22+
}

internal/usergroup/parser_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package usergroup
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/moby/sys/user"
9+
)
10+
11+
func TestParseSubidFileWithNewlinesAndComments(t *testing.T) {
12+
tmpDir, err := os.MkdirTemp("", "parsesubid")
13+
if err != nil {
14+
t.Fatal(err)
15+
}
16+
fnamePath := filepath.Join(tmpDir, "testsubuid")
17+
fcontent := `tss:100000:65536
18+
# empty default subuid/subgid file
19+
20+
dockremap:231072:65536`
21+
if err := os.WriteFile(fnamePath, []byte(fcontent), 0o644); err != nil {
22+
t.Fatal(err)
23+
}
24+
ranges, err := user.ParseSubIDFileFilter(fnamePath, func(sid user.SubID) bool {
25+
return sid.Name == "dockremap"
26+
})
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
if len(ranges) != 1 {
31+
t.Fatalf("wanted 1 element in ranges, got %d instead", len(ranges))
32+
}
33+
if ranges[0].SubID != 231072 {
34+
t.Fatalf("wanted 231072, got %d instead", ranges[0].SubID)
35+
}
36+
if ranges[0].Count != 65536 {
37+
t.Fatalf("wanted 65536, got %d instead", ranges[0].Count)
38+
}
39+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//go:build !windows
22

3-
package idtools // import "github.com/docker/docker/pkg/idtools"
3+
package usergroup
44

55
import (
66
"fmt"

0 commit comments

Comments
 (0)