idtools_unix.go 8.7 KB

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