diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bf0e18e9..821a762e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,14 +43,7 @@ jobs: - name: Test run: | - go test -c ./internals/daemon - PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./daemon.test -check.v -check.f ^execSuite\.TestUserGroup$ - PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./daemon.test -check.v -check.f ^execSuite\.TestUserIDGroupID$ - PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./daemon.test -check.v -check.f ^filesSuite\.TestWriteUserGroupReal$ - PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./daemon.test -check.v -check.f ^filesSuite\.TestMakeDirsUserGroupReal$ - go test -c ./internals/osutil - PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./osutil.test -check.v -check.f ^mkdirSuite\.TestMakeParentsChmodAndChown$ - + PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H go test -count=1 -tags=roottest -run=TestWithRoot ./... go test -c ./internals/overlord/servstate/ PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./servstate.test -check.v -check.f ^S.TestUserGroup$ diff --git a/internals/daemon/api_exec_root_test.go b/internals/daemon/api_exec_root_test.go new file mode 100644 index 00000000..a1584325 --- /dev/null +++ b/internals/daemon/api_exec_root_test.go @@ -0,0 +1,179 @@ +//go:build roottest + +// Copyright (c) 2024 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package daemon + +import ( + "bytes" + "fmt" + "log" + "os" + "os/user" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/canonical/pebble/client" + "github.com/canonical/pebble/internals/reaper" +) + +var rootTestDaemon *Daemon +var rootTestPebbleClient *client.Client + +func TestMain(m *testing.M) { + err := reaper.Start() + if err != nil { + fmt.Printf("cannot start reaper: %v", err) + os.Exit(1) + } + tmpDir, err := os.MkdirTemp("", "pebble") + if err != nil { + fmt.Printf("cannot create temporary directory: %v", err) + os.Exit(1) + } + socketPath := tmpDir + ".pebble.socket" + rootTestDaemon, err := New(&Options{ + Dir: tmpDir, + SocketPath: socketPath, + }) + if err != nil { + fmt.Printf("cannot create daemon: %v", err) + os.Exit(1) + } + err = rootTestDaemon.Init() + if err != nil { + fmt.Printf("cannot init daemon: %v", err) + os.Exit(1) + } + rootTestDaemon.Start() + rootTestPebbleClient, err = client.New(&client.Config{Socket: socketPath}) + if err != nil { + fmt.Printf("cannot create client: %v", err) + os.Exit(1) + } + + exitCode := m.Run() + + err = rootTestDaemon.Stop(nil) + if err != nil { + fmt.Printf("cannot stop daemon: %v", err) + os.Exit(1) + } + err = reaper.Stop() + if err != nil { + fmt.Printf("cannot stop reaper: %v", err) + os.Exit(1) + } + err = os.RemoveAll(tmpDir) + if err != nil { + log.Fatalf("cannot remove temporary directory: %v", err) + } + + os.Exit(exitCode) +} + +func TestWithRootUserGroup(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("requires running as root") + } + username := os.Getenv("PEBBLE_TEST_USER") + group := os.Getenv("PEBBLE_TEST_GROUP") + if username == "" || group == "" { + t.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP") + } + stdout, stderr := pebbleExec(t, "", &client.ExecOptions{ + Command: []string{"/bin/sh", "-c", "id -n -u && id -n -g"}, + User: username, + Group: group, + }) + expectedStdout := username + "\n" + group + "\n" + if stdout != expectedStdout { + t.Fatalf("pebble exec stdout error, expected: %v, got %v", expectedStdout, stdout) + } + if stderr != "" { + t.Fatalf("pebble exec stderr is not empty: %v", stderr) + } + + _, err := rootTestPebbleClient.Exec(&client.ExecOptions{ + Command: []string{"pwd"}, + Environment: map[string]string{"HOME": "/non/existent"}, + User: username, + Group: group, + }) + // c.Assert(err, ErrorMatches, `.*home directory.*does not exist`) + if matched, _ := regexp.MatchString(`.*home directory.*does not exist`, err.Error()); !matched { + t.Errorf("Error message doesn't match, expected: %v, got: %v", `.*home directory.*does not exist`, err.Error()) + } +} + +func TestWithRootUserIDGroupID(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("requires running as root") + } + username := os.Getenv("PEBBLE_TEST_USER") + group := os.Getenv("PEBBLE_TEST_GROUP") + if username == "" || group == "" { + t.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP") + } + u, err := user.Lookup(username) + if err != nil { + t.Fatalf("cannot look up username: %v", err) + } + g, err := user.LookupGroup(group) + if err != nil { + t.Fatalf("cannot look up group: %v", err) + } + uid, err := strconv.Atoi(u.Uid) + if err != nil { + t.Fatalf("cannot convert uid to int: %v", err) + } + gid, err := strconv.Atoi(g.Gid) + if err != nil { + t.Fatalf("cannot convert gid to int: %v", err) + } + stdout, stderr := pebbleExec(t, "", &client.ExecOptions{ + Command: []string{"/bin/sh", "-c", "id -n -u && id -n -g"}, + UserID: &uid, + GroupID: &gid, + }) + expectedStdout := username + "\n" + group + "\n" + if stdout != expectedStdout { + t.Fatalf("pebble exec stdout error, expected: %v, got %v", expectedStdout, stdout) + } + if stderr != "" { + t.Fatalf("pebble exec stderr is not empty: %v", stderr) + } +} + +func pebbleExec(t *testing.T, stdin string, opts *client.ExecOptions) (stdout, stderr string) { + t.Helper() + + outBuf := &bytes.Buffer{} + errBuf := &bytes.Buffer{} + opts.Stdin = strings.NewReader(stdin) + opts.Stdout = outBuf + opts.Stderr = errBuf + process, err := rootTestPebbleClient.Exec(opts) + if err != nil { + t.Fatalf("pebble exec failed: %v", err) + } + + if waitErr := process.Wait(); waitErr != nil { + t.Fatalf("pebble exec process wait error: %v", waitErr) + } + return outBuf.String(), errBuf.String() +} diff --git a/internals/daemon/api_exec_test.go b/internals/daemon/api_exec_test.go index 23ede9ae..a7263837 100644 --- a/internals/daemon/api_exec_test.go +++ b/internals/daemon/api_exec_test.go @@ -23,7 +23,6 @@ import ( "os" "os/user" "path/filepath" - "strconv" "strings" "time" @@ -258,62 +257,6 @@ func (s *execSuite) TestCurrentUserGroup(c *C) { c.Check(stderr, Equals, "") } -// See .github/workflows/tests.yml for how to run this test as root. -func (s *execSuite) TestUserGroup(c *C) { - if os.Getuid() != 0 { - c.Skip("requires running as root") - } - username := os.Getenv("PEBBLE_TEST_USER") - group := os.Getenv("PEBBLE_TEST_GROUP") - if username == "" || group == "" { - c.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP") - } - stdout, stderr, waitErr := s.exec(c, "", &client.ExecOptions{ - Command: []string{"/bin/sh", "-c", "id -n -u && id -n -g"}, - User: username, - Group: group, - }) - c.Assert(waitErr, IsNil) - c.Check(stdout, Equals, username+"\n"+group+"\n") - c.Check(stderr, Equals, "") - - _, err := s.client.Exec(&client.ExecOptions{ - Command: []string{"pwd"}, - Environment: map[string]string{"HOME": "/non/existent"}, - User: username, - Group: group, - }) - c.Assert(err, ErrorMatches, `.*home directory.*does not exist`) -} - -// See .github/workflows/tests.yml for how to run this test as root. -func (s *execSuite) TestUserIDGroupID(c *C) { - if os.Getuid() != 0 { - c.Skip("requires running as root") - } - username := os.Getenv("PEBBLE_TEST_USER") - group := os.Getenv("PEBBLE_TEST_GROUP") - if username == "" || group == "" { - c.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP") - } - u, err := user.Lookup(username) - c.Assert(err, IsNil) - g, err := user.LookupGroup(group) - c.Assert(err, IsNil) - uid, err := strconv.Atoi(u.Uid) - c.Assert(err, IsNil) - gid, err := strconv.Atoi(g.Gid) - c.Assert(err, IsNil) - stdout, stderr, waitErr := s.exec(c, "", &client.ExecOptions{ - Command: []string{"/bin/sh", "-c", "id -n -u && id -n -g"}, - UserID: &uid, - GroupID: &gid, - }) - c.Assert(waitErr, IsNil) - c.Check(stdout, Equals, username+"\n"+group+"\n") - c.Check(stderr, Equals, "") -} - func (s *execSuite) exec(c *C, stdin string, opts *client.ExecOptions) (stdout, stderr string, waitErr error) { outBuf := &bytes.Buffer{} errBuf := &bytes.Buffer{} diff --git a/internals/daemon/api_files_root_test.go b/internals/daemon/api_files_root_test.go new file mode 100644 index 00000000..c9bf878e --- /dev/null +++ b/internals/daemon/api_files_root_test.go @@ -0,0 +1,480 @@ +//go:build roottest + +// Copyright (c) 2024 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package daemon + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "os/user" + "strconv" + "syscall" + "testing" + + "github.com/canonical/pebble/internals/osutil" +) + +func TestWithRootMakeDirsUserGroupReal(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("requires running as root") + } + username := os.Getenv("PEBBLE_TEST_USER") + group := os.Getenv("PEBBLE_TEST_GROUP") + if username == "" || group == "" { + t.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP") + } + u, err := user.Lookup(username) + if err != nil { + t.Fatalf("cannot look up username: %v", err) + } + g, err := user.LookupGroup(group) + if err != nil { + t.Fatalf("cannot look up group: %v", err) + } + uid, err := strconv.Atoi(u.Uid) + if err != nil { + t.Fatalf("cannot convert uid to int: %v", err) + } + gid, err := strconv.Atoi(g.Gid) + if err != nil { + t.Fatalf("cannot convert gid to int: %v", err) + } + + tmpDir := testWithRootMakeDirsUserGroup(t, uid, gid, username, group) + + info, err := os.Stat(tmpDir + "/normal") + if err != nil { + t.Fatalf("cannot stat dir %s: %v", tmpDir+"/normal", err) + } + statT := info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(0) { + t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/normal", uint32(0), statT.Uid) + } + if statT.Gid != uint32(0) { + t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/normal", uint32(0), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/uid-gid") + if err != nil { + t.Fatalf("cannot stat dir %s: %v", tmpDir+"/uid-gid", err) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(uid) { + t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/uid-gid", uint32(uid), statT.Uid) + } + if statT.Gid != uint32(gid) { + t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/uid-gid", uint32(gid), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/user-group") + if err != nil { + t.Fatalf("cannot stat dir %s: %v", tmpDir+"/user-group", err) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(uid) { + t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/user-group", uint32(uid), statT.Uid) + } + if statT.Gid != uint32(gid) { + t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/user-group", uint32(gid), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/nested1") + if err != nil { + t.Fatalf("cannot stat dir %s: %v", tmpDir+"/nested1", err) + } + if int(info.Mode()&os.ModePerm) != 0o755 { + t.Fatalf("dir %s mode error, expected: %v, got: %v", tmpDir+"/nested1", 0o755, int(info.Mode()&os.ModePerm)) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(0) { + t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/nested1", uint32(0), statT.Uid) + } + if statT.Gid != uint32(0) { + t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/nested1", uint32(0), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/nested1/normal") + if err != nil { + t.Fatalf("cannot stat dir %s: %v", tmpDir+"/nested1/normal", err) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(0) { + t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/nested1/normal", uint32(0), statT.Uid) + } + if statT.Gid != uint32(0) { + t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/nested1/normal", uint32(0), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/nested2") + if err != nil { + t.Fatalf("cannot stat dir %s: %v", tmpDir+"/nested2", err) + } + if int(info.Mode()&os.ModePerm) != 0o755 { + t.Fatalf("dir %s mode error, expected: %v, got: %v", tmpDir+"/nested2", 0o755, int(info.Mode()&os.ModePerm)) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(uid) { + t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/nested2", uint32(uid), statT.Uid) + } + if statT.Gid != uint32(gid) { + t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/nested2", uint32(gid), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/nested2/user-group") + if err != nil { + t.Fatalf("cannot stat dir %s: %v", tmpDir+"/nested2/user-group", err) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(uid) { + t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/nested2/user-group", uint32(uid), statT.Uid) + } + if statT.Gid != uint32(gid) { + t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/nested2/user-group", uint32(gid), statT.Uid) + } +} + +func testWithRootMakeDirsUserGroup(t *testing.T, uid, gid int, user, group string) string { + tmpDir := t.TempDir() + + headers := http.Header{ + "Content-Type": []string{"application/json"}, + } + payload := struct { + Action string + Dirs []makeDirsItem + }{ + Action: "make-dirs", + Dirs: []makeDirsItem{ + {Path: tmpDir + "/normal"}, + {Path: tmpDir + "/uid-gid", UserID: &uid, GroupID: &gid}, + {Path: tmpDir + "/user-group", User: user, Group: group}, + {Path: tmpDir + "/nested1/normal", MakeParents: true}, + {Path: tmpDir + "/nested2/user-group", User: user, Group: group, MakeParents: true}, + }, + } + reqBody, err := json.Marshal(payload) + if err != nil { + t.Fatalf("cannot marshal payload: %v", err) + } + body := doRequestRootTest(t, v1PostFiles, "POST", "/v1/files", nil, headers, reqBody) + + var r testFilesResponse + if err := json.NewDecoder(body).Decode(&r); err != nil { + t.Fatalf("cannot decode response body for /v1/files: %v", err) + } + if r.StatusCode != http.StatusOK { + t.Fatalf("test file response status code error, expected: %v, got %v", http.StatusOK, r.StatusCode) + + } + if r.Type != "sync" { + t.Fatalf("test file response type error, expected: sync, got %v", r.StatusCode) + + } + if len(r.Result) != 5 { + t.Fatalf("test file response result length error, expected: 5, got %v", len(r.Result)) + + } + checkFileResultRootTest(t, r.Result[0], tmpDir+"/normal", "", "") + checkFileResultRootTest(t, r.Result[1], tmpDir+"/uid-gid", "", "") + checkFileResultRootTest(t, r.Result[2], tmpDir+"/user-group", "", "") + checkFileResultRootTest(t, r.Result[3], tmpDir+"/nested1/normal", "", "") + checkFileResultRootTest(t, r.Result[4], tmpDir+"/nested2/user-group", "", "") + + if !osutil.IsDir(tmpDir + "/normal") { + t.Fatalf("file %s is not a directory", tmpDir+"/normal") + + } + if !osutil.IsDir(tmpDir + "/uid-gid") { + t.Fatalf("file %s is not a directory", tmpDir+"/uid-gid") + + } + if !osutil.IsDir(tmpDir + "/user-group") { + t.Fatalf("file %s is not a directory", tmpDir+"/user-group") + + } + if !osutil.IsDir(tmpDir + "/nested1/normal") { + t.Fatalf("file %s is not a directory", tmpDir+"/nested1/normal") + + } + if !osutil.IsDir(tmpDir + "/nested2/user-group") { + t.Fatalf("file %s is not a directory", tmpDir+"/nested2/user-group") + + } + + return tmpDir +} + +func doRequestRootTest(t *testing.T, f ResponseFunc, method, url string, query url.Values, headers http.Header, body []byte) *bytes.Buffer { + t.Helper() + + var bodyReader io.Reader + if body != nil { + bodyReader = bytes.NewBuffer(body) + } + req, err := http.NewRequest(method, url, bodyReader) + if err != nil { + t.Fatalf("http request error: %s", err) + } + if query != nil { + req.URL.RawQuery = query.Encode() + } + req.Header = headers + handler := f(apiCmd(url), req, nil) + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + response := recorder.Result() + if response.StatusCode != http.StatusOK { + t.Fatalf("http request to %s failed: %v", url, err) + } + return recorder.Body +} + +func checkFileResultRootTest(t *testing.T, r testFileResult, path, errorKind, errorMsg string) { + t.Helper() + + if r.Path != path { + t.Fatalf("error checking test file path, eexpected: %v, got: %v", path, r.Path) + } + if r.Error.Kind != errorKind { + t.Fatalf("error checking test file error kind, eexpected: %v, got: %v", errorKind, r.Error.Kind) + } + if r.Error.Message != errorMsg { + t.Fatalf("error checking test file error message, eexpected: %v, got: %v", errorMsg, r.Error.Message) + } +} + +func TestWithRootWriteUserGroupReal(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("requires running as root") + } + username := os.Getenv("PEBBLE_TEST_USER") + group := os.Getenv("PEBBLE_TEST_GROUP") + if username == "" || group == "" { + t.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP") + } + u, err := user.Lookup(username) + if err != nil { + t.Fatalf("cannot look up username: %v", err) + } + g, err := user.LookupGroup(group) + if err != nil { + t.Fatalf("cannot look up group: %v", err) + } + uid, err := strconv.Atoi(u.Uid) + if err != nil { + t.Fatalf("cannot convert uid to int: %v", err) + } + gid, err := strconv.Atoi(g.Gid) + if err != nil { + t.Fatalf("cannot convert gid to int: %v", err) + } + + tmpDir := testWriteUserGroupRootTest(t, uid, gid, username, group) + + info, err := os.Stat(tmpDir + "/normal") + if err != nil { + t.Fatalf("cannot stat file %s: %v", tmpDir+"/normal", err) + } + statT := info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(0) { + t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/normal", uint32(0), statT.Uid) + } + if statT.Gid != uint32(0) { + t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/normal", uint32(0), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/uid-gid") + if err != nil { + t.Fatalf("cannot stat file %s: %v", tmpDir+"/uid-gid", err) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(uid) { + t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/uid-gid", uint32(uid), statT.Uid) + } + if statT.Gid != uint32(gid) { + t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/uid-gid", uint32(gid), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/user-group") + if err != nil { + t.Fatalf("cannot stat file %s: %v", tmpDir+"/user-group", err) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(uid) { + t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/user-group", uint32(uid), statT.Uid) + } + if statT.Gid != uint32(gid) { + t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/user-group", uint32(gid), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/nested1") + if err != nil { + t.Fatalf("cannot stat file %s: %v", tmpDir+"/nested1", err) + } + if int(info.Mode()&os.ModePerm) != 0o755 { + t.Fatalf("file %s mode error, expected: %v, got: %v", tmpDir+"/nested1", 0o755, int(info.Mode()&os.ModePerm)) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(0) { + t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/nested1", uint32(0), statT.Uid) + } + if statT.Gid != uint32(0) { + t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/nested1", uint32(0), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/nested1/normal") + if err != nil { + t.Fatalf("cannot stat file %s: %v", tmpDir+"/nested1/normal", err) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(0) { + t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/nested1/normal", uint32(0), statT.Uid) + } + if statT.Gid != uint32(0) { + t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/nested1/normal", uint32(0), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/nested2") + if err != nil { + t.Fatalf("cannot stat file %s: %v", tmpDir+"/nested2", err) + } + if int(info.Mode()&os.ModePerm) != 0o755 { + t.Fatalf("file %s mode error, expected: %v, got: %v", tmpDir+"/nested2", 0o755, int(info.Mode()&os.ModePerm)) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(uid) { + t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/nested2", uint32(uid), statT.Uid) + } + if statT.Gid != uint32(gid) { + t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/nested2", uint32(gid), statT.Uid) + } + + info, err = os.Stat(tmpDir + "/nested2/user-group") + if err != nil { + t.Fatalf("cannot stat file %s: %v", tmpDir+"/nested2/user-group", err) + } + statT = info.Sys().(*syscall.Stat_t) + if statT.Uid != uint32(uid) { + t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/nested2/user-group", uint32(uid), statT.Uid) + } + if statT.Gid != uint32(gid) { + t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/nested2/user-group", uint32(gid), statT.Uid) + } +} + +func testWriteUserGroupRootTest(t *testing.T, uid, gid int, user, group string) string { + tmpDir := t.TempDir() + pathNormal := tmpDir + "/normal" + pathUidGid := tmpDir + "/uid-gid" + pathUserGroup := tmpDir + "/user-group" + pathNested := tmpDir + "/nested1/normal" + pathNestedUserGroup := tmpDir + "/nested2/user-group" + + headers := http.Header{ + "Content-Type": []string{"multipart/form-data; boundary=01234567890123456789012345678901"}, + } + body := doRequestRootTest(t, v1PostFiles, "POST", "/v1/files", nil, headers, + []byte(fmt.Sprintf(` +--01234567890123456789012345678901 +Content-Disposition: form-data; name="request" + +{ + "action": "write", + "files": [ + {"path": "%[1]s"}, + {"path": "%[2]s", "user-id": %[3]d, "group-id": %[4]d}, + {"path": "%[5]s", "user": "%[6]s", "group": "%[7]s"}, + {"path": "%[8]s", "make-dirs": true}, + {"path": "%[9]s", "user": "%[10]s", "group": "%[11]s", "make-dirs": true} + ] +} +--01234567890123456789012345678901 +Content-Disposition: form-data; name="files"; filename="%[1]s" + +normal +--01234567890123456789012345678901 +Content-Disposition: form-data; name="files"; filename="%[2]s" + +uid gid +--01234567890123456789012345678901 +Content-Disposition: form-data; name="files"; filename="%[5]s" + +user group +--01234567890123456789012345678901 +Content-Disposition: form-data; name="files"; filename="%[8]s" + +nested +--01234567890123456789012345678901 +Content-Disposition: form-data; name="files"; filename="%[9]s" + +nested user group +--01234567890123456789012345678901-- +`, pathNormal, pathUidGid, uid, gid, pathUserGroup, user, group, + pathNested, pathNestedUserGroup, user, group))) + + var r testFilesResponse + if err := json.NewDecoder(body).Decode(&r); err != nil { + t.Fatalf("cannot decode response body for /v1/files: %v", err) + } + if r.StatusCode != http.StatusOK { + t.Fatalf("test file response status code error, expected: %v, got %v", http.StatusOK, r.StatusCode) + + } + if r.Type != "sync" { + t.Fatalf("test file response type error, expected: sync, got %v", r.StatusCode) + + } + if len(r.Result) != 5 { + t.Fatalf("test file response result length error, expected: 5, got %v", len(r.Result)) + + } + checkFileResultRootTest(t, r.Result[0], pathNormal, "", "") + checkFileResultRootTest(t, r.Result[1], pathUidGid, "", "") + checkFileResultRootTest(t, r.Result[2], pathUserGroup, "", "") + checkFileResultRootTest(t, r.Result[3], pathNested, "", "") + checkFileResultRootTest(t, r.Result[4], pathNestedUserGroup, "", "") + + assertFileRootTest(t, pathNormal, 0o644, "normal") + assertFileRootTest(t, pathUidGid, 0o644, "uid gid") + assertFileRootTest(t, pathUserGroup, 0o644, "user group") + assertFileRootTest(t, pathNested, 0o644, "nested") + assertFileRootTest(t, pathNestedUserGroup, 0o644, "nested user group") + + return tmpDir +} + +func assertFileRootTest(t *testing.T, path string, perm os.FileMode, content string) { + b, err := os.ReadFile(path) + if err != nil { + t.Fatalf("cannot read file %s: %v", path, err) + } + if string(b) != content { + t.Fatalf("file content error, expected: %v, got: %v", content, string(b)) + } + info, err := os.Stat(path) + if err != nil { + t.Fatalf("cannot stat file %s: %v", path, err) + } + if info.Mode().Perm() != perm { + t.Fatalf("error checking permission, expected: %v, got: %v", perm, info.Mode().Perm()) + } +} diff --git a/internals/daemon/api_files_test.go b/internals/daemon/api_files_test.go index 79c0f97e..2f4ba673 100644 --- a/internals/daemon/api_files_test.go +++ b/internals/daemon/api_files_test.go @@ -531,72 +531,6 @@ func (s *filesSuite) testMakeDirsUserGroup(c *C, uid, gid int, user, group strin return tmpDir } -// See .github/workflows/tests.yml for how to run this test as root. -func (s *filesSuite) TestMakeDirsUserGroupReal(c *C) { - if os.Getuid() != 0 { - c.Skip("requires running as root") - } - username := os.Getenv("PEBBLE_TEST_USER") - group := os.Getenv("PEBBLE_TEST_GROUP") - if username == "" || group == "" { - c.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP") - } - u, err := user.Lookup(username) - c.Assert(err, IsNil) - g, err := user.LookupGroup(group) - c.Assert(err, IsNil) - uid, err := strconv.Atoi(u.Uid) - c.Assert(err, IsNil) - gid, err := strconv.Atoi(g.Gid) - c.Assert(err, IsNil) - - tmpDir := s.testMakeDirsUserGroup(c, uid, gid, username, group) - - info, err := os.Stat(tmpDir + "/normal") - c.Assert(err, IsNil) - statT := info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(0)) - c.Check(statT.Gid, Equals, uint32(0)) - - info, err = os.Stat(tmpDir + "/uid-gid") - c.Assert(err, IsNil) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(uid)) - c.Check(statT.Gid, Equals, uint32(uid)) - - info, err = os.Stat(tmpDir + "/user-group") - c.Assert(err, IsNil) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(uid)) - c.Check(statT.Gid, Equals, uint32(uid)) - - info, err = os.Stat(tmpDir + "/nested1") - c.Assert(err, IsNil) - c.Check(int(info.Mode()&os.ModePerm), Equals, 0o755) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(0)) - c.Check(statT.Gid, Equals, uint32(0)) - - info, err = os.Stat(tmpDir + "/nested1/normal") - c.Assert(err, IsNil) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(0)) - c.Check(statT.Gid, Equals, uint32(0)) - - info, err = os.Stat(tmpDir + "/nested2") - c.Assert(err, IsNil) - c.Check(int(info.Mode()&os.ModePerm), Equals, 0o755) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(uid)) - c.Check(statT.Gid, Equals, uint32(gid)) - - info, err = os.Stat(tmpDir + "/nested2/user-group") - c.Assert(err, IsNil) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(uid)) - c.Check(statT.Gid, Equals, uint32(gid)) -} - func (s *filesSuite) TestRemoveSingle(c *C) { tmpDir := c.MkDir() writeTempFile(c, tmpDir, "file", "a", 0o644) @@ -988,72 +922,6 @@ func (s *filesSuite) TestWriteUserGroupMocked(c *C) { c.Check(mkdirCalls[1], Equals, mkdirArgs{tmpDir + "/nested2", 0o755, osutil.MkdirOptions{MakeParents: true, ExistOK: true, Chmod: true, Chown: true, UserID: 56, GroupID: 78}}) } -// See .github/workflows/tests.yml for how to run this test as root. -func (s *filesSuite) TestWriteUserGroupReal(c *C) { - if os.Getuid() != 0 { - c.Skip("requires running as root") - } - username := os.Getenv("PEBBLE_TEST_USER") - group := os.Getenv("PEBBLE_TEST_GROUP") - if username == "" || group == "" { - c.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP") - } - u, err := user.Lookup(username) - c.Assert(err, IsNil) - g, err := user.LookupGroup(group) - c.Assert(err, IsNil) - uid, err := strconv.Atoi(u.Uid) - c.Assert(err, IsNil) - gid, err := strconv.Atoi(g.Gid) - c.Assert(err, IsNil) - - tmpDir := s.testWriteUserGroup(c, uid, gid, username, group) - - info, err := os.Stat(tmpDir + "/normal") - c.Assert(err, IsNil) - statT := info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(0)) - c.Check(statT.Gid, Equals, uint32(0)) - - info, err = os.Stat(tmpDir + "/uid-gid") - c.Assert(err, IsNil) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(uid)) - c.Check(statT.Gid, Equals, uint32(uid)) - - info, err = os.Stat(tmpDir + "/user-group") - c.Assert(err, IsNil) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(uid)) - c.Check(statT.Gid, Equals, uint32(uid)) - - info, err = os.Stat(tmpDir + "/nested1") - c.Assert(err, IsNil) - c.Check(int(info.Mode()&os.ModePerm), Equals, 0o755) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(0)) - c.Check(statT.Gid, Equals, uint32(0)) - - info, err = os.Stat(tmpDir + "/nested1/normal") - c.Assert(err, IsNil) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(0)) - c.Check(statT.Gid, Equals, uint32(0)) - - info, err = os.Stat(tmpDir + "/nested2") - c.Assert(err, IsNil) - c.Check(int(info.Mode()&os.ModePerm), Equals, 0o755) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(uid)) - c.Check(statT.Gid, Equals, uint32(gid)) - - info, err = os.Stat(tmpDir + "/nested2/user-group") - c.Assert(err, IsNil) - statT = info.Sys().(*syscall.Stat_t) - c.Check(statT.Uid, Equals, uint32(uid)) - c.Check(statT.Gid, Equals, uint32(gid)) -} - func (s *filesSuite) testWriteUserGroup(c *C, uid, gid int, user, group string) string { tmpDir := c.MkDir() pathNormal := tmpDir + "/normal" diff --git a/internals/osutil/mkdir_root_test.go b/internals/osutil/mkdir_root_test.go new file mode 100644 index 00000000..d3148e5f --- /dev/null +++ b/internals/osutil/mkdir_root_test.go @@ -0,0 +1,111 @@ +//go:build roottest + +// Copyright (c) 2024 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package osutil_test + +import ( + "os" + "os/user" + "strconv" + "syscall" + "testing" + + "github.com/canonical/pebble/internals/osutil" + "github.com/canonical/pebble/internals/osutil/sys" +) + +func TestWithRootMakeParentsChmodAndChown(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("requires running as root") + } + + username := os.Getenv("PEBBLE_TEST_USER") + group := os.Getenv("PEBBLE_TEST_GROUP") + if username == "" || group == "" { + t.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP") + } + + u, err := user.Lookup(username) + if err != nil { + t.Fatalf("cannot look up username: %v", err) + } + g, err := user.LookupGroup(group) + if err != nil { + t.Fatalf("cannot look up group: %v", err) + } + uid, err := strconv.Atoi(u.Uid) + if err != nil { + t.Fatalf("cannot convert uid to int: %v", err) + } + gid, err := strconv.Atoi(g.Gid) + if err != nil { + t.Fatalf("cannot convert gid to int: %v", err) + } + tmpDir := t.TempDir() + + err = osutil.Mkdir(tmpDir+"/foo/bar", 0o777, &osutil.MkdirOptions{ + MakeParents: true, + Chmod: true, + Chown: true, + UserID: sys.UserID(uid), + GroupID: sys.GroupID(gid), + }) + if err != nil { + t.Fatalf(": %v", err) + } + if !osutil.IsDir(tmpDir + "/foo") { + t.Fatalf("file %s is not a directory", tmpDir+"/foo") + } + if !osutil.IsDir(tmpDir + "/foo/bar") { + t.Fatalf("file %s is not a directory", tmpDir+"/foo/bar") + } + + info, err := os.Stat(tmpDir + "/foo") + if err != nil { + t.Fatalf("cannot stat dir %s: %v", tmpDir+"/foo", err) + } + if info.Mode().Perm() != os.FileMode(0o777) { + t.Fatalf("error checking dir %s permission, expected: %v, got: %v", tmpDir+"/foo", 0o777, info.Mode().Perm()) + } + stat, ok := info.Sys().(*syscall.Stat_t) + if !ok { + t.Fatalf("syscall stat on dir %s error", tmpDir+"/foo") + } + if int(stat.Uid) != uid { + t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/foo", uid, int(stat.Uid)) + } + if int(stat.Uid) != uid { + t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/foo", gid, int(stat.Gid)) + } + + info, err = os.Stat(tmpDir + "/foo/bar") + if err != nil { + t.Fatalf(": %v", err) + } + if info.Mode().Perm() != os.FileMode(0o777) { + t.Fatalf("error checking dir %s permission, expected: %v, got: %v", tmpDir+"/foo/bar", 0o777, info.Mode().Perm()) + } + stat, ok = info.Sys().(*syscall.Stat_t) + if !ok { + t.Fatalf("syscall stat on dir %s error", tmpDir+"/foo/bar") + } + if int(stat.Uid) != uid { + t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/foo/bar", uid, int(stat.Uid)) + } + if int(stat.Uid) != uid { + t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/foo/bar", gid, int(stat.Gid)) + } +} diff --git a/internals/osutil/mkdir_test.go b/internals/osutil/mkdir_test.go index 6983e26d..668a31ba 100644 --- a/internals/osutil/mkdir_test.go +++ b/internals/osutil/mkdir_test.go @@ -16,14 +16,11 @@ package osutil_test import ( "os" - "os/user" - "strconv" "syscall" "gopkg.in/check.v1" "github.com/canonical/pebble/internals/osutil" - "github.com/canonical/pebble/internals/osutil/sys" ) type mkdirSuite struct{} @@ -207,53 +204,3 @@ func (mkdirSuite) TestMakeParentsAndNoChmod(c *check.C) { c.Assert(err, check.IsNil) c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0o755)) } - -// See .github/workflows/tests.yml for how to run this test as root. -func (mkdirSuite) TestMakeParentsChmodAndChown(c *check.C) { - if os.Getuid() != 0 { - c.Skip("requires running as root") - } - - username := os.Getenv("PEBBLE_TEST_USER") - group := os.Getenv("PEBBLE_TEST_GROUP") - if username == "" || group == "" { - c.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP") - } - - u, err := user.Lookup(username) - c.Assert(err, check.IsNil) - g, err := user.LookupGroup(group) - c.Assert(err, check.IsNil) - uid, err := strconv.Atoi(u.Uid) - c.Assert(err, check.IsNil) - gid, err := strconv.Atoi(g.Gid) - c.Assert(err, check.IsNil) - tmpDir := c.MkDir() - - err = osutil.Mkdir(tmpDir+"/foo/bar", 0o777, &osutil.MkdirOptions{ - MakeParents: true, - Chmod: true, - Chown: true, - UserID: sys.UserID(uid), - GroupID: sys.GroupID(gid), - }) - c.Assert(err, check.IsNil) - c.Assert(osutil.IsDir(tmpDir+"/foo"), check.Equals, true) - c.Assert(osutil.IsDir(tmpDir+"/foo/bar"), check.Equals, true) - - info, err := os.Stat(tmpDir + "/foo") - c.Assert(err, check.IsNil) - c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0o777)) - stat, ok := info.Sys().(*syscall.Stat_t) - c.Assert(ok, check.Equals, true) - c.Assert(int(stat.Uid), check.Equals, uid) - c.Assert(int(stat.Gid), check.Equals, gid) - - info, err = os.Stat(tmpDir + "/foo/bar") - c.Assert(err, check.IsNil) - c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0o777)) - stat, ok = info.Sys().(*syscall.Stat_t) - c.Assert(ok, check.Equals, true) - c.Assert(int(stat.Uid), check.Equals, uid) - c.Assert(int(stat.Gid), check.Equals, gid) -}