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