123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- //go:build !windows
- package idtools // import "github.com/docker/docker/pkg/idtools"
- import (
- "bytes"
- "fmt"
- "io"
- "os"
- "os/exec"
- "path/filepath"
- "strconv"
- "syscall"
- "github.com/opencontainers/runc/libcontainer/user"
- )
- func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error {
- path, err := filepath.Abs(path)
- if err != nil {
- return err
- }
- stat, err := os.Stat(path)
- if err == nil {
- if !stat.IsDir() {
- return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
- }
- if !chownExisting {
- return nil
- }
- // short-circuit -- we were called with an existing directory and chown was requested
- return setPermissions(path, mode, owner, stat)
- }
- // make an array containing the original path asked for, plus (for mkAll == true)
- // all path components leading up to the complete path that don't exist before we MkdirAll
- // so that we can chown all of them properly at the end. If chownExisting is false, we won't
- // chown the full directory path if it exists
- var paths []string
- if os.IsNotExist(err) {
- paths = []string{path}
- }
- if mkAll {
- // walk back to "/" looking for directories which do not exist
- // and add them to the paths array for chown after creation
- dirPath := path
- for {
- dirPath = filepath.Dir(dirPath)
- if dirPath == "/" {
- break
- }
- if _, err = os.Stat(dirPath); err != nil && os.IsNotExist(err) {
- paths = append(paths, dirPath)
- }
- }
- if err = os.MkdirAll(path, mode); err != nil {
- return err
- }
- } else if err = os.Mkdir(path, mode); err != nil {
- return err
- }
- // even if it existed, we will chown the requested path + any subpaths that
- // didn't exist when we called MkdirAll
- for _, pathComponent := range paths {
- if err = setPermissions(pathComponent, mode, owner, nil); err != nil {
- return err
- }
- }
- return nil
- }
- // LookupUser uses traditional local system files lookup (from libcontainer/user) on a username,
- // followed by a call to `getent` for supporting host configured non-files passwd and group dbs
- func LookupUser(name string) (user.User, error) {
- // first try a local system files lookup using existing capabilities
- usr, err := user.LookupUser(name)
- if err == nil {
- return usr, nil
- }
- // local files lookup failed; attempt to call `getent` to query configured passwd dbs
- usr, err = getentUser(name)
- if err != nil {
- return user.User{}, err
- }
- return usr, nil
- }
- // LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid,
- // followed by a call to `getent` for supporting host configured non-files passwd and group dbs
- func LookupUID(uid int) (user.User, error) {
- // first try a local system files lookup using existing capabilities
- usr, err := user.LookupUid(uid)
- if err == nil {
- return usr, nil
- }
- // local files lookup failed; attempt to call `getent` to query configured passwd dbs
- return getentUser(strconv.Itoa(uid))
- }
- func getentUser(name string) (user.User, error) {
- reader, err := callGetent("passwd", name)
- if err != nil {
- return user.User{}, err
- }
- users, err := user.ParsePasswd(reader)
- if err != nil {
- return user.User{}, err
- }
- if len(users) == 0 {
- return user.User{}, fmt.Errorf("getent failed to find passwd entry for %q", name)
- }
- return users[0], nil
- }
- // LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name,
- // followed by a call to `getent` for supporting host configured non-files passwd and group dbs
- func LookupGroup(name string) (user.Group, error) {
- // first try a local system files lookup using existing capabilities
- group, err := user.LookupGroup(name)
- if err == nil {
- return group, nil
- }
- // local files lookup failed; attempt to call `getent` to query configured group dbs
- return getentGroup(name)
- }
- // LookupGID uses traditional local system files lookup (from libcontainer/user) on a group ID,
- // followed by a call to `getent` for supporting host configured non-files passwd and group dbs
- func LookupGID(gid int) (user.Group, error) {
- // first try a local system files lookup using existing capabilities
- group, err := user.LookupGid(gid)
- if err == nil {
- return group, nil
- }
- // local files lookup failed; attempt to call `getent` to query configured group dbs
- return getentGroup(strconv.Itoa(gid))
- }
- func getentGroup(name string) (user.Group, error) {
- reader, err := callGetent("group", name)
- if err != nil {
- return user.Group{}, err
- }
- groups, err := user.ParseGroup(reader)
- if err != nil {
- return user.Group{}, err
- }
- if len(groups) == 0 {
- return user.Group{}, fmt.Errorf("getent failed to find groups entry for %q", name)
- }
- return groups[0], nil
- }
- func callGetent(database, key string) (io.Reader, error) {
- getentCmd, err := resolveBinary("getent")
- // if no `getent` command within the execution environment, can't do anything else
- if err != nil {
- return nil, fmt.Errorf("unable to find getent command: %w", err)
- }
- command := exec.Command(getentCmd, database, key)
- // we run getent within container filesystem, but without /dev so /dev/null is not available for exec to mock stdin
- command.Stdin = io.NopCloser(bytes.NewReader(nil))
- out, err := command.CombinedOutput()
- if err != nil {
- exitCode, errC := getExitCode(err)
- if errC != nil {
- return nil, err
- }
- switch exitCode {
- case 1:
- return nil, fmt.Errorf("getent reported invalid parameters/database unknown")
- case 2:
- return nil, fmt.Errorf("getent unable to find entry %q in %s database", key, database)
- case 3:
- return nil, fmt.Errorf("getent database doesn't support enumeration")
- default:
- return nil, err
- }
- }
- return bytes.NewReader(out), nil
- }
- // getExitCode returns the ExitStatus of the specified error if its type is
- // exec.ExitError, returns 0 and an error otherwise.
- func getExitCode(err error) (int, error) {
- exitCode := 0
- if exiterr, ok := err.(*exec.ExitError); ok {
- if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok {
- return procExit.ExitStatus(), nil
- }
- }
- return exitCode, fmt.Errorf("failed to get exit code")
- }
- // setPermissions performs a chown/chmod only if the uid/gid don't match what's requested
- // Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the
- // dir is on an NFS share, so don't call chown unless we absolutely must.
- // Likewise for setting permissions.
- func setPermissions(p string, mode os.FileMode, owner Identity, stat os.FileInfo) error {
- if stat == nil {
- var err error
- stat, err = os.Stat(p)
- if err != nil {
- return err
- }
- }
- if stat.Mode().Perm() != mode.Perm() {
- if err := os.Chmod(p, mode.Perm()); err != nil {
- return err
- }
- }
- ssi := stat.Sys().(*syscall.Stat_t)
- if ssi.Uid == uint32(owner.UID) && ssi.Gid == uint32(owner.GID) {
- return nil
- }
- return os.Chown(p, owner.UID, owner.GID)
- }
- // LoadIdentityMapping takes a requested username and
- // using the data from /etc/sub{uid,gid} ranges, creates the
- // proper uid and gid remapping ranges for that user/group pair
- func LoadIdentityMapping(name string) (IdentityMapping, error) {
- usr, err := LookupUser(name)
- if err != nil {
- return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err)
- }
- subuidRanges, err := lookupSubUIDRanges(usr)
- if err != nil {
- return IdentityMapping{}, err
- }
- subgidRanges, err := lookupSubGIDRanges(usr)
- if err != nil {
- return IdentityMapping{}, err
- }
- return IdentityMapping{
- UIDMaps: subuidRanges,
- GIDMaps: subgidRanges,
- }, nil
- }
- func lookupSubUIDRanges(usr user.User) ([]IDMap, error) {
- rangeList, err := parseSubuid(strconv.Itoa(usr.Uid))
- if err != nil {
- return nil, err
- }
- if len(rangeList) == 0 {
- rangeList, err = parseSubuid(usr.Name)
- if err != nil {
- return nil, err
- }
- }
- if len(rangeList) == 0 {
- return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name)
- }
- return createIDMap(rangeList), nil
- }
- func lookupSubGIDRanges(usr user.User) ([]IDMap, error) {
- rangeList, err := parseSubgid(strconv.Itoa(usr.Uid))
- if err != nil {
- return nil, err
- }
- if len(rangeList) == 0 {
- rangeList, err = parseSubgid(usr.Name)
- if err != nil {
- return nil, err
- }
- }
- if len(rangeList) == 0 {
- return nil, fmt.Errorf("no subgid ranges found for user %q", usr.Name)
- }
- return createIDMap(rangeList), nil
- }
|