github.com/endophage/docker@v1.4.2-0.20161027011718-242853499895/pkg/idtools/usergroupadd_linux.go (about) 1 package idtools 2 3 import ( 4 "fmt" 5 "os/exec" 6 "path/filepath" 7 "regexp" 8 "sort" 9 "strconv" 10 "strings" 11 "sync" 12 ) 13 14 // add a user and/or group to Linux /etc/passwd, /etc/group using standard 15 // Linux distribution commands: 16 // adduser --system --shell /bin/false --disabled-login --disabled-password --no-create-home --group <username> 17 // useradd -r -s /bin/false <username> 18 19 var ( 20 once sync.Once 21 userCommand string 22 23 cmdTemplates = map[string]string{ 24 "adduser": "--system --shell /bin/false --no-create-home --disabled-login --disabled-password --group %s", 25 "useradd": "-r -s /bin/false %s", 26 "usermod": "-%s %d-%d %s", 27 } 28 29 idOutRegexp = regexp.MustCompile(`uid=([0-9]+).*gid=([0-9]+)`) 30 // default length for a UID/GID subordinate range 31 defaultRangeLen = 65536 32 defaultRangeStart = 100000 33 userMod = "usermod" 34 ) 35 36 func resolveBinary(binname string) (string, error) { 37 binaryPath, err := exec.LookPath(binname) 38 if err != nil { 39 return "", err 40 } 41 resolvedPath, err := filepath.EvalSymlinks(binaryPath) 42 if err != nil { 43 return "", err 44 } 45 //only return no error if the final resolved binary basename 46 //matches what was searched for 47 if filepath.Base(resolvedPath) == binname { 48 return resolvedPath, nil 49 } 50 return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) 51 } 52 53 // AddNamespaceRangesUser takes a username and uses the standard system 54 // utility to create a system user/group pair used to hold the 55 // /etc/sub{uid,gid} ranges which will be used for user namespace 56 // mapping ranges in containers. 57 func AddNamespaceRangesUser(name string) (int, int, error) { 58 if err := addUser(name); err != nil { 59 return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err) 60 } 61 62 // Query the system for the created uid and gid pair 63 out, err := execCmd("id", name) 64 if err != nil { 65 return -1, -1, fmt.Errorf("Error trying to find uid/gid for new user %q: %v", name, err) 66 } 67 matches := idOutRegexp.FindStringSubmatch(strings.TrimSpace(string(out))) 68 if len(matches) != 3 { 69 return -1, -1, fmt.Errorf("Can't find uid, gid from `id` output: %q", string(out)) 70 } 71 uid, err := strconv.Atoi(matches[1]) 72 if err != nil { 73 return -1, -1, fmt.Errorf("Can't convert found uid (%s) to int: %v", matches[1], err) 74 } 75 gid, err := strconv.Atoi(matches[2]) 76 if err != nil { 77 return -1, -1, fmt.Errorf("Can't convert found gid (%s) to int: %v", matches[2], err) 78 } 79 80 // Now we need to create the subuid/subgid ranges for our new user/group (system users 81 // do not get auto-created ranges in subuid/subgid) 82 83 if err := createSubordinateRanges(name); err != nil { 84 return -1, -1, fmt.Errorf("Couldn't create subordinate ID ranges: %v", err) 85 } 86 return uid, gid, nil 87 } 88 89 func addUser(userName string) error { 90 once.Do(func() { 91 // set up which commands are used for adding users/groups dependent on distro 92 if _, err := resolveBinary("adduser"); err == nil { 93 userCommand = "adduser" 94 } else if _, err := resolveBinary("useradd"); err == nil { 95 userCommand = "useradd" 96 } 97 }) 98 if userCommand == "" { 99 return fmt.Errorf("Cannot add user; no useradd/adduser binary found") 100 } 101 args := fmt.Sprintf(cmdTemplates[userCommand], userName) 102 out, err := execCmd(userCommand, args) 103 if err != nil { 104 return fmt.Errorf("Failed to add user with error: %v; output: %q", err, string(out)) 105 } 106 return nil 107 } 108 109 func createSubordinateRanges(name string) error { 110 111 // first, we should verify that ranges weren't automatically created 112 // by the distro tooling 113 ranges, err := parseSubuid(name) 114 if err != nil { 115 return fmt.Errorf("Error while looking for subuid ranges for user %q: %v", name, err) 116 } 117 if len(ranges) == 0 { 118 // no UID ranges; let's create one 119 startID, err := findNextUIDRange() 120 if err != nil { 121 return fmt.Errorf("Can't find available subuid range: %v", err) 122 } 123 out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "v", startID, startID+defaultRangeLen-1, name)) 124 if err != nil { 125 return fmt.Errorf("Unable to add subuid range to user: %q; output: %s, err: %v", name, out, err) 126 } 127 } 128 129 ranges, err = parseSubgid(name) 130 if err != nil { 131 return fmt.Errorf("Error while looking for subgid ranges for user %q: %v", name, err) 132 } 133 if len(ranges) == 0 { 134 // no GID ranges; let's create one 135 startID, err := findNextGIDRange() 136 if err != nil { 137 return fmt.Errorf("Can't find available subgid range: %v", err) 138 } 139 out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "w", startID, startID+defaultRangeLen-1, name)) 140 if err != nil { 141 return fmt.Errorf("Unable to add subgid range to user: %q; output: %s, err: %v", name, out, err) 142 } 143 } 144 return nil 145 } 146 147 func findNextUIDRange() (int, error) { 148 ranges, err := parseSubuid("ALL") 149 if err != nil { 150 return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subuid file: %v", err) 151 } 152 sort.Sort(ranges) 153 return findNextRangeStart(ranges) 154 } 155 156 func findNextGIDRange() (int, error) { 157 ranges, err := parseSubgid("ALL") 158 if err != nil { 159 return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subgid file: %v", err) 160 } 161 sort.Sort(ranges) 162 return findNextRangeStart(ranges) 163 } 164 165 func findNextRangeStart(rangeList ranges) (int, error) { 166 startID := defaultRangeStart 167 for _, arange := range rangeList { 168 if wouldOverlap(arange, startID) { 169 startID = arange.Start + arange.Length 170 } 171 } 172 return startID, nil 173 } 174 175 func wouldOverlap(arange subIDRange, ID int) bool { 176 low := ID 177 high := ID + defaultRangeLen 178 if (low >= arange.Start && low <= arange.Start+arange.Length) || 179 (high <= arange.Start+arange.Length && high >= arange.Start) { 180 return true 181 } 182 return false 183 } 184 185 func execCmd(cmd, args string) ([]byte, error) { 186 execCmd := exec.Command(cmd, strings.Split(args, " ")...) 187 return execCmd.CombinedOutput() 188 }