| //go:build !windows | |
| // +build !windows | |
| package idtools // import "github.com/docker/docker/pkg/idtools" | |
| import ( | |
| "fmt" | |
| "os" | |
| "os/exec" | |
| "os/user" | |
| "path/filepath" | |
| "syscall" | |
| "testing" | |
| "golang.org/x/sys/unix" | |
| "gotest.tools/v3/assert" | |
| is "gotest.tools/v3/assert/cmp" | |
| "gotest.tools/v3/skip" | |
| ) | |
| const ( | |
| tempUser = "tempuser" | |
| ) | |
| type node struct { | |
| uid int | |
| gid int | |
| } | |
| func TestMkdirAllAndChown(t *testing.T) { | |
| RequiresRoot(t) | |
| dirName, err := os.MkdirTemp("", "mkdirall") | |
| if err != nil { | |
| t.Fatalf("Couldn't create temp dir: %v", err) | |
| } | |
| defer os.RemoveAll(dirName) | |
| testTree := map[string]node{ | |
| "usr": {0, 0}, | |
| "usr/bin": {0, 0}, | |
| "lib": {33, 33}, | |
| "lib/x86_64": {45, 45}, | |
| "lib/x86_64/share": {1, 1}, | |
| } | |
| if err := buildTree(dirName, testTree); err != nil { | |
| t.Fatal(err) | |
| } | |
| // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid | |
| if err := MkdirAllAndChown(filepath.Join(dirName, "usr", "share"), 0o755, Identity{UID: 99, GID: 99}); err != nil { | |
| t.Fatal(err) | |
| } | |
| testTree["usr/share"] = node{99, 99} | |
| verifyTree, err := readTree(dirName, "") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := compareTrees(testTree, verifyTree); err != nil { | |
| t.Fatal(err) | |
| } | |
| // test 2-deep new directories--both should be owned by the uid/gid pair | |
| if err := MkdirAllAndChown(filepath.Join(dirName, "lib", "some", "other"), 0o755, Identity{UID: 101, GID: 101}); err != nil { | |
| t.Fatal(err) | |
| } | |
| testTree["lib/some"] = node{101, 101} | |
| testTree["lib/some/other"] = node{101, 101} | |
| verifyTree, err = readTree(dirName, "") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := compareTrees(testTree, verifyTree); err != nil { | |
| t.Fatal(err) | |
| } | |
| // test a directory that already exists; should be chowned, but nothing else | |
| if err := MkdirAllAndChown(filepath.Join(dirName, "usr"), 0o755, Identity{UID: 102, GID: 102}); err != nil { | |
| t.Fatal(err) | |
| } | |
| testTree["usr"] = node{102, 102} | |
| verifyTree, err = readTree(dirName, "") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := compareTrees(testTree, verifyTree); err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| func TestMkdirAllAndChownNew(t *testing.T) { | |
| RequiresRoot(t) | |
| dirName, err := os.MkdirTemp("", "mkdirnew") | |
| assert.NilError(t, err) | |
| defer os.RemoveAll(dirName) | |
| testTree := map[string]node{ | |
| "usr": {0, 0}, | |
| "usr/bin": {0, 0}, | |
| "lib": {33, 33}, | |
| "lib/x86_64": {45, 45}, | |
| "lib/x86_64/share": {1, 1}, | |
| } | |
| assert.NilError(t, buildTree(dirName, testTree)) | |
| // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid | |
| err = MkdirAllAndChownNew(filepath.Join(dirName, "usr", "share"), 0o755, Identity{UID: 99, GID: 99}) | |
| assert.NilError(t, err) | |
| testTree["usr/share"] = node{99, 99} | |
| verifyTree, err := readTree(dirName, "") | |
| assert.NilError(t, err) | |
| assert.NilError(t, compareTrees(testTree, verifyTree)) | |
| // test 2-deep new directories--both should be owned by the uid/gid pair | |
| err = MkdirAllAndChownNew(filepath.Join(dirName, "lib", "some", "other"), 0o755, Identity{UID: 101, GID: 101}) | |
| assert.NilError(t, err) | |
| testTree["lib/some"] = node{101, 101} | |
| testTree["lib/some/other"] = node{101, 101} | |
| verifyTree, err = readTree(dirName, "") | |
| assert.NilError(t, err) | |
| assert.NilError(t, compareTrees(testTree, verifyTree)) | |
| // test a directory that already exists; should NOT be chowned | |
| err = MkdirAllAndChownNew(filepath.Join(dirName, "usr"), 0o755, Identity{UID: 102, GID: 102}) | |
| assert.NilError(t, err) | |
| verifyTree, err = readTree(dirName, "") | |
| assert.NilError(t, err) | |
| assert.NilError(t, compareTrees(testTree, verifyTree)) | |
| } | |
| func TestMkdirAllAndChownNewRelative(t *testing.T) { | |
| RequiresRoot(t) | |
| tests := []struct { | |
| in string | |
| out []string | |
| }{ | |
| { | |
| in: "dir1", | |
| out: []string{"dir1"}, | |
| }, | |
| { | |
| in: "dir2/subdir2", | |
| out: []string{"dir2", "dir2/subdir2"}, | |
| }, | |
| { | |
| in: "dir3/subdir3/", | |
| out: []string{"dir3", "dir3/subdir3"}, | |
| }, | |
| { | |
| in: "dir4/subdir4/.", | |
| out: []string{"dir4", "dir4/subdir4"}, | |
| }, | |
| { | |
| in: "dir5/././subdir5/", | |
| out: []string{"dir5", "dir5/subdir5"}, | |
| }, | |
| { | |
| in: "./dir6", | |
| out: []string{"dir6"}, | |
| }, | |
| { | |
| in: "./dir7/subdir7", | |
| out: []string{"dir7", "dir7/subdir7"}, | |
| }, | |
| { | |
| in: "./dir8/subdir8/", | |
| out: []string{"dir8", "dir8/subdir8"}, | |
| }, | |
| { | |
| in: "./dir9/subdir9/.", | |
| out: []string{"dir9", "dir9/subdir9"}, | |
| }, | |
| { | |
| in: "./dir10/././subdir10/", | |
| out: []string{"dir10", "dir10/subdir10"}, | |
| }, | |
| } | |
| // Set the current working directory to the temp-dir, as we're | |
| // testing relative paths. | |
| tmpDir := t.TempDir() | |
| setWorkingDirectory(t, tmpDir) | |
| const expectedUIDGID = 101 | |
| for _, tc := range tests { | |
| tc := tc | |
| t.Run(tc.in, func(t *testing.T) { | |
| for _, p := range tc.out { | |
| _, err := os.Stat(p) | |
| assert.ErrorIs(t, err, os.ErrNotExist) | |
| } | |
| err := MkdirAllAndChownNew(tc.in, 0o755, Identity{UID: expectedUIDGID, GID: expectedUIDGID}) | |
| assert.Check(t, err) | |
| for _, p := range tc.out { | |
| s := &unix.Stat_t{} | |
| err = unix.Stat(p, s) | |
| if assert.Check(t, err) { | |
| assert.Check(t, is.Equal(uint64(s.Uid), uint64(expectedUIDGID))) | |
| assert.Check(t, is.Equal(uint64(s.Gid), uint64(expectedUIDGID))) | |
| } | |
| } | |
| }) | |
| } | |
| } | |
| // Change the current working directory for the duration of the test. This may | |
| // break if tests are run in parallel. | |
| func setWorkingDirectory(t *testing.T, dir string) { | |
| t.Helper() | |
| cwd, err := os.Getwd() | |
| assert.NilError(t, err) | |
| t.Cleanup(func() { | |
| assert.NilError(t, os.Chdir(cwd)) | |
| }) | |
| err = os.Chdir(dir) | |
| assert.NilError(t, err) | |
| } | |
| func TestMkdirAndChown(t *testing.T) { | |
| RequiresRoot(t) | |
| dirName, err := os.MkdirTemp("", "mkdir") | |
| if err != nil { | |
| t.Fatalf("Couldn't create temp dir: %v", err) | |
| } | |
| defer os.RemoveAll(dirName) | |
| testTree := map[string]node{ | |
| "usr": {0, 0}, | |
| } | |
| if err := buildTree(dirName, testTree); err != nil { | |
| t.Fatal(err) | |
| } | |
| // test a directory that already exists; should just chown to the requested uid/gid | |
| if err := MkdirAndChown(filepath.Join(dirName, "usr"), 0o755, Identity{UID: 99, GID: 99}); err != nil { | |
| t.Fatal(err) | |
| } | |
| testTree["usr"] = node{99, 99} | |
| verifyTree, err := readTree(dirName, "") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := compareTrees(testTree, verifyTree); err != nil { | |
| t.Fatal(err) | |
| } | |
| // create a subdir under a dir which doesn't exist--should fail | |
| if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin", "subdir"), 0o755, Identity{UID: 102, GID: 102}); err == nil { | |
| t.Fatalf("Trying to create a directory with Mkdir where the parent doesn't exist should have failed") | |
| } | |
| // create a subdir under an existing dir; should only change the ownership of the new subdir | |
| if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin"), 0o755, Identity{UID: 102, GID: 102}); err != nil { | |
| t.Fatal(err) | |
| } | |
| testTree["usr/bin"] = node{102, 102} | |
| verifyTree, err = readTree(dirName, "") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := compareTrees(testTree, verifyTree); err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| func buildTree(base string, tree map[string]node) error { | |
| for path, node := range tree { | |
| fullPath := filepath.Join(base, path) | |
| if err := os.MkdirAll(fullPath, 0o755); err != nil { | |
| return fmt.Errorf("couldn't create path: %s; error: %v", fullPath, err) | |
| } | |
| if err := os.Chown(fullPath, node.uid, node.gid); err != nil { | |
| return fmt.Errorf("couldn't chown path: %s; error: %v", fullPath, err) | |
| } | |
| } | |
| return nil | |
| } | |
| func readTree(base, root string) (map[string]node, error) { | |
| tree := make(map[string]node) | |
| dirInfos, err := os.ReadDir(base) | |
| if err != nil { | |
| return nil, fmt.Errorf("couldn't read directory entries for %q: %v", base, err) | |
| } | |
| for _, info := range dirInfos { | |
| s := &unix.Stat_t{} | |
| if err := unix.Stat(filepath.Join(base, info.Name()), s); err != nil { | |
| return nil, fmt.Errorf("can't stat file %q: %v", filepath.Join(base, info.Name()), err) | |
| } | |
| tree[filepath.Join(root, info.Name())] = node{int(s.Uid), int(s.Gid)} | |
| if info.IsDir() { | |
| // read the subdirectory | |
| subtree, err := readTree(filepath.Join(base, info.Name()), filepath.Join(root, info.Name())) | |
| if err != nil { | |
| return nil, err | |
| } | |
| for path, nodeinfo := range subtree { | |
| tree[path] = nodeinfo | |
| } | |
| } | |
| } | |
| return tree, nil | |
| } | |
| func compareTrees(left, right map[string]node) error { | |
| if len(left) != len(right) { | |
| return fmt.Errorf("trees aren't the same size") | |
| } | |
| for path, nodeLeft := range left { | |
| if nodeRight, ok := right[path]; ok { | |
| if nodeRight.uid != nodeLeft.uid || nodeRight.gid != nodeLeft.gid { | |
| // mismatch | |
| return fmt.Errorf("mismatched ownership for %q: expected: %d:%d, got: %d:%d", path, | |
| nodeLeft.uid, nodeLeft.gid, nodeRight.uid, nodeRight.gid) | |
| } | |
| continue | |
| } | |
| return fmt.Errorf("right tree didn't contain path %q", path) | |
| } | |
| return nil | |
| } | |
| func delUser(t *testing.T, name string) { | |
| out, err := exec.Command("userdel", name).CombinedOutput() | |
| assert.Check(t, err, out) | |
| } | |
| 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 := parseSubidFile(fnamePath, "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].Start != 231072 { | |
| t.Fatalf("wanted 231072, got %d instead", ranges[0].Start) | |
| } | |
| if ranges[0].Length != 65536 { | |
| t.Fatalf("wanted 65536, got %d instead", ranges[0].Length) | |
| } | |
| } | |
| func TestGetRootUIDGID(t *testing.T) { | |
| uidMap := []IDMap{ | |
| { | |
| ContainerID: 0, | |
| HostID: os.Getuid(), | |
| Size: 1, | |
| }, | |
| } | |
| gidMap := []IDMap{ | |
| { | |
| ContainerID: 0, | |
| HostID: os.Getgid(), | |
| Size: 1, | |
| }, | |
| } | |
| uid, gid, err := GetRootUIDGID(uidMap, gidMap) | |
| assert.Check(t, err) | |
| assert.Check(t, is.Equal(os.Geteuid(), uid)) | |
| assert.Check(t, is.Equal(os.Getegid(), gid)) | |
| uidMapError := []IDMap{ | |
| { | |
| ContainerID: 1, | |
| HostID: os.Getuid(), | |
| Size: 1, | |
| }, | |
| } | |
| _, _, err = GetRootUIDGID(uidMapError, gidMap) | |
| assert.Check(t, is.Error(err, "Container ID 0 cannot be mapped to a host ID")) | |
| } | |
| func TestToContainer(t *testing.T) { | |
| uidMap := []IDMap{ | |
| { | |
| ContainerID: 2, | |
| HostID: 2, | |
| Size: 1, | |
| }, | |
| } | |
| containerID, err := toContainer(2, uidMap) | |
| assert.Check(t, err) | |
| assert.Check(t, is.Equal(uidMap[0].ContainerID, containerID)) | |
| } | |
| func TestNewIDMappings(t *testing.T) { | |
| RequiresRoot(t) | |
| _, _, 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 := 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 = MkdirAllAndChown(dirName, 0o700, 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) { | |
| RequiresRoot(t) | |
| 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 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, "")) | |
| } | |
| // TestMkdirIsNotDir checks that mkdirAs() function (used by MkdirAll...) | |
| // returns a correct error in case a directory which it is about to create | |
| // already exists but is a file (rather than a directory). | |
| func TestMkdirIsNotDir(t *testing.T) { | |
| file, err := os.CreateTemp("", t.Name()) | |
| if err != nil { | |
| t.Fatalf("Couldn't create temp dir: %v", err) | |
| } | |
| defer os.Remove(file.Name()) | |
| err = mkdirAs(file.Name(), 0o755, Identity{UID: 0, GID: 0}, false, false) | |
| assert.Check(t, is.Error(err, "mkdir "+file.Name()+": not a directory")) | |
| } | |
| func RequiresRoot(t *testing.T) { | |
| skip.If(t, os.Getuid() != 0, "skipping test that requires root") | |
| } |