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