diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index aa04b7d694a..ebc38e75a0b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -394,6 +394,7 @@ jobs: - nosecboot - faultinject - race + - snapdusergo steps: - name: Checkout code @@ -491,6 +492,12 @@ jobs: cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 SKIP_DIRTY_CHECK=1 GO_TEST_RACE=1 SKIP_COVERAGE=1 ./run-checks --unit + - name: Test Go (snapdusergo) + if: ${{ matrix.unit-scenario == 'snapdusergo' }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=snapdusergo ./run-checks --unit + - name: Upload the coverage results if: ${{ matrix.gochannel != 'latest/stable' && matrix.unit-scenario != 'race' }} uses: actions/upload-artifact@v4 diff --git a/osutil/group_test.go b/osutil/group_test.go index 390fb30b9fa..de9e19dd5e6 100644 --- a/osutil/group_test.go +++ b/osutil/group_test.go @@ -38,7 +38,21 @@ var _ = check.Suite(&findUserGroupSuite{}) func (s *findUserGroupSuite) SetUpTest(c *check.C) { // exit 2 is not found - s.mockGetent = testutil.MockCommand(c, "getent", "exit 2") + if user.GetentBased { + s.mockGetent = testutil.MockCommand(c, "getent", ` +if [ "${1}" == "passwd" ] && [ "${2}" == "root" ]; then + echo 'root:x:0:0:root:/root:/bin/bash' + exit 0 +fi +if [ "${1}" == "group" ] && [ "${2}" == "root" ]; then + echo 'root:x:0:' + exit 0 +fi +exit 2`) + } else { + s.mockGetent = testutil.MockCommand(c, "getent", ` +exit 2`) + } } func (s *findUserGroupSuite) TearDownTest(c *check.C) { @@ -49,8 +63,10 @@ func (s *findUserGroupSuite) TestFindUidNoGetentFallback(c *check.C) { uid, err := osutil.FindUidNoGetentFallback("root") c.Assert(err, check.IsNil) c.Assert(uid, check.Equals, uint64(0)) - // getent shouldn't have been called with FindUidNoGetentFallback() - c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil)) + if !user.GetentBased { + // getent shouldn't have been called with FindUidNoGetentFallback() + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil)) + } } func (s *findUserGroupSuite) TestFindUidNonexistent(c *check.C) { @@ -58,16 +74,24 @@ func (s *findUserGroupSuite) TestFindUidNonexistent(c *check.C) { c.Assert(err, check.ErrorMatches, "user: unknown user lakatos") _, ok := err.(user.UnknownUserError) c.Assert(ok, check.Equals, true) - // getent shouldn't have been called with FindUidNoGetentFallback() - c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil)) + if user.GetentBased { + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ + {"getent", "passwd", "lakatos"}, + }) + } else { + // getent shouldn't have been called with FindUidNoGetentFallback() + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil)) + } } func (s *findUserGroupSuite) TestFindUidWithGetentFallback(c *check.C) { uid, err := osutil.FindUidWithGetentFallback("root") c.Assert(err, check.IsNil) c.Assert(uid, check.Equals, uint64(0)) - // getent shouldn't have been called since 'root' is in /etc/passwd - c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil)) + if !user.GetentBased { + // getent shouldn't have been called since 'root' is in /etc/passwd + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil)) + } } func (s *findUserGroupSuite) TestFindUidGetentNonexistent(c *check.C) { @@ -75,10 +99,17 @@ func (s *findUserGroupSuite) TestFindUidGetentNonexistent(c *check.C) { c.Assert(err, check.ErrorMatches, "user: unknown user lakatos") _, ok := err.(user.UnknownUserError) c.Assert(ok, check.Equals, true) - // getent should've have been called - c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ - {"getent", "passwd", "lakatos"}, - }) + if user.GetentBased { + // getent should've have been called + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ + {"getent", "passwd", "lakatos"}, + {"getent", "passwd", "lakatos"}, + }) + } else { + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ + {"getent", "passwd", "lakatos"}, + }) + } } func (s *findUserGroupSuite) TestFindUidGetentFoundFromGetent(c *check.C) { @@ -110,10 +141,18 @@ func (s *findUserGroupSuite) TestFindUidGetentMockedOtherError(c *check.C) { uid, err := osutil.FindUidWithGetentFallback("lakatos") c.Assert(err, check.ErrorMatches, "getent failed with: exit status 3") c.Check(uid, check.Equals, uint64(0)) - // getent should've have been called - c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ - {"getent", "passwd", "lakatos"}, - }) + if user.GetentBased { + // getent should've have been called + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ + {"getent", "passwd", "lakatos"}, + {"getent", "passwd", "lakatos"}, + }) + } else { + // getent should've have been called + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ + {"getent", "passwd", "lakatos"}, + }) + } } func (s *findUserGroupSuite) TestFindUidGetentMocked(c *check.C) { @@ -136,10 +175,12 @@ func (s *findUserGroupSuite) TestFindUidGetentMockedMalformated(c *check.C) { func (s *findUserGroupSuite) TestFindGidNoGetentFallback(c *check.C) { gid, err := osutil.FindGidNoGetentFallback("root") - c.Assert(err, check.IsNil) - c.Assert(gid, check.Equals, uint64(0)) - // getent shouldn't have been called with FindGidNoGetentFallback() - c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil)) + if !user.GetentBased { + c.Assert(err, check.IsNil) + c.Assert(gid, check.Equals, uint64(0)) + // getent shouldn't have been called with FindGidNoGetentFallback() + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil)) + } } func (s *findUserGroupSuite) TestFindGidNonexistent(c *check.C) { @@ -176,8 +217,10 @@ func (s *findUserGroupSuite) TestFindGidWithGetentFallback(c *check.C) { gid, err := osutil.FindGidWithGetentFallback("root") c.Assert(err, check.IsNil) c.Assert(gid, check.Equals, uint64(0)) - // getent shouldn't have been called since 'root' is in /etc/group - c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil)) + if !user.GetentBased { + // getent shouldn't have been called since 'root' is in /etc/group + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil)) + } } func (s *findUserGroupSuite) TestFindGidGetentNonexistent(c *check.C) { @@ -185,10 +228,18 @@ func (s *findUserGroupSuite) TestFindGidGetentNonexistent(c *check.C) { c.Assert(err, check.ErrorMatches, "group: unknown group lakatos") _, ok := err.(user.UnknownGroupError) c.Assert(ok, check.Equals, true) - // getent should've have been called - c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ - {"getent", "group", "lakatos"}, - }) + if user.GetentBased { + // getent should've have been called + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ + {"getent", "group", "lakatos"}, + {"getent", "group", "lakatos"}, + }) + } else { + // getent should've have been called + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ + {"getent", "group", "lakatos"}, + }) + } } func (s *findUserGroupSuite) TestFindGidGetentMockedOtherError(c *check.C) { @@ -198,9 +249,16 @@ func (s *findUserGroupSuite) TestFindGidGetentMockedOtherError(c *check.C) { c.Assert(err, check.ErrorMatches, "getent failed with: exit status 3") c.Check(gid, check.Equals, uint64(0)) // getent should've have been called - c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ - {"getent", "group", "lakatos"}, - }) + if user.GetentBased { + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ + {"getent", "group", "lakatos"}, + {"getent", "group", "lakatos"}, + }) + } else { + c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{ + {"getent", "group", "lakatos"}, + }) + } } func (s *findUserGroupSuite) TestFindGidGetentMocked(c *check.C) { diff --git a/osutil/strace/strace_test.go b/osutil/strace/strace_test.go index 7c22d8eabfe..334bf240b3a 100644 --- a/osutil/strace/strace_test.go +++ b/osutil/strace/strace_test.go @@ -21,6 +21,7 @@ package strace_test import ( "os" + "os/exec" "path/filepath" "testing" @@ -98,19 +99,36 @@ func (s *straceSuite) TestStraceCommandHappyFromSnap(c *C) { } func (s *straceSuite) TestStraceCommandNoSudo(c *C) { + tmp := c.MkDir() + + if user.GetentBased { + getEntPath, err := exec.LookPath("getent") + c.Assert(err, IsNil) + err = os.Symlink(getEntPath, filepath.Join(tmp, "getent")) + c.Assert(err, IsNil) + } + origPath := os.Getenv("PATH") defer func() { os.Setenv("PATH", origPath) }() + os.Setenv("PATH", tmp) - os.Setenv("PATH", "/not-exists") _, err := strace.Command(nil, "foo") c.Assert(err, ErrorMatches, `cannot use strace without sudo: exec: "sudo": executable file not found in \$PATH`) } func (s *straceSuite) TestStraceCommandNoStrace(c *C) { + tmp := c.MkDir() + + if user.GetentBased { + getEntPath, err := exec.LookPath("getent") + c.Assert(err, IsNil) + err = os.Symlink(getEntPath, filepath.Join(tmp, "getent")) + c.Assert(err, IsNil) + } + origPath := os.Getenv("PATH") defer func() { os.Setenv("PATH", origPath) }() - tmp := c.MkDir() os.Setenv("PATH", tmp) err := os.WriteFile(filepath.Join(tmp, "sudo"), nil, 0755) c.Assert(err, IsNil) diff --git a/osutil/user/getent.go b/osutil/user/getent.go index 40d8fca79a4..cacef50e094 100644 --- a/osutil/user/getent.go +++ b/osutil/user/getent.go @@ -37,6 +37,9 @@ func getEnt(params ...string) ([]byte, error) { if err != nil { var exitError *exec.ExitError if errors.As(err, &exitError) { + if exitError.ExitCode() == 2 { + return nil, nil + } return nil, fmt.Errorf("getent returned an error: %q", exitError.Stderr) } return nil, fmt.Errorf("getent could not be executed: %w", err) diff --git a/osutil/user/getent_test.go b/osutil/user/getent_test.go index 3eb9b34b934..b21ba37427d 100644 --- a/osutil/user/getent_test.go +++ b/osutil/user/getent_test.go @@ -23,6 +23,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "testing" . "gopkg.in/check.v1" @@ -46,12 +47,17 @@ func (s *getentSuite) SetUpTest(c *C) { s.getentDir = c.MkDir() s.mockGetent = testutil.MockCommand(c, "getent", fmt.Sprintf(` -cat '%s'/"${1}${2:+/}${2-}" +set -eu +base='%s'/"${1}${2:+/}${2-}" +cat "${base}" +if [ -f "${base}.exit" ]; then + exit "$(cat "${base}.exit")" +fi `, s.getentDir)) s.AddCleanup(s.mockGetent.Restore) } -func (s *getentSuite) mockGetentOutput(c *C, value string, params ...string) { +func (s *getentSuite) mockGetentOutput(c *C, value string, exit int, params ...string) { path := []string{s.getentDir} path = append(path, params...) resultPath := filepath.Join(path...) @@ -60,11 +66,16 @@ func (s *getentSuite) mockGetentOutput(c *C, value string, params ...string) { b := []byte(value) err = os.WriteFile(resultPath, b, 0644) c.Assert(err, IsNil) + if exit != 0 { + exitBytes := []byte(strconv.Itoa(exit)) + err = os.WriteFile(resultPath+".exit", exitBytes, 0644) + c.Assert(err, IsNil) + } } func (s *getentSuite) TestLookupGroupByName(c *C) { s.mockGetentOutput(c, `mygroup:x:60000:myuser,someuser -`, "group", "mygroup") +`, 0, "group", "mygroup") grp, err := user.LookupGroupFromGetent(user.GroupMatchGroupname("mygroup")) c.Assert(err, IsNil) @@ -79,7 +90,7 @@ func (s *getentSuite) TestLookupGroupByNameError(c *C) { } func (s *getentSuite) TestLookupGroupByNameDoesNotExist(c *C) { - s.mockGetentOutput(c, ``, "group", "mygroup") + s.mockGetentOutput(c, ``, 2, "group", "mygroup") grp, err := user.LookupGroupFromGetent(user.GroupMatchGroupname("mygroup")) c.Assert(err, IsNil) @@ -90,7 +101,7 @@ func (s *getentSuite) TestLookupGroupByNumericalName(c *C) { // This is probably not valid s.mockGetentOutput(c, `mygroup:x:60001:myuser,someuser 1mygroup:x:60000:myuser,someuser -`, "group") +`, 0, "group") grp, err := user.LookupGroupFromGetent(user.GroupMatchGroupname("1mygroup")) c.Assert(err, IsNil) @@ -101,7 +112,7 @@ func (s *getentSuite) TestLookupGroupByNumericalName(c *C) { func (s *getentSuite) TestLookupUserByName(c *C) { s.mockGetentOutput(c, `johndoe:x:60000:60000:John Doe:/home/johndoe:/bin/bash -`, "passwd", "johndoe") +`, 0, "passwd", "johndoe") usr, err := user.LookupUserFromGetent(user.UserMatchUsername("johndoe")) c.Assert(err, IsNil) @@ -115,7 +126,7 @@ func (s *getentSuite) TestLookupUserByName(c *C) { func (s *getentSuite) TestLookupUserByUid(c *C) { s.mockGetentOutput(c, `johndoe:x:60000:60000:John Doe:/home/johndoe:/bin/bash -`, "passwd", "60000") +`, 0, "passwd", "60000") usr, err := user.LookupUserFromGetent(user.UserMatchUid(60000)) c.Assert(err, IsNil) @@ -131,7 +142,7 @@ func (s *getentSuite) TestLookupUserByNumericalName(c *C) { // This is probably not valid s.mockGetentOutput(c, `johndoe:x:60001:60001:John Doe2:/home/johndoe2:/bin/bash 1johndoe:x:60000:60000:John Doe:/home/johndoe:/bin/bash -`, "passwd") +`, 0, "passwd") usr, err := user.LookupUserFromGetent(user.UserMatchUsername("1johndoe")) c.Assert(err, IsNil) @@ -142,3 +153,27 @@ func (s *getentSuite) TestLookupUserByNumericalName(c *C) { c.Check(usr.Name, Equals, "John Doe") c.Check(usr.HomeDir, Equals, "/home/johndoe") } + +func (s *getentSuite) TestLookupUserByNameMissing(c *C) { + s.mockGetentOutput(c, ``, 2, "passwd", "johndoe") + + usr, err := user.LookupUserFromGetent(user.UserMatchUsername("johndoe")) + c.Assert(err, IsNil) + c.Assert(usr, IsNil) +} + +func (s *getentSuite) TestLookupUserUidMissing(c *C) { + s.mockGetentOutput(c, ``, 2, "passwd", "60000") + + usr, err := user.LookupUserFromGetent(user.UserMatchUid(60000)) + c.Assert(err, IsNil) + c.Assert(usr, IsNil) +} + +func (s *getentSuite) TestLookupGroupByNameMissing(c *C) { + s.mockGetentOutput(c, ``, 2, "group", "mygroup") + + grp, err := user.LookupGroupFromGetent(user.GroupMatchGroupname("mygroup")) + c.Assert(err, IsNil) + c.Assert(grp, IsNil) +} diff --git a/osutil/user/user.go b/osutil/user/user.go index b94e1c2d790..9494cddd1b9 100644 --- a/osutil/user/user.go +++ b/osutil/user/user.go @@ -31,6 +31,8 @@ type ( UnknownGroupError = osuser.UnknownGroupError ) +const GetentBased = false + // Current returns the current user // // This is a wrapper for (os/user).Current diff --git a/osutil/user/user_from_snap.go b/osutil/user/user_from_snap.go index 912c71847e6..288b10f222e 100644 --- a/osutil/user/user_from_snap.go +++ b/osutil/user/user_from_snap.go @@ -35,6 +35,8 @@ type ( UnknownGroupError = osuser.UnknownGroupError ) +const GetentBased = true + // Current returns the current user func Current() (*User, error) { u, err := lookupUserFromGetent(userMatchUid(os.Getuid()))