//go:build !windows package daemon import ( "fmt" "math" "strconv" "strings" "github.com/moby/go-archive" "github.com/moby/moby/v2/daemon/container" "github.com/moby/moby/v2/errdefs" "github.com/moby/sys/user" ) func (daemon *Daemon) tarCopyOptions(ctr *container.Container, allowOverwriteDirWithFile bool) (*archive.TarOptions, error) { if ctr.Config.User == "" { return daemon.defaultTarCopyOptions(allowOverwriteDirWithFile), nil } uid, gid, err := getUIDGID(ctr.Config.User) if err != nil { return nil, errdefs.InvalidParameter(err) } return &archive.TarOptions{ NoOverwriteDirNonDir: !allowOverwriteDirWithFile, ChownOpts: &archive.ChownOpts{UID: uid, GID: gid}, }, nil } // getUIDGID resolves the UID and GID of a given container's Config.User, // which can contain a user name (or ID) and, optionally, group (or ID). // // usergrp is a username or uid, and optional group, in the format `user[:group]`. // Both `user` and `group` can be provided as an `uid` / `gid`, so the following // formats are supported: // // - username - valid username from /etc/passwd // - username:groupname - valid username; valid groupname from /etc/passwd, /etc/group // - uid - 32-bit unsigned int valid Linux UID value // - uid:gid - uid value; 32-bit unsigned int Linux GID value // - username:gid - valid username from getent(1), gid value; 32-bit unsigned int Linux GID value // - uid:groupname - 32-bit unsigned int valid Linux UID value, valid groupname from /etc/group // // If only a username (or uid) is provided, an attempt is made to look up the gid // for that username using /etc/passwd func getUIDGID(ctrUser string) (uid int, gid int, _ error) { userNameOrID, groupNameOrID, _ := strings.Cut(ctrUser, ":") // Align with behavior of docker run, which treats an empty username // or groupname as default (0 (root)). // // docker run --rm --user ":33" alpine id // uid=0(root) gid=33 groups=33 // // docker run --rm --user "33:" alpine id // uid=33 gid=0(root) groups=0(root) if userNameOrID != "" { var err error uid, gid, err = lookupUser(userNameOrID) if err != nil { return 0, 0, err } } if groupNameOrID != "" { var err error gid, err = lookupGID(groupNameOrID) if err != nil { return 0, 0, err } } return uid, gid, nil } // getIDOrName checks whether nameOrID is a ID (integer) or a Name. // It assumes nameOrID is a name when failing to parse as integer, // in which case a non-empty name is returned. func getIDOrName(nameOrID string) (id int, name string) { if uid, err := strconv.ParseUint(nameOrID, 10, 32); err == nil && uid <= math.MaxInt32 { // uid provided return int(uid), "" } // not an id, assume name return 0, nameOrID } func lookupUser(nameOrID string) (uid, gid int, _ error) { userID, userName := getIDOrName(nameOrID) if userName != "" { u, err := user.LookupUser(userName) if err != nil { return 0, 0, fmt.Errorf("failed to look up user %q in container: %w", userName, err) } return u.Uid, u.Gid, nil } u, err := user.LookupUid(userID) if err != nil { // Match behavior of "docker run": when using a UID for the // user, resolving the user and its primary group is best-effort. // If a user with the given UID is found, we use its primary // group, otherwise use it as-is and use the default (0) as // GID. // // docker run --rm --user 12345 ubuntu id // uid=12345 gid=0(root) groups=0(root) // // docker run --rm ubuntu cat /etc/passwd | grep www-data // www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin // // docker run --rm --user 33 ubuntu id // uid=33(www-data) gid=33(www-data) groups=33(www-data) return userID, 0, nil } return u.Uid, u.Gid, nil } func lookupGID(nameOrID string) (int, error) { groupID, groupName := getIDOrName(nameOrID) if groupName == "" { // GID is passed, no need to look up return groupID, nil } group, err := user.LookupGroup(groupName) if err != nil { return 0, fmt.Errorf("failed to look up group %q in container: %w", nameOrID, err) } return group.Gid, nil }