From 1dd7f13dfbc1dd377eabace0239b1c05cd60b144 Mon Sep 17 00:00:00 2001 From: baude Date: Thu, 25 Oct 2018 13:39:25 -0500 Subject: get user and group information using securejoin and runc's user library for the purposes of performance and security, we use securejoin to contstruct the root fs's path so that symlinks are what they appear to be and no pointing to something naughty. then instead of chrooting to parse /etc/passwd|/etc/group, we now use the runc user/group methods which saves us quite a bit of performance. Signed-off-by: baude --- pkg/lookup/lookup.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 pkg/lookup/lookup.go (limited to 'pkg/lookup') diff --git a/pkg/lookup/lookup.go b/pkg/lookup/lookup.go new file mode 100644 index 000000000..b27e2a724 --- /dev/null +++ b/pkg/lookup/lookup.go @@ -0,0 +1,156 @@ +package lookup + +import ( + "github.com/cyphar/filepath-securejoin" + "github.com/opencontainers/runc/libcontainer/user" + "github.com/sirupsen/logrus" + "strconv" +) + +const ( + etcpasswd = "/etc/passwd" + etcgroup = "/etc/group" +) + +// Overrides allows you to override defaults in GetUserGroupInfo +type Overrides struct { + DefaultUser *user.ExecUser + ContainerEtcPasswdPath string + ContainerEtcGroupPath string +} + +// GetUserGroupInfo takes string forms of the the container's mount path and the container user and +// returns a ExecUser with uid, gid, sgids, and home. And override can be provided for defaults. +func GetUserGroupInfo(containerMount, containerUser string, override *Overrides) (*user.ExecUser, error) { + var ( + passwdDest, groupDest string + defaultExecUser *user.ExecUser + err error + ) + passwdPath := etcpasswd + groupPath := etcgroup + + if override != nil { + // Check for an override /etc/passwd path + if override.ContainerEtcPasswdPath != "" { + passwdPath = override.ContainerEtcPasswdPath + } + // Check for an override for /etc/group path + if override.ContainerEtcGroupPath != "" { + groupPath = override.ContainerEtcGroupPath + } + } + + // Check for an override default user + if override != nil && override.DefaultUser != nil { + defaultExecUser = override.DefaultUser + } else { + // Define a default container user + //defaultExecUser = &user.ExecUser{ + // Uid: 0, + // Gid: 0, + // Home: "/", + defaultExecUser = nil + + } + + // Make sure the /etc/group and /etc/passwd destinations are not a symlink to something naughty + if passwdDest, err = securejoin.SecureJoin(containerMount, passwdPath); err != nil { + logrus.Debug(err) + return nil, err + } + if groupDest, err = securejoin.SecureJoin(containerMount, groupPath); err != nil { + logrus.Debug(err) + return nil, err + } + return user.GetExecUserPath(containerUser, defaultExecUser, passwdDest, groupDest) +} + +// GetContainerGroups uses securejoin to get a list of numerical groupids from a container. Per the runc +// function it calls: 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 GetContainerGroups(groups []string, containerMount string, override *Overrides) ([]uint32, error) { + var ( + groupDest string + err error + uintgids []uint32 + ) + + groupPath := etcgroup + if override != nil && override.ContainerEtcGroupPath != "" { + groupPath = override.ContainerEtcGroupPath + } + + if groupDest, err = securejoin.SecureJoin(containerMount, groupPath); err != nil { + logrus.Debug(err) + return nil, err + } + + gids, err := user.GetAdditionalGroupsPath(groups, groupDest) + if err != nil { + return nil, err + } + // For libpod, we want []uint32s + for _, gid := range gids { + uintgids = append(uintgids, uint32(gid)) + } + return uintgids, nil +} + +// GetUser takes a containermount path and user name or id and returns +// a matching User structure from /etc/passwd. If it cannot locate a user +// with the provided information, an ErrNoPasswdEntries is returned. +func GetUser(containerMount, userIDorName string) (*user.User, error) { + var inputIsName bool + uid, err := strconv.Atoi(userIDorName) + if err != nil { + inputIsName = true + } + passwdDest, err := securejoin.SecureJoin(containerMount, etcpasswd) + if err != nil { + return nil, err + } + users, err := user.ParsePasswdFileFilter(passwdDest, func(u user.User) bool { + if inputIsName { + return u.Name == userIDorName + } + return u.Uid == uid + }) + if err != nil { + return nil, err + } + if len(users) > 0 { + return &users[0], nil + } + return nil, user.ErrNoPasswdEntries +} + +// GetGroup takes ac ontainermount path and a group name or id and returns +// a match Group struct from /etc/group. if it cannot locate a group, +// an ErrNoGroupEntries error is returned. +func GetGroup(containerMount, groupIDorName string) (*user.Group, error) { + var inputIsName bool + gid, err := strconv.Atoi(groupIDorName) + if err != nil { + inputIsName = true + } + + groupDest, err := securejoin.SecureJoin(containerMount, etcgroup) + if err != nil { + return nil, err + } + + groups, err := user.ParseGroupFileFilter(groupDest, func(g user.Group) bool { + if inputIsName { + return g.Name == groupIDorName + } + return g.Gid == gid + }) + if err != nil { + return nil, err + } + if len(groups) > 0 { + return &groups[0], nil + } + return nil, user.ErrNoGroupEntries +} -- cgit v1.2.3-54-g00ecf From ae68bec75cf59e8a530dbc55f320f7bb0be9a62b Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Tue, 6 Nov 2018 06:26:35 -0500 Subject: Don't fail if /etc/passwd or /etc/group does not exists Container images can be created without passwd or group file, currently if one of these containers gets run with a --user flag the container blows up complaining about t a missing /etc/passwd file. We just need to check if the error on read is ENOEXIST then allow the read to return, not fail. Signed-off-by: Daniel J Walsh --- libpod/container_internal.go | 4 ++-- pkg/lookup/lookup.go | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) (limited to 'pkg/lookup') diff --git a/libpod/container_internal.go b/libpod/container_internal.go index d928c4aed..558099e82 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/opencontainers/runc/libcontainer/user" "io" "io/ioutil" "os" @@ -25,6 +24,7 @@ import ( "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/chrootarchive" "github.com/containers/storage/pkg/mount" + "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux/label" @@ -1069,7 +1069,7 @@ func (c *Container) generatePasswd() (string, error) { } originPasswdFile := filepath.Join(c.state.Mountpoint, "/etc/passwd") orig, err := ioutil.ReadFile(originPasswdFile) - if err != nil { + if err != nil && !os.IsNotExist(err) { return "", errors.Wrapf(err, "unable to read passwd file %s", originPasswdFile) } diff --git a/pkg/lookup/lookup.go b/pkg/lookup/lookup.go index b27e2a724..a9d975b4b 100644 --- a/pkg/lookup/lookup.go +++ b/pkg/lookup/lookup.go @@ -1,10 +1,12 @@ package lookup import ( + "os" + "strconv" + "github.com/cyphar/filepath-securejoin" "github.com/opencontainers/runc/libcontainer/user" "github.com/sirupsen/logrus" - "strconv" ) const ( @@ -116,7 +118,7 @@ func GetUser(containerMount, userIDorName string) (*user.User, error) { } return u.Uid == uid }) - if err != nil { + if err != nil && !os.IsNotExist(err) { return nil, err } if len(users) > 0 { @@ -146,7 +148,7 @@ func GetGroup(containerMount, groupIDorName string) (*user.Group, error) { } return g.Gid == gid }) - if err != nil { + if err != nil && !os.IsNotExist(err) { return nil, err } if len(groups) > 0 { -- cgit v1.2.3-54-g00ecf From 39df2093e89a2816f317b72a73106166031045e6 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 4 Dec 2018 12:50:59 -0800 Subject: pkg/lookup: Return ID-only pointers on ErrNo*Entries Callers that only care about the IDs should try to convert the identifier to an integer before calling the Get* functions, so they can save the cost of hitting the filesystem and maybe or maybe not finding the other fields (User.Name, etc.). But callers that *want* the other fields but only actually need the ID can, with this commit, just call the Get* function and ignore ErrNo*Entries responses: user, err := lookup.GetUser(mount, userIDorName) if err != nil && err != ErrNoPasswdEntries { return err } Previously, they'd have to perform their own integer-conversion attempt in Get* error handling, with logic like: user, err := lookup.GetUser(mount, userIDorName) if err == ErrNoPasswdEntries { uuid, err := strconv.ParseUint(userIDorName, 10, 32) if err == nil { user.Uid = int(uuid) } } else if err != nil { return err } Signed-off-by: W. Trevor King --- pkg/lookup/lookup.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) (limited to 'pkg/lookup') diff --git a/pkg/lookup/lookup.go b/pkg/lookup/lookup.go index a9d975b4b..70b97144f 100644 --- a/pkg/lookup/lookup.go +++ b/pkg/lookup/lookup.go @@ -99,9 +99,11 @@ func GetContainerGroups(groups []string, containerMount string, override *Overri return uintgids, nil } -// GetUser takes a containermount path and user name or id and returns +// GetUser takes a containermount path and user name or ID and returns // a matching User structure from /etc/passwd. If it cannot locate a user // with the provided information, an ErrNoPasswdEntries is returned. +// When the provided user name was an ID, a User structure with Uid +// set is returned along with ErrNoPasswdEntries. func GetUser(containerMount, userIDorName string) (*user.User, error) { var inputIsName bool uid, err := strconv.Atoi(userIDorName) @@ -124,12 +126,17 @@ func GetUser(containerMount, userIDorName string) (*user.User, error) { if len(users) > 0 { return &users[0], nil } + if !inputIsName { + return &user.User{Uid: uid}, user.ErrNoPasswdEntries + } return nil, user.ErrNoPasswdEntries } -// GetGroup takes ac ontainermount path and a group name or id and returns -// a match Group struct from /etc/group. if it cannot locate a group, -// an ErrNoGroupEntries error is returned. +// GetGroup takes a containermount path and a group name or ID and returns +// a match Group struct from /etc/group. If it cannot locate a group, +// an ErrNoGroupEntries error is returned. When the provided group name +// was an ID, a Group structure with Gid set is returned along with +// ErrNoGroupEntries. func GetGroup(containerMount, groupIDorName string) (*user.Group, error) { var inputIsName bool gid, err := strconv.Atoi(groupIDorName) @@ -154,5 +161,8 @@ func GetGroup(containerMount, groupIDorName string) (*user.Group, error) { if len(groups) > 0 { return &groups[0], nil } + if !inputIsName { + return &user.Group{Gid: gid}, user.ErrNoGroupEntries + } return nil, user.ErrNoGroupEntries } -- cgit v1.2.3-54-g00ecf