github.com/rawahars/moby@v24.0.4+incompatible/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 command := exec.Command(getentCmd, database, key) 171 // we run getent within container filesystem, but without /dev so /dev/null is not available for exec to mock stdin 172 command.Stdin = io.NopCloser(bytes.NewReader(nil)) 173 out, err := command.CombinedOutput() 174 if err != nil { 175 exitCode, errC := getExitCode(err) 176 if errC != nil { 177 return nil, err 178 } 179 switch exitCode { 180 case 1: 181 return nil, fmt.Errorf("getent reported invalid parameters/database unknown") 182 case 2: 183 return nil, fmt.Errorf("getent unable to find entry %q in %s database", key, database) 184 case 3: 185 return nil, fmt.Errorf("getent database doesn't support enumeration") 186 default: 187 return nil, err 188 } 189 } 190 return bytes.NewReader(out), nil 191 } 192 193 // getExitCode returns the ExitStatus of the specified error if its type is 194 // exec.ExitError, returns 0 and an error otherwise. 195 func getExitCode(err error) (int, error) { 196 exitCode := 0 197 if exiterr, ok := err.(*exec.ExitError); ok { 198 if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok { 199 return procExit.ExitStatus(), nil 200 } 201 } 202 return exitCode, fmt.Errorf("failed to get exit code") 203 } 204 205 // setPermissions performs a chown/chmod only if the uid/gid don't match what's requested 206 // 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 207 // dir is on an NFS share, so don't call chown unless we absolutely must. 208 // Likewise for setting permissions. 209 func setPermissions(p string, mode os.FileMode, owner Identity, stat os.FileInfo) error { 210 if stat == nil { 211 var err error 212 stat, err = os.Stat(p) 213 if err != nil { 214 return err 215 } 216 } 217 if stat.Mode().Perm() != mode.Perm() { 218 if err := os.Chmod(p, mode.Perm()); err != nil { 219 return err 220 } 221 } 222 ssi := stat.Sys().(*syscall.Stat_t) 223 if ssi.Uid == uint32(owner.UID) && ssi.Gid == uint32(owner.GID) { 224 return nil 225 } 226 return os.Chown(p, owner.UID, owner.GID) 227 } 228 229 // LoadIdentityMapping takes a requested username and 230 // using the data from /etc/sub{uid,gid} ranges, creates the 231 // proper uid and gid remapping ranges for that user/group pair 232 func LoadIdentityMapping(name string) (IdentityMapping, error) { 233 usr, err := LookupUser(name) 234 if err != nil { 235 return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err) 236 } 237 238 subuidRanges, err := lookupSubUIDRanges(usr) 239 if err != nil { 240 return IdentityMapping{}, err 241 } 242 subgidRanges, err := lookupSubGIDRanges(usr) 243 if err != nil { 244 return IdentityMapping{}, err 245 } 246 247 return IdentityMapping{ 248 UIDMaps: subuidRanges, 249 GIDMaps: subgidRanges, 250 }, nil 251 } 252 253 func lookupSubUIDRanges(usr user.User) ([]IDMap, error) { 254 rangeList, err := parseSubuid(strconv.Itoa(usr.Uid)) 255 if err != nil { 256 return nil, err 257 } 258 if len(rangeList) == 0 { 259 rangeList, err = parseSubuid(usr.Name) 260 if err != nil { 261 return nil, err 262 } 263 } 264 if len(rangeList) == 0 { 265 return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name) 266 } 267 return createIDMap(rangeList), nil 268 } 269 270 func lookupSubGIDRanges(usr user.User) ([]IDMap, error) { 271 rangeList, err := parseSubgid(strconv.Itoa(usr.Uid)) 272 if err != nil { 273 return nil, err 274 } 275 if len(rangeList) == 0 { 276 rangeList, err = parseSubgid(usr.Name) 277 if err != nil { 278 return nil, err 279 } 280 } 281 if len(rangeList) == 0 { 282 return nil, fmt.Errorf("no subgid ranges found for user %q", usr.Name) 283 } 284 return createIDMap(rangeList), nil 285 }