diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index c0ebdc2e7b..6e4428583d 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -405,7 +405,7 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM } } - user, groups, umask, err := h.securityPolicyEnforcer.GetUserInfo(id, settings.OCISpecification.Process) + user, groups, umask, err := h.securityPolicyEnforcer.GetUserInfo(settings.OCISpecification.Process, settings.OCISpecification.Root.Path) if err != nil { return nil, err } @@ -753,7 +753,7 @@ func (h *Host) ExecProcess(ctx context.Context, containerID string, params prot. var umask string var allowStdioAccess bool - user, groups, umask, err = h.securityPolicyEnforcer.GetUserInfo(containerID, params.OCIProcess) + user, groups, umask, err = h.securityPolicyEnforcer.GetUserInfo(params.OCIProcess, c.spec.Root.Path) if err != nil { return 0, err } diff --git a/internal/guest/spec/spec.go b/internal/guest/spec/spec.go index f931a4133d..ebb923b358 100644 --- a/internal/guest/spec/spec.go +++ b/internal/guest/spec/spec.go @@ -165,82 +165,99 @@ func SetCoreRLimit(spec *oci.Spec, value string) error { return nil } +type ParseUserStrResult struct { + UID uint32 + GID uint32 + Username string + Groupname string +} + // ParseUserStr parses `userStr`, looks up container filesystem's /etc/passwd and /etc/group // files for UID and GID for the process. // // NB: When `userStr` represents a UID, which doesn't exist, return UID as is with GID set to 0. -func ParseUserStr(rootPath, userStr string) (uint32, uint32, error) { +func ParseUserStr(rootPath, userStr string) (res ParseUserStrResult, err error) { parts := strings.Split(userStr, ":") - switch len(parts) { - case 1: - v, err := strconv.Atoi(parts[0]) + if len(parts) > 2 { + return res, fmt.Errorf("invalid userstr: '%s'", userStr) + } + + var v int + var u user.User + var g user.Group + + // Handle the "user part" first. + v, err = strconv.Atoi(parts[0]) + if err != nil { + // this is a username string, evaluate uid/gid + u, err = GetUser(rootPath, func(u user.User) bool { + return u.Name == parts[0] + }) if err != nil { - // this is a username string, evaluate uid/gid - u, err := getUser(rootPath, func(u user.User) bool { - return u.Name == userStr - }) - if err != nil { - return 0, 0, errors.Wrapf(err, "failed to find user by username: %s", userStr) - } - return uint32(u.Uid), uint32(u.Gid), nil + return res, errors.Wrapf(err, "failed to find user by username: %s", parts[0]) } - - // this is a UID, parse /etc/passwd to find GID. - u, err := getUser(rootPath, func(u user.User) bool { + res.UID = uint32(u.Uid) + res.GID = uint32(u.Gid) + res.Username = u.Name + // Will determine group name below + } else { + // this is a UID, parse /etc/passwd to find name and GID. + u, err = GetUser(rootPath, func(u user.User) bool { return u.Uid == v }) if err != nil { - // UID doesn't exist, return as is with GID 0. if OutOfUint32Bounds(v) { - return 0, 0, errors.Errorf("UID (%d) exceeds uint32 bounds", v) - } - return uint32(v), 0, nil - } - return uint32(u.Uid), uint32(u.Gid), nil - case 2: - var ( - userName, groupName string - uid, gid uint32 - ) - v, err := strconv.Atoi(parts[0]) - if err != nil { - // username string, lookup UID - userName = parts[0] - u, err := getUser(rootPath, func(u user.User) bool { - return u.Name == userName - }) - if err != nil { - return 0, 0, errors.Wrapf(err, "failed to find user by username: %s", userName) + return res, errors.Errorf("UID (%d) exceeds uint32 bounds", v) } - uid = uint32(u.Uid) + // UID doesn't exist, continue with GID default to 0 (but we still want to + // look up its name) unless overridden. + res.UID = uint32(v) } else { - if OutOfUint32Bounds(v) { - return 0, 0, errors.Errorf("UID (%d) exceeds uint32 bounds", v) - } - uid = uint32(v) + res.UID = uint32(u.Uid) + res.GID = uint32(u.Gid) + res.Username = u.Name + } + } + + if len(parts) == 1 { + g, err = GetGroup(rootPath, func(g user.Group) bool { + return g.Gid == int(res.GID) + }) + // If GID doesn't exist we just don't fill in groupName + if err == nil { + res.Groupname = g.Name } + return res, nil + } - v, err = strconv.Atoi(parts[1]) + v, err = strconv.Atoi(parts[1]) + if err != nil { + // this is a group name string, evaluate gid + g, err = GetGroup(rootPath, func(g user.Group) bool { + return g.Name == parts[1] + }) + if err != nil { + return res, errors.Wrapf(err, "failed to find group by groupname: %s", parts[1]) + } + res.GID = uint32(g.Gid) + res.Groupname = g.Name + } else { + // this is a GID, parse /etc/group to find name. + g, err = GetGroup(rootPath, func(g user.Group) bool { + return g.Gid == v + }) if err != nil { - // groupname string, lookup GID - groupName = parts[1] - g, err := getGroup(rootPath, func(g user.Group) bool { - return g.Name == groupName - }) - if err != nil { - return 0, 0, errors.Wrapf(err, "failed to find group by groupname: %s", groupName) - } - gid = uint32(g.Gid) - } else { if OutOfUint32Bounds(v) { - return 0, 0, errors.Errorf("GID (%d) exceeds uint32 bounds", v) + return res, errors.Errorf("GID (%d) exceeds uint32 bounds", v) } - gid = uint32(v) + // GID doesn't exist, continue with GID as is. + res.GID = uint32(v) + } else { + res.GID = uint32(g.Gid) + res.Groupname = g.Name } - return uid, gid, nil - default: - return 0, 0, fmt.Errorf("invalid userstr: '%s'", userStr) } + return res, nil } // SetUserStr sets `spec.Process` to the valid `userstr` based on the OCI Image Spec @@ -264,17 +281,17 @@ func ParseUserStr(rootPath, userStr string) (uint32, uint32, error) { func SetUserStr(spec *oci.Spec, userstr string) error { setProcess(spec) - uid, gid, err := ParseUserStr(spec.Root.Path, userstr) + usrInfo, err := ParseUserStr(spec.Root.Path, userstr) if err != nil { return err } - spec.Process.User.UID, spec.Process.User.GID = uid, gid + spec.Process.User.UID, spec.Process.User.GID = usrInfo.UID, usrInfo.GID return nil } -// getUser looks up /etc/passwd file in a container filesystem and returns parsed user -func getUser(rootPath string, filter func(user.User) bool) (user.User, error) { +// GetUser looks up /etc/passwd file in a container filesystem and returns parsed user +func GetUser(rootPath string, filter func(user.User) bool) (user.User, error) { users, err := user.ParsePasswdFileFilter(filepath.Join(rootPath, "/etc/passwd"), filter) if err != nil { return user.User{}, err @@ -293,8 +310,8 @@ func getUser(rootPath string, filter func(user.User) bool) (user.User, error) { return users[0], nil } -// getGroup looks up /etc/group file in a container filesystem and returns parsed group -func getGroup(rootPath string, filter func(user.Group) bool) (user.Group, error) { +// GetGroup looks up /etc/group file in a container filesystem and returns parsed group +func GetGroup(rootPath string, filter func(user.Group) bool) (user.Group, error) { groups, err := user.ParseGroupFileFilter(filepath.Join(rootPath, "/etc/group"), filter) if err != nil { return user.Group{}, err diff --git a/pkg/securitypolicy/regopolicy_test.go b/pkg/securitypolicy/regopolicy_test.go index ea18500d23..61953966d3 100644 --- a/pkg/securitypolicy/regopolicy_test.go +++ b/pkg/securitypolicy/regopolicy_test.go @@ -9,6 +9,9 @@ import ( "encoding/json" "fmt" "math/rand" + "os" + "path/filepath" + "slices" "sort" "strconv" "strings" @@ -5953,6 +5956,318 @@ func Test_Rego_Capabiltiies_Placeholder_Object_Unprivileged(t *testing.T) { assertDecisionJSONDoesNotContain(t, err, DefaultUnprivilegedCapabilities()...) } +func Test_Rego_GetUserInfo_WithEtcPasswdAndGroup(t *testing.T) { + etcPasswdString := strings.Join([]string{ + "root:x:0:0:root:/root:/bin/bash", + "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin", + "postgres:x:105:111:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash", + "ihavenogroup:x:1000:1000:ihavenogroup:/home/ihavenogroup:/bin/bash", + "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin", + "", + }, "\n") + etcGroupsString := strings.Join([]string{ + "root:x:0:", + "daemon:x:1:", + "postgres:x:111:", + "nogroup:x:65534:", + "", + }, "\n") + + testDir := t.TempDir() + os.MkdirAll(filepath.Join(testDir, "etc"), 0755) + if err := os.WriteFile(filepath.Join(testDir, "etc/passwd"), []byte(etcPasswdString), 0644); err != nil { + t.Fatalf("Failed to write /etc/passwd: %v", err) + } + if err := os.WriteFile(filepath.Join(testDir, "etc/group"), []byte(etcGroupsString), 0644); err != nil { + t.Fatalf("Failed to write /etc/group: %v", err) + } + + regoEnforcer, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}) + if err != nil { + t.Errorf("cannot compile open door rego policy: %v", err) + return + } + + testCases := []getUserInfoTestCase{ + { + userStrs: []string{"0:0", "root:root", "0:root", "root:0", "root", "0", ""}, + additionalGIDs: []uint32{}, + expectedUID: 0, + expectedGIDs: []int{0}, + expectedUsername: "root", + expectedGroupNames: []string{"root"}, + }, + { + userStrs: []string{"1:1", "daemon:daemon", "1:daemon", "daemon:1", "daemon", "1"}, + additionalGIDs: []uint32{}, + expectedUID: 1, + expectedGIDs: []int{1}, + expectedUsername: "daemon", + expectedGroupNames: []string{"daemon"}, + }, + { + userStrs: []string{"1:0", "daemon:root", "daemon:0", "1:root"}, + additionalGIDs: []uint32{}, + expectedUID: 1, + expectedGIDs: []int{0}, + expectedUsername: "daemon", + expectedGroupNames: []string{"root"}, + }, + { + userStrs: []string{"105:111", "postgres:postgres", "105", "postgres", "105:postgres", "postgres:111"}, + additionalGIDs: []uint32{}, + expectedUID: 105, + expectedGIDs: []int{111}, + expectedUsername: "postgres", + expectedGroupNames: []string{"postgres"}, + }, + { + userStrs: []string{"postgres:daemon", "105:1", "postgres:1", "105:daemon"}, + additionalGIDs: []uint32{}, + expectedUID: 105, + expectedGIDs: []int{1}, + expectedUsername: "postgres", + expectedGroupNames: []string{"daemon"}, + }, + { + userStrs: []string{"1234", "1234:0"}, + additionalGIDs: []uint32{}, + expectedUID: 1234, + expectedGIDs: []int{0}, + expectedUsername: "", + expectedGroupNames: []string{"root"}, + }, + { + userStrs: []string{"0:1234"}, + additionalGIDs: []uint32{}, + expectedUID: 0, + expectedGIDs: []int{1234}, + expectedUsername: "root", + expectedGroupNames: []string{""}, + }, + { + userStrs: []string{"1234:1234"}, + additionalGIDs: []uint32{}, + expectedUID: 1234, + expectedGIDs: []int{1234}, + expectedUsername: "", + expectedGroupNames: []string{""}, + }, + { + userStrs: []string{"1234:postgres", "1234:111"}, + additionalGIDs: []uint32{}, + expectedUID: 1234, + expectedGIDs: []int{111}, + expectedUsername: "", + expectedGroupNames: []string{"postgres"}, + }, + { + userStrs: []string{"postgres:1234", "105:1234"}, + additionalGIDs: []uint32{}, + expectedUID: 105, + expectedGIDs: []int{1234}, + expectedUsername: "postgres", + expectedGroupNames: []string{""}, + }, + { + userStrs: []string{"ihavenogroup:nogroup", "ihavenogroup:65534", "1000:65534"}, + additionalGIDs: []uint32{}, + expectedUID: 1000, + expectedGIDs: []int{65534}, + expectedUsername: "ihavenogroup", + expectedGroupNames: []string{"nogroup"}, + }, + { + userStrs: []string{"ihavenogroup", "ihavenogroup:1000", "1000:1000"}, + additionalGIDs: []uint32{}, + expectedUID: 1000, + expectedGIDs: []int{1000}, + expectedUsername: "ihavenogroup", + expectedGroupNames: []string{""}, + }, + { + userStrs: []string{"nobody", "nobody:nogroup", "nobody:65534", "65534:65534", "65534:nogroup"}, + additionalGIDs: []uint32{100}, + expectErr: true, // non-existent additionalGIDs not allowed + }, + { + userStrs: []string{"nobody", "nobody:nogroup", "nobody:65534", "65534:65534", "65534:nogroup"}, + additionalGIDs: []uint32{111}, + expectedUID: 65534, + expectedGIDs: []int{65534, 111}, + expectedUsername: "nobody", + expectedGroupNames: []string{"nogroup", "postgres"}, + }, + { + userStrs: []string{"1234", "1234:0"}, + additionalGIDs: []uint32{111}, + expectedUID: 1234, + expectedGIDs: []int{0, 111}, + expectedUsername: "", + expectedGroupNames: []string{"root", "postgres"}, + }, + { + userStrs: []string{"nonexistentuser", "nonexistentuser:0", "nonexistentuser:nonexistentgroup"}, + additionalGIDs: []uint32{}, + expectErr: true, // non-existent username will fail since we don't know the UID + }, + { + userStrs: []string{"postgres:nonexistentgroup", "105:nonexistentgroup"}, + additionalGIDs: []uint32{}, + expectErr: true, // non-existent group will fail since we don't know the GID + }, + { + userStrs: []string{":", "malformed:b:c", ":root", "root:", ":0", "0:", "2a01:110::/32"}, + additionalGIDs: []uint32{}, + expectErr: true, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("TestCase%d", i), func(t *testing.T) { + for _, userStr := range tc.userStrs { + testGetUserInfo(t, tc, userStr, regoEnforcer, testDir) + } + }) + } +} + +func Test_Rego_GetUserInfo_NoEtc(t *testing.T) { + testDir := t.TempDir() + + regoEnforcer, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}) + if err != nil { + t.Errorf("cannot compile open door rego policy: %v", err) + return + } + + testCases := []getUserInfoTestCase{ + { + userStrs: []string{"0:0", "0", ""}, + additionalGIDs: []uint32{}, + expectedUID: 0, + expectedGIDs: []int{0}, + expectedUsername: "", + expectedGroupNames: []string{""}, + }, + { + userStrs: []string{"1:1"}, + additionalGIDs: []uint32{}, + expectedUID: 1, + expectedGIDs: []int{1}, + expectedUsername: "", + expectedGroupNames: []string{""}, + }, + { + userStrs: []string{"root:root", "root", "daemon:daemon", "1:daemon", "daemon:1", "daemon", "daemon:root", "daemon:0"}, + additionalGIDs: []uint32{}, + expectErr: true, // no /etc/passwd or /etc/group, so we cannot resolve any names + }, + { + userStrs: []string{"1:0", "1"}, + additionalGIDs: []uint32{}, + expectedUID: 1, + expectedGIDs: []int{0}, + expectedUsername: "", + expectedGroupNames: []string{""}, + }, + { + userStrs: []string{"1234:4321"}, + additionalGIDs: []uint32{}, + expectedUID: 1234, + expectedGIDs: []int{4321}, + expectedUsername: "", + expectedGroupNames: []string{""}, + }, + { + userStrs: []string{"1234:4321"}, + additionalGIDs: []uint32{5678, 9012}, + expectErr: true, // non-existent additionalGIDs not allowed + }, + { + userStrs: []string{":", "malformed:b:c", ":root", "root:", ":0", "0:", "2a01:110::/32"}, + additionalGIDs: []uint32{}, + expectErr: true, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("TestCase%d", i), func(t *testing.T) { + for _, userStr := range tc.userStrs { + testGetUserInfo(t, tc, userStr, regoEnforcer, testDir) + } + }) + } +} + +func Test_Rego_GetUserInfo_EtcPasswdOnly(t *testing.T) { + etcPasswdString := strings.Join([]string{ + "root:x:0:0:root:/root:/bin/bash", + "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin", + "postgres:x:105:111:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash", + "ihavenogroup:x:1000:1000:ihavenogroup:/home/ihavenogroup:/bin/bash", + "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin", + "", + }, "\n") + + testDir := t.TempDir() + os.MkdirAll(filepath.Join(testDir, "etc"), 0755) + if err := os.WriteFile(filepath.Join(testDir, "etc/passwd"), []byte(etcPasswdString), 0644); err != nil { + t.Fatalf("Failed to write /etc/passwd: %v", err) + } + + regoEnforcer, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}) + if err != nil { + t.Errorf("cannot compile open door rego policy: %v", err) + return + } + + testCases := []getUserInfoTestCase{ + { + userStrs: []string{"0:0", "root:0", "0", "root", ""}, + additionalGIDs: []uint32{}, + expectedUID: 0, + expectedGIDs: []int{0}, + expectedUsername: "root", + expectedGroupNames: []string{""}, + }, + { + userStrs: []string{"1000:1234", "ihavenogroup:1234"}, + additionalGIDs: []uint32{}, + expectedUID: 1000, + expectedGIDs: []int{1234}, + expectedUsername: "ihavenogroup", + expectedGroupNames: []string{""}, + }, + { + userStrs: []string{"nonexistentuser"}, + additionalGIDs: []uint32{}, + expectErr: true, // non-existent username will fail since we don't know the UID + }, + { + userStrs: []string{"1000:root", "0:root"}, + additionalGIDs: []uint32{}, + expectErr: true, // No /etc/group, can't resolve group names + }, + { + userStrs: []string{"1234"}, + additionalGIDs: []uint32{}, + expectedUID: 1234, + expectedGIDs: []int{0}, + expectedUsername: "", + expectedGroupNames: []string{""}, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("TestCase%d", i), func(t *testing.T) { + for _, userStr := range tc.userStrs { + testGetUserInfo(t, tc, userStr, regoEnforcer, testDir) + } + }) + } +} + // // Setup and "fixtures" follow... // @@ -7619,3 +7934,79 @@ func assertDecisionJSONDoesNotContain(t *testing.T, err error, expectedValues .. return true } + +type getUserInfoTestCase struct { + userStrs []string + additionalGIDs []uint32 + expectErr bool + expectedUID int + expectedGIDs []int + expectedUsername string + expectedGroupNames []string +} + +func testGetUserInfo(t *testing.T, tc getUserInfoTestCase, userStr string, regoEnforcer *regoEnforcer, testDir string) { + testName := userStr + if userStr == "" { + testName = "(empty)" + } + if len(tc.additionalGIDs) > 0 { + testName += " additionalGIDs=" + for i, gid := range tc.additionalGIDs { + if i > 0 { + testName += "," + } + testName += fmt.Sprintf("%d", gid) + } + } + t.Run(testName, func(t *testing.T) { + ociProcess := &oci.Process{ + User: oci.User{ + UID: 0, + GID: 0, + Umask: nil, + Username: userStr, + AdditionalGids: tc.additionalGIDs, + }, + } + userIDName, groupIDNames, umask, err := regoEnforcer.GetUserInfo(ociProcess, testDir) + if tc.expectErr { + if err == nil { + t.Errorf("Expected error for userStr %q, but succeed", userStr) + } + return + } + if err != nil { + t.Errorf("GetUserInfo failed for userStr %q: %v", userStr, err) + return + } + if userIDName.ID != fmt.Sprintf("%d", tc.expectedUID) { + t.Errorf("Expected UID %d, got %s", tc.expectedUID, userIDName.ID) + } + if userIDName.Name != tc.expectedUsername { + t.Errorf("Expected username %s, got %s", tc.expectedUsername, userIDName.Name) + } + groupIDs := make([]int, 0, len(groupIDNames)) + for _, groupIDName := range groupIDNames { + gid, err := strconv.Atoi(groupIDName.ID) + if err != nil { + t.Errorf("Returned group ID %s is not a valid integer", groupIDName.ID) + } + groupIDs = append(groupIDs, gid) + } + if !slices.Equal(groupIDs, tc.expectedGIDs) { + t.Errorf("Expected group IDs %v, got %v", tc.expectedGIDs, groupIDs) + } + groupNames := make([]string, 0, len(groupIDNames)) + for _, groupIDName := range groupIDNames { + groupNames = append(groupNames, groupIDName.Name) + } + if !slices.Equal(groupNames, tc.expectedGroupNames) { + t.Errorf("Expected group names %v, got %v", tc.expectedGroupNames, groupNames) + } + defaultUmask := "0022" + if umask != defaultUmask { + t.Errorf("Expected umask '%s', got '%s'", defaultUmask, umask) + } + }) +} diff --git a/pkg/securitypolicy/securitypolicyenforcer.go b/pkg/securitypolicy/securitypolicyenforcer.go index 8792966229..1dae5412ce 100644 --- a/pkg/securitypolicy/securitypolicyenforcer.go +++ b/pkg/securitypolicy/securitypolicyenforcer.go @@ -79,7 +79,7 @@ type SecurityPolicyEnforcer interface { LoadFragment(ctx context.Context, issuer string, feed string, code string) error EnforceScratchMountPolicy(ctx context.Context, scratchPath string, encrypted bool) (err error) EnforceScratchUnmountPolicy(ctx context.Context, scratchPath string) (err error) - GetUserInfo(containerID string, spec *oci.Process) (IDName, []IDName, string, error) + GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) } //nolint @@ -587,7 +587,7 @@ func (StandardSecurityPolicyEnforcer) EnforceScratchUnmountPolicy(context.Contex } // Stub. We are deprecating the standard enforcer. -func (StandardSecurityPolicyEnforcer) GetUserInfo(containerID string, spec *oci.Process) (IDName, []IDName, string, error) { +func (StandardSecurityPolicyEnforcer) GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) { return IDName{}, nil, "", nil } @@ -951,7 +951,7 @@ func (OpenDoorSecurityPolicyEnforcer) EnforceScratchUnmountPolicy(context.Contex return nil } -func (OpenDoorSecurityPolicyEnforcer) GetUserInfo(containerID string, spec *oci.Process) (IDName, []IDName, string, error) { +func (OpenDoorSecurityPolicyEnforcer) GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) { return IDName{}, nil, "", nil } @@ -1037,6 +1037,6 @@ func (ClosedDoorSecurityPolicyEnforcer) EnforceScratchUnmountPolicy(context.Cont return errors.New("unmounting scratch is denied by the policy") } -func (ClosedDoorSecurityPolicyEnforcer) GetUserInfo(containerID string, spec *oci.Process) (IDName, []IDName, string, error) { +func (ClosedDoorSecurityPolicyEnforcer) GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) { return IDName{}, nil, "", nil } diff --git a/pkg/securitypolicy/securitypolicyenforcer_rego.go b/pkg/securitypolicy/securitypolicyenforcer_rego.go index 52a0cbf571..c9567734ab 100644 --- a/pkg/securitypolicy/securitypolicyenforcer_rego.go +++ b/pkg/securitypolicy/securitypolicyenforcer_rego.go @@ -15,7 +15,7 @@ import ( "strings" "syscall" - "github.com/opencontainers/runc/libcontainer/user" + "github.com/moby/sys/user" oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -992,92 +992,88 @@ func (policy *regoEnforcer) EnforceScratchUnmountPolicy(ctx context.Context, scr return nil } -func getUser(passwdPath string, filter func(user.User) bool) (user.User, error) { - users, err := user.ParsePasswdFileFilter(passwdPath, filter) - if err != nil { - return user.User{}, err - } - if len(users) != 1 { - return user.User{}, errors.Errorf("expected exactly 1 user matched '%d'", len(users)) - } - return users[0], nil -} - -func getGroup(groupPath string, filter func(user.Group) bool) (user.Group, error) { - groups, err := user.ParseGroupFileFilter(groupPath, filter) - if err != nil { - return user.Group{}, err - } - if len(groups) != 1 { - return user.Group{}, errors.Errorf("expected exactly 1 group matched '%d'", len(groups)) - } - return groups[0], nil -} - -func (policy *regoEnforcer) GetUserInfo(containerID string, process *oci.Process) (IDName, []IDName, string, error) { - rootPath := filepath.Join(guestpath.LCOWRootPrefixInUVM, containerID, guestpath.RootfsPath) +func (policy *regoEnforcer) GetUserInfo(process *oci.Process, rootPath string) ( + userIDName IDName, + groupIDNames []IDName, + umask string, + err error, +) { passwdPath := filepath.Join(rootPath, "/etc/passwd") groupPath := filepath.Join(rootPath, "/etc/group") if process == nil { - return IDName{}, nil, "", errors.New("spec.Process is nil") + err = errors.New("spec.Process is nil") + return } // this default value is used in the Linux kernel if no umask is specified - umask := "0022" + umask = "0022" if process.User.Umask != nil { umask = fmt.Sprintf("%04o", *process.User.Umask) } + parsedUserStr := false + checkGroup := true + if process.User.Username != "" { - uid, gid, err := specGuest.ParseUserStr(rootPath, process.User.Username) - if err == nil { - userIDName := IDName{ID: strconv.FormatUint(uint64(uid), 10)} - groupIDName := IDName{ID: strconv.FormatUint(uint64(gid), 10)} - return userIDName, []IDName{groupIDName}, umask, nil + var usrInfo specGuest.ParseUserStrResult + usrInfo, err = specGuest.ParseUserStr(rootPath, process.User.Username) + if err != nil { + err = errors.Errorf("failed to parse user str %q: %v", process.User.Username, err) + return } - log.G(context.Background()).WithError(err).Warn("failed to parse user str, fallback to lookup") + userIDName = IDName{ID: strconv.FormatUint(uint64(usrInfo.UID), 10), Name: usrInfo.Username} + groupIDNames = []IDName{ + {ID: strconv.FormatUint(uint64(usrInfo.GID), 10), Name: usrInfo.Groupname}, + } + parsedUserStr = true } - // fallback UID/GID lookup - uid := process.User.UID - userIDName := IDName{ID: strconv.FormatUint(uint64(uid), 10), Name: ""} - if _, err := os.Stat(passwdPath); err == nil { - userInfo, err := getUser(passwdPath, func(user user.User) bool { - return uint32(user.Uid) == uid - }) + if !parsedUserStr { + // fallback UID/GID lookup + uid := process.User.UID + userIDName = IDName{ID: strconv.FormatUint(uint64(uid), 10), Name: ""} + if _, err = os.Stat(passwdPath); err == nil { + var userInfo user.User + userInfo, err = specGuest.GetUser(rootPath, func(user user.User) bool { + return uint32(user.Uid) == uid + }) - if err != nil { - return userIDName, nil, "", err - } + if err != nil { + return userIDName, nil, "", err + } - userIDName.Name = userInfo.Name - } + userIDName.Name = userInfo.Name + } - gid := process.User.GID - groupIDName := IDName{ID: strconv.FormatUint(uint64(gid), 10), Name: ""} + gid := process.User.GID + groupIDName := IDName{ID: strconv.FormatUint(uint64(gid), 10), Name: ""} - checkGroup := true - if _, err := os.Stat(groupPath); err == nil { - groupInfo, err := getGroup(groupPath, func(group user.Group) bool { - return uint32(group.Gid) == gid - }) + if _, err = os.Stat(groupPath); err == nil { + var groupInfo user.Group + groupInfo, err = specGuest.GetGroup(rootPath, func(group user.Group) bool { + return uint32(group.Gid) == gid + }) - if err != nil { - return userIDName, nil, "", err + if err != nil { + return userIDName, nil, "", err + } + groupIDName.Name = groupInfo.Name + } else { + checkGroup = false + err = nil } - groupIDName.Name = groupInfo.Name - } else { - checkGroup = false + + groupIDNames = []IDName{groupIDName} } - groupIDNames := []IDName{groupIDName} additionalGIDs := process.User.AdditionalGids if len(additionalGIDs) > 0 { for _, gid := range additionalGIDs { - groupIDName = IDName{ID: strconv.FormatUint(uint64(gid), 10), Name: ""} + groupIDName := IDName{ID: strconv.FormatUint(uint64(gid), 10), Name: ""} if checkGroup { - groupInfo, err := getGroup(groupPath, func(group user.Group) bool { + var groupInfo user.Group + groupInfo, err = specGuest.GetGroup(rootPath, func(group user.Group) bool { return uint32(group.Gid) == gid }) if err != nil { diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_deprecated.go b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_deprecated.go deleted file mode 100644 index c6cd443455..0000000000 --- a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_deprecated.go +++ /dev/null @@ -1,81 +0,0 @@ -package user - -import ( - "io" - - "github.com/moby/sys/user" -) - -// LookupUser looks up a user by their username in /etc/passwd. If the user -// cannot be found (or there is no /etc/passwd file on the filesystem), then -// LookupUser returns an error. -func LookupUser(username string) (user.User, error) { - return user.LookupUser(username) -} - -// LookupUid looks up a user by their user id in /etc/passwd. If the user cannot -// be found (or there is no /etc/passwd file on the filesystem), then LookupId -// returns an error. -func LookupUid(uid int) (user.User, error) { //nolint:revive // ignore var-naming: func LookupUid should be LookupUID - return user.LookupUid(uid) -} - -// LookupGroup looks up a group by its name in /etc/group. If the group cannot -// be found (or there is no /etc/group file on the filesystem), then LookupGroup -// returns an error. -func LookupGroup(groupname string) (user.Group, error) { - return user.LookupGroup(groupname) -} - -// LookupGid looks up a group by its group id in /etc/group. If the group cannot -// be found (or there is no /etc/group file on the filesystem), then LookupGid -// returns an error. -func LookupGid(gid int) (user.Group, error) { - return user.LookupGid(gid) -} - -func GetPasswdPath() (string, error) { - return user.GetPasswdPath() -} - -func GetPasswd() (io.ReadCloser, error) { - return user.GetPasswd() -} - -func GetGroupPath() (string, error) { - return user.GetGroupPath() -} - -func GetGroup() (io.ReadCloser, error) { - return user.GetGroup() -} - -// CurrentUser looks up the current user by their user id in /etc/passwd. If the -// user cannot be found (or there is no /etc/passwd file on the filesystem), -// then CurrentUser returns an error. -func CurrentUser() (user.User, error) { - return user.CurrentUser() -} - -// CurrentGroup looks up the current user's group by their primary group id's -// entry in /etc/passwd. If the group cannot be found (or there is no -// /etc/group file on the filesystem), then CurrentGroup returns an error. -func CurrentGroup() (user.Group, error) { - return user.CurrentGroup() -} - -func CurrentUserSubUIDs() ([]user.SubID, error) { - return user.CurrentUserSubUIDs() -} - -func CurrentUserSubGIDs() ([]user.SubID, error) { - return user.CurrentUserSubGIDs() -} - -func CurrentProcessUIDMap() ([]user.IDMap, error) { - return user.CurrentProcessUIDMap() -} - -func CurrentProcessGIDMap() ([]user.IDMap, error) { - return user.CurrentProcessGIDMap() -} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/user_deprecated.go b/vendor/github.com/opencontainers/runc/libcontainer/user/user_deprecated.go deleted file mode 100644 index 3c29f3d1d8..0000000000 --- a/vendor/github.com/opencontainers/runc/libcontainer/user/user_deprecated.go +++ /dev/null @@ -1,146 +0,0 @@ -// Package user is an alias for [github.com/moby/sys/user]. -// -// Deprecated: use [github.com/moby/sys/user]. -package user - -import ( - "io" - - "github.com/moby/sys/user" -) - -var ( - // ErrNoPasswdEntries is returned if no matching entries were found in /etc/group. - ErrNoPasswdEntries = user.ErrNoPasswdEntries - // ErrNoGroupEntries is returned if no matching entries were found in /etc/passwd. - ErrNoGroupEntries = user.ErrNoGroupEntries - // ErrRange is returned if a UID or GID is outside of the valid range. - ErrRange = user.ErrRange -) - -type ( - User = user.User - - Group = user.Group - - // SubID represents an entry in /etc/sub{u,g}id. - SubID = user.SubID - - // IDMap represents an entry in /proc/PID/{u,g}id_map. - IDMap = user.IDMap - - ExecUser = user.ExecUser -) - -func ParsePasswdFile(path string) ([]user.User, error) { - return user.ParsePasswdFile(path) -} - -func ParsePasswd(passwd io.Reader) ([]user.User, error) { - return user.ParsePasswd(passwd) -} - -func ParsePasswdFileFilter(path string, filter func(user.User) bool) ([]user.User, error) { - return user.ParsePasswdFileFilter(path, filter) -} - -func ParsePasswdFilter(r io.Reader, filter func(user.User) bool) ([]user.User, error) { - return user.ParsePasswdFilter(r, filter) -} - -func ParseGroupFile(path string) ([]user.Group, error) { - return user.ParseGroupFile(path) -} - -func ParseGroup(group io.Reader) ([]user.Group, error) { - return user.ParseGroup(group) -} - -func ParseGroupFileFilter(path string, filter func(user.Group) bool) ([]user.Group, error) { - return user.ParseGroupFileFilter(path, filter) -} - -func ParseGroupFilter(r io.Reader, filter func(user.Group) bool) ([]user.Group, error) { - return user.ParseGroupFilter(r, filter) -} - -// GetExecUserPath is a wrapper for GetExecUser. It reads data from each of the -// given file paths and uses that data as the arguments to GetExecUser. If the -// files cannot be opened for any reason, the error is ignored and a nil -// io.Reader is passed instead. -func GetExecUserPath(userSpec string, defaults *user.ExecUser, passwdPath, groupPath string) (*user.ExecUser, error) { - return user.GetExecUserPath(userSpec, defaults, passwdPath, groupPath) -} - -// GetExecUser parses a user specification string (using the passwd and group -// readers as sources for /etc/passwd and /etc/group data, respectively). In -// the case of blank fields or missing data from the sources, the values in -// defaults is used. -// -// GetExecUser will return an error if a user or group literal could not be -// found in any entry in passwd and group respectively. -// -// Examples of valid user specifications are: -// - "" -// - "user" -// - "uid" -// - "user:group" -// - "uid:gid -// - "user:gid" -// - "uid:group" -// -// It should be noted that if you specify a numeric user or group id, they will -// not be evaluated as usernames (only the metadata will be filled). So attempting -// to parse a user with user.Name = "1337" will produce the user with a UID of -// 1337. -func GetExecUser(userSpec string, defaults *user.ExecUser, passwd, group io.Reader) (*user.ExecUser, error) { - return user.GetExecUser(userSpec, defaults, passwd, group) -} - -// GetAdditionalGroups looks up a list of groups by name or group id -// against the given /etc/group formatted data. If a group name cannot -// be found, an error will be returned. If a group id cannot be found, -// or the given group data is nil, the id will be returned as-is -// provided it is in the legal range. -func GetAdditionalGroups(additionalGroups []string, group io.Reader) ([]int, error) { - return user.GetAdditionalGroups(additionalGroups, group) -} - -// GetAdditionalGroupsPath is a wrapper around GetAdditionalGroups -// that opens the groupPath given and gives it as an argument to -// GetAdditionalGroups. -func GetAdditionalGroupsPath(additionalGroups []string, groupPath string) ([]int, error) { - return user.GetAdditionalGroupsPath(additionalGroups, groupPath) -} - -func ParseSubIDFile(path string) ([]user.SubID, error) { - return user.ParseSubIDFile(path) -} - -func ParseSubID(subid io.Reader) ([]user.SubID, error) { - return user.ParseSubID(subid) -} - -func ParseSubIDFileFilter(path string, filter func(user.SubID) bool) ([]user.SubID, error) { - return user.ParseSubIDFileFilter(path, filter) -} - -func ParseSubIDFilter(r io.Reader, filter func(user.SubID) bool) ([]user.SubID, error) { - return user.ParseSubIDFilter(r, filter) -} - -func ParseIDMapFile(path string) ([]user.IDMap, error) { - return user.ParseIDMapFile(path) -} - -func ParseIDMap(r io.Reader) ([]user.IDMap, error) { - return user.ParseIDMap(r) -} - -func ParseIDMapFileFilter(path string, filter func(user.IDMap) bool) ([]user.IDMap, error) { - return user.ParseIDMapFileFilter(path, filter) -} - -func ParseIDMapFilter(r io.Reader, filter func(user.IDMap) bool) ([]user.IDMap, error) { - return user.ParseIDMapFilter(r, filter) -} diff --git a/vendor/modules.txt b/vendor/modules.txt index d9b33fb991..f0747c7edc 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -408,7 +408,6 @@ github.com/opencontainers/image-spec/specs-go/v1 # github.com/opencontainers/runc v1.3.0 ## explicit; go 1.23.0 github.com/opencontainers/runc/libcontainer/devices -github.com/opencontainers/runc/libcontainer/user # github.com/opencontainers/runtime-spec v1.2.1 ## explicit github.com/opencontainers/runtime-spec/specs-go