| //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") |
| } |