github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/pkg/idtools/idtools_unix.go (about)

     1  //go:build !windows
     2  // +build !windows
     3  
     4  package idtools // import "github.com/docker/docker/pkg/idtools"
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strconv"
    14  	"sync"
    15  	"syscall"
    16  
    17  	"github.com/opencontainers/runc/libcontainer/user"
    18  )
    19  
    20  var (
    21  	entOnce   sync.Once
    22  	getentCmd string
    23  )
    24  
    25  func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error {
    26  	path, err := filepath.Abs(path)
    27  	if err != nil {
    28  		return err
    29  	}
    30  
    31  	stat, err := os.Stat(path)
    32  	if err == nil {
    33  		if !stat.IsDir() {
    34  			return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
    35  		}
    36  		if !chownExisting {
    37  			return nil
    38  		}
    39  
    40  		// short-circuit -- we were called with an existing directory and chown was requested
    41  		return setPermissions(path, mode, owner, stat)
    42  	}
    43  
    44  	// make an array containing the original path asked for, plus (for mkAll == true)
    45  	// all path components leading up to the complete path that don't exist before we MkdirAll
    46  	// so that we can chown all of them properly at the end.  If chownExisting is false, we won't
    47  	// chown the full directory path if it exists
    48  	var paths []string
    49  	if os.IsNotExist(err) {
    50  		paths = []string{path}
    51  	}
    52  
    53  	if mkAll {
    54  		// walk back to "/" looking for directories which do not exist
    55  		// and add them to the paths array for chown after creation
    56  		dirPath := path
    57  		for {
    58  			dirPath = filepath.Dir(dirPath)
    59  			if dirPath == "/" {
    60  				break
    61  			}
    62  			if _, err = os.Stat(dirPath); err != nil && os.IsNotExist(err) {
    63  				paths = append(paths, dirPath)
    64  			}
    65  		}
    66  		if err = os.MkdirAll(path, mode); err != nil {
    67  			return err
    68  		}
    69  	} else if err = os.Mkdir(path, mode); err != nil {
    70  		return err
    71  	}
    72  	// even if it existed, we will chown the requested path + any subpaths that
    73  	// didn't exist when we called MkdirAll
    74  	for _, pathComponent := range paths {
    75  		if err = setPermissions(pathComponent, mode, owner, nil); err != nil {
    76  			return err
    77  		}
    78  	}
    79  	return nil
    80  }
    81  
    82  // LookupUser uses traditional local system files lookup (from libcontainer/user) on a username,
    83  // followed by a call to `getent` for supporting host configured non-files passwd and group dbs
    84  func LookupUser(name string) (user.User, error) {
    85  	// first try a local system files lookup using existing capabilities
    86  	usr, err := user.LookupUser(name)
    87  	if err == nil {
    88  		return usr, nil
    89  	}
    90  	// local files lookup failed; attempt to call `getent` to query configured passwd dbs
    91  	usr, err = getentUser(name)
    92  	if err != nil {
    93  		return user.User{}, err
    94  	}
    95  	return usr, nil
    96  }
    97  
    98  // LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid,
    99  // followed by a call to `getent` for supporting host configured non-files passwd and group dbs
   100  func LookupUID(uid int) (user.User, error) {
   101  	// first try a local system files lookup using existing capabilities
   102  	usr, err := user.LookupUid(uid)
   103  	if err == nil {
   104  		return usr, nil
   105  	}
   106  	// local files lookup failed; attempt to call `getent` to query configured passwd dbs
   107  	return getentUser(strconv.Itoa(uid))
   108  }
   109  
   110  func getentUser(name string) (user.User, error) {
   111  	reader, err := callGetent("passwd", name)
   112  	if err != nil {
   113  		return user.User{}, err
   114  	}
   115  	users, err := user.ParsePasswd(reader)
   116  	if err != nil {
   117  		return user.User{}, err
   118  	}
   119  	if len(users) == 0 {
   120  		return user.User{}, fmt.Errorf("getent failed to find passwd entry for %q", name)
   121  	}
   122  	return users[0], nil
   123  }
   124  
   125  // LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name,
   126  // followed by a call to `getent` for supporting host configured non-files passwd and group dbs
   127  func LookupGroup(name string) (user.Group, error) {
   128  	// first try a local system files lookup using existing capabilities
   129  	group, err := user.LookupGroup(name)
   130  	if err == nil {
   131  		return group, nil
   132  	}
   133  	// local files lookup failed; attempt to call `getent` to query configured group dbs
   134  	return getentGroup(name)
   135  }
   136  
   137  // LookupGID uses traditional local system files lookup (from libcontainer/user) on a group ID,
   138  // followed by a call to `getent` for supporting host configured non-files passwd and group dbs
   139  func LookupGID(gid int) (user.Group, error) {
   140  	// first try a local system files lookup using existing capabilities
   141  	group, err := user.LookupGid(gid)
   142  	if err == nil {
   143  		return group, nil
   144  	}
   145  	// local files lookup failed; attempt to call `getent` to query configured group dbs
   146  	return getentGroup(strconv.Itoa(gid))
   147  }
   148  
   149  func getentGroup(name string) (user.Group, error) {
   150  	reader, err := callGetent("group", name)
   151  	if err != nil {
   152  		return user.Group{}, err
   153  	}
   154  	groups, err := user.ParseGroup(reader)
   155  	if err != nil {
   156  		return user.Group{}, err
   157  	}
   158  	if len(groups) == 0 {
   159  		return user.Group{}, fmt.Errorf("getent failed to find groups entry for %q", name)
   160  	}
   161  	return groups[0], nil
   162  }
   163  
   164  func callGetent(database, key string) (io.Reader, error) {
   165  	entOnce.Do(func() { getentCmd, _ = resolveBinary("getent") })
   166  	// if no `getent` command on host, can't do anything else
   167  	if getentCmd == "" {
   168  		return nil, fmt.Errorf("unable to find getent command")
   169  	}
   170  	out, err := exec.Command(getentCmd, database, key).CombinedOutput()
   171  	if err != nil {
   172  		exitCode, errC := getExitCode(err)
   173  		if errC != nil {
   174  			return nil, err
   175  		}
   176  		switch exitCode {
   177  		case 1:
   178  			return nil, fmt.Errorf("getent reported invalid parameters/database unknown")
   179  		case 2:
   180  			return nil, fmt.Errorf("getent unable to find entry %q in %s database", key, database)
   181  		case 3:
   182  			return nil, fmt.Errorf("getent database doesn't support enumeration")
   183  		default:
   184  			return nil, err
   185  		}
   186  	}
   187  	return bytes.NewReader(out), nil
   188  }
   189  
   190  // getExitCode returns the ExitStatus of the specified error if its type is
   191  // exec.ExitError, returns 0 and an error otherwise.
   192  func getExitCode(err error) (int, error) {
   193  	exitCode := 0
   194  	if exiterr, ok := err.(*exec.ExitError); ok {
   195  		if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok {
   196  			return procExit.ExitStatus(), nil
   197  		}
   198  	}
   199  	return exitCode, fmt.Errorf("failed to get exit code")
   200  }
   201  
   202  // setPermissions performs a chown/chmod only if the uid/gid don't match what's requested
   203  // 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
   204  // dir is on an NFS share, so don't call chown unless we absolutely must.
   205  // Likewise for setting permissions.
   206  func setPermissions(p string, mode os.FileMode, owner Identity, stat os.FileInfo) error {
   207  	if stat == nil {
   208  		var err error
   209  		stat, err = os.Stat(p)
   210  		if err != nil {
   211  			return err
   212  		}
   213  	}
   214  	if stat.Mode().Perm() != mode.Perm() {
   215  		if err := os.Chmod(p, mode.Perm()); err != nil {
   216  			return err
   217  		}
   218  	}
   219  	ssi := stat.Sys().(*syscall.Stat_t)
   220  	if ssi.Uid == uint32(owner.UID) && ssi.Gid == uint32(owner.GID) {
   221  		return nil
   222  	}
   223  	return os.Chown(p, owner.UID, owner.GID)
   224  }
   225  
   226  // LoadIdentityMapping takes a requested username and
   227  // using the data from /etc/sub{uid,gid} ranges, creates the
   228  // proper uid and gid remapping ranges for that user/group pair
   229  func LoadIdentityMapping(name string) (IdentityMapping, error) {
   230  	usr, err := LookupUser(name)
   231  	if err != nil {
   232  		return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err)
   233  	}
   234  
   235  	subuidRanges, err := lookupSubUIDRanges(usr)
   236  	if err != nil {
   237  		return IdentityMapping{}, err
   238  	}
   239  	subgidRanges, err := lookupSubGIDRanges(usr)
   240  	if err != nil {
   241  		return IdentityMapping{}, err
   242  	}
   243  
   244  	return IdentityMapping{
   245  		UIDMaps: subuidRanges,
   246  		GIDMaps: subgidRanges,
   247  	}, nil
   248  }
   249  
   250  func lookupSubUIDRanges(usr user.User) ([]IDMap, error) {
   251  	rangeList, err := parseSubuid(strconv.Itoa(usr.Uid))
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	if len(rangeList) == 0 {
   256  		rangeList, err = parseSubuid(usr.Name)
   257  		if err != nil {
   258  			return nil, err
   259  		}
   260  	}
   261  	if len(rangeList) == 0 {
   262  		return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name)
   263  	}
   264  	return createIDMap(rangeList), nil
   265  }
   266  
   267  func lookupSubGIDRanges(usr user.User) ([]IDMap, error) {
   268  	rangeList, err := parseSubgid(strconv.Itoa(usr.Uid))
   269  	if err != nil {
   270  		return nil, err
   271  	}
   272  	if len(rangeList) == 0 {
   273  		rangeList, err = parseSubgid(usr.Name)
   274  		if err != nil {
   275  			return nil, err
   276  		}
   277  	}
   278  	if len(rangeList) == 0 {
   279  		return nil, fmt.Errorf("no subgid ranges found for user %q", usr.Name)
   280  	}
   281  	return createIDMap(rangeList), nil
   282  }