Browse code

Change subordinate range-owning user to be a system user

Change user/group creation to use flags to adduser/useradd to enforce it
being a system user. Use system user defaults that auto-create a
matching group. These changes allow us to remove all group creation
code, and in doing so we also removed the code that finds available uid,
gid integers and use post-creation query to gather the system-generated
uid and gid.

The only added complexity is that today distros don't auto-create
subordinate ID ranges for a new ID if it is a system ID, so we now need
to handle finding a free range and then calling the `usermod` tool to
add the ranges for that ID. Note that this requires the distro supports
the `-v` and `-w` flags on `usermod` for subordinate ID range additions.

Docker-DCO-1.1-Signed-off-by: Phil Estes <estesp@linux.vnet.ibm.com> (github: estesp)

Phil Estes authored on 2016/03/17 07:44:10
Showing 2 changed files
... ...
@@ -155,6 +155,9 @@ func parseSubgid(username string) (ranges, error) {
155 155
 	return parseSubidFile(subgidFileName, username)
156 156
 }
157 157
 
158
+// parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid)
159
+// and return all found ranges for a specified username. If the special value
160
+// "ALL" is supplied for username, then all ranges in the file will be returned
158 161
 func parseSubidFile(path, username string) (ranges, error) {
159 162
 	var rangeList ranges
160 163
 
... ...
@@ -178,8 +181,7 @@ func parseSubidFile(path, username string) (ranges, error) {
178 178
 		if len(parts) != 3 {
179 179
 			return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path)
180 180
 		}
181
-		if parts[0] == username {
182
-			// return the first entry for a user; ignores potential for multiple ranges per user
181
+		if parts[0] == username || username == "ALL" {
183 182
 			startid, err := strconv.Atoi(parts[1])
184 183
 			if err != nil {
185 184
 				return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
... ...
@@ -4,31 +4,31 @@ import (
4 4
 	"fmt"
5 5
 	"os/exec"
6 6
 	"path/filepath"
7
+	"regexp"
8
+	"sort"
9
+	"strconv"
7 10
 	"strings"
8
-	"syscall"
9 11
 )
10 12
 
11 13
 // add a user and/or group to Linux /etc/passwd, /etc/group using standard
12 14
 // Linux distribution commands:
13
-// adduser --uid <id> --shell /bin/login --no-create-home --disabled-login --ingroup <groupname> <username>
14
-// useradd -M -u <id> -s /bin/nologin -N -g <groupname> <username>
15
-// addgroup --gid <id> <groupname>
16
-// groupadd -g <id> <groupname>
17
-
18
-const baseUID int = 10000
19
-const baseGID int = 10000
20
-const idMAX int = 65534
15
+// adduser --system --shell /bin/false --disabled-login --disabled-password --no-create-home --group <username>
16
+// useradd -r -s /bin/false <username>
21 17
 
22 18
 var (
23
-	userCommand  string
24
-	groupCommand string
19
+	userCommand string
25 20
 
26 21
 	cmdTemplates = map[string]string{
27
-		"adduser":  "--uid %d --shell /bin/false --no-create-home --disabled-login --ingroup %s %s",
28
-		"useradd":  "-M -u %d -s /bin/false -N -g %s %s",
29
-		"addgroup": "--gid %d %s",
30
-		"groupadd": "-g %d %s",
22
+		"adduser": "--system --shell /bin/false --no-create-home --disabled-login --disabled-password --group %s",
23
+		"useradd": "-r -s /bin/false %s",
24
+		"usermod": "-%s %d-%d %s",
31 25
 	}
26
+
27
+	idOutRegexp = regexp.MustCompile(`uid=([0-9]+).*gid=([0-9]+)`)
28
+	// default length for a UID/GID subordinate range
29
+	defaultRangeLen   = 65536
30
+	defaultRangeStart = 100000
31
+	userMod           = "usermod"
32 32
 )
33 33
 
34 34
 func init() {
... ...
@@ -38,11 +38,6 @@ func init() {
38 38
 	} else if _, err := resolveBinary("useradd"); err == nil {
39 39
 		userCommand = "useradd"
40 40
 	}
41
-	if _, err := resolveBinary("addgroup"); err == nil {
42
-		groupCommand = "addgroup"
43
-	} else if _, err := resolveBinary("groupadd"); err == nil {
44
-		groupCommand = "groupadd"
45
-	}
46 41
 }
47 42
 
48 43
 func resolveBinary(binname string) (string, error) {
... ...
@@ -62,94 +57,132 @@ func resolveBinary(binname string) (string, error) {
62 62
 	return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath)
63 63
 }
64 64
 
65
-// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair
66
-// and calls the appropriate helper function to add the group and then
67
-// the user to the group in /etc/group and /etc/passwd respectively.
68
-// This new user's /etc/sub{uid,gid} ranges will be used for user namespace
65
+// AddNamespaceRangesUser takes a username and uses the standard system
66
+// utility to create a system user/group pair used to hold the
67
+// /etc/sub{uid,gid} ranges which will be used for user namespace
69 68
 // mapping ranges in containers.
70 69
 func AddNamespaceRangesUser(name string) (int, int, error) {
71
-	// Find unused uid, gid pair
72
-	uid, err := findUnusedUID(baseUID)
70
+	if err := addUser(name); err != nil {
71
+		return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err)
72
+	}
73
+
74
+	// Query the system for the created uid and gid pair
75
+	out, err := execCmd("id", name)
73 76
 	if err != nil {
74
-		return -1, -1, fmt.Errorf("Unable to find unused UID: %v", err)
77
+		return -1, -1, fmt.Errorf("Error trying to find uid/gid for new user %q: %v", name, err)
78
+	}
79
+	matches := idOutRegexp.FindStringSubmatch(strings.TrimSpace(string(out)))
80
+	if len(matches) != 3 {
81
+		return -1, -1, fmt.Errorf("Can't find uid, gid from `id` output: %q", string(out))
75 82
 	}
76
-	gid, err := findUnusedGID(baseGID)
83
+	uid, err := strconv.Atoi(matches[1])
77 84
 	if err != nil {
78
-		return -1, -1, fmt.Errorf("Unable to find unused GID: %v", err)
85
+		return -1, -1, fmt.Errorf("Can't convert found uid (%s) to int: %v", matches[1], err)
79 86
 	}
80
-
81
-	// First add the group that we will use
82
-	if err := addGroup(name, gid); err != nil {
83
-		return -1, -1, fmt.Errorf("Error adding group %q: %v", name, err)
87
+	gid, err := strconv.Atoi(matches[2])
88
+	if err != nil {
89
+		return -1, -1, fmt.Errorf("Can't convert found gid (%s) to int: %v", matches[2], err)
84 90
 	}
85
-	// Add the user as a member of the group
86
-	if err := addUser(name, uid, name); err != nil {
87
-		return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err)
91
+
92
+	// Now we need to create the subuid/subgid ranges for our new user/group (system users
93
+	// do not get auto-created ranges in subuid/subgid)
94
+
95
+	if err := createSubordinateRanges(name); err != nil {
96
+		return -1, -1, fmt.Errorf("Couldn't create subordinate ID ranges: %v", err)
88 97
 	}
89 98
 	return uid, gid, nil
90 99
 }
91 100
 
92
-func addUser(userName string, uid int, groupName string) error {
101
+func addUser(userName string) error {
93 102
 
94 103
 	if userCommand == "" {
95 104
 		return fmt.Errorf("Cannot add user; no useradd/adduser binary found")
96 105
 	}
97
-	args := fmt.Sprintf(cmdTemplates[userCommand], uid, groupName, userName)
98
-	return execAddCmd(userCommand, args)
106
+	args := fmt.Sprintf(cmdTemplates[userCommand], userName)
107
+	out, err := execCmd(userCommand, args)
108
+	if err != nil {
109
+		return fmt.Errorf("Failed to add user with error: %v; output: %q", err, string(out))
110
+	}
111
+	return nil
99 112
 }
100 113
 
101
-func addGroup(groupName string, gid int) error {
114
+func createSubordinateRanges(name string) error {
102 115
 
103
-	if groupCommand == "" {
104
-		return fmt.Errorf("Cannot add group; no groupadd/addgroup binary found")
116
+	// first, we should verify that ranges weren't automatically created
117
+	// by the distro tooling
118
+	ranges, err := parseSubuid(name)
119
+	if err != nil {
120
+		return fmt.Errorf("Error while looking for subuid ranges for user %q: %v", name, err)
105 121
 	}
106
-	args := fmt.Sprintf(cmdTemplates[groupCommand], gid, groupName)
107
-	// only error out if the error isn't that the group already exists
108
-	// if the group exists then our needs are already met
109
-	if err := execAddCmd(groupCommand, args); err != nil && !strings.Contains(err.Error(), "already exists") {
110
-		return err
122
+	if len(ranges) == 0 {
123
+		// no UID ranges; let's create one
124
+		startID, err := findNextUIDRange()
125
+		if err != nil {
126
+			return fmt.Errorf("Can't find available subuid range: %v", err)
127
+		}
128
+		out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "v", startID, startID+defaultRangeLen-1, name))
129
+		if err != nil {
130
+			return fmt.Errorf("Unable to add subuid range to user: %q; output: %s, err: %v", name, out, err)
131
+		}
111 132
 	}
112
-	return nil
113
-}
114 133
 
115
-func execAddCmd(cmd, args string) error {
116
-	execCmd := exec.Command(cmd, strings.Split(args, " ")...)
117
-	out, err := execCmd.CombinedOutput()
134
+	ranges, err = parseSubgid(name)
118 135
 	if err != nil {
119
-		return fmt.Errorf("Failed to add user/group with error: %v; output: %q", err, string(out))
136
+		return fmt.Errorf("Error while looking for subgid ranges for user %q: %v", name, err)
137
+	}
138
+	if len(ranges) == 0 {
139
+		// no GID ranges; let's create one
140
+		startID, err := findNextGIDRange()
141
+		if err != nil {
142
+			return fmt.Errorf("Can't find available subgid range: %v", err)
143
+		}
144
+		out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "w", startID, startID+defaultRangeLen-1, name))
145
+		if err != nil {
146
+			return fmt.Errorf("Unable to add subgid range to user: %q; output: %s, err: %v", name, out, err)
147
+		}
120 148
 	}
121 149
 	return nil
122 150
 }
123 151
 
124
-func findUnusedUID(startUID int) (int, error) {
125
-	return findUnused("passwd", startUID)
152
+func findNextUIDRange() (int, error) {
153
+	ranges, err := parseSubuid("ALL")
154
+	if err != nil {
155
+		return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subuid file: %v", err)
156
+	}
157
+	sort.Sort(ranges)
158
+	return findNextRangeStart(ranges)
126 159
 }
127 160
 
128
-func findUnusedGID(startGID int) (int, error) {
129
-	return findUnused("group", startGID)
161
+func findNextGIDRange() (int, error) {
162
+	ranges, err := parseSubgid("ALL")
163
+	if err != nil {
164
+		return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subgid file: %v", err)
165
+	}
166
+	sort.Sort(ranges)
167
+	return findNextRangeStart(ranges)
130 168
 }
131 169
 
132
-func findUnused(file string, id int) (int, error) {
133
-	for {
134
-		cmdStr := fmt.Sprintf("cat /etc/%s | cut -d: -f3 | grep '^%d$'", file, id)
135
-		cmd := exec.Command("sh", "-c", cmdStr)
136
-		if err := cmd.Run(); err != nil {
137
-			// if a non-zero return code occurs, then we know the ID was not found
138
-			// and is usable
139
-			if exiterr, ok := err.(*exec.ExitError); ok {
140
-				// The program has exited with an exit code != 0
141
-				if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
142
-					if status.ExitStatus() == 1 {
143
-						//no match, we can use this ID
144
-						return id, nil
145
-					}
146
-				}
147
-			}
148
-			return -1, fmt.Errorf("Error looking in /etc/%s for unused ID: %v", file, err)
149
-		}
150
-		id++
151
-		if id > idMAX {
152
-			return -1, fmt.Errorf("Maximum id in %q reached with finding unused numeric ID", file)
170
+func findNextRangeStart(rangeList ranges) (int, error) {
171
+	startID := defaultRangeStart
172
+	for _, arange := range rangeList {
173
+		if wouldOverlap(arange, startID) {
174
+			startID = arange.Start + arange.Length
153 175
 		}
154 176
 	}
177
+	return startID, nil
178
+}
179
+
180
+func wouldOverlap(arange subIDRange, ID int) bool {
181
+	low := ID
182
+	high := ID + defaultRangeLen
183
+	if (low >= arange.Start && low <= arange.Start+arange.Length) ||
184
+		(high <= arange.Start+arange.Length && high >= arange.Start) {
185
+		return true
186
+	}
187
+	return false
188
+}
189
+
190
+func execCmd(cmd, args string) ([]byte, error) {
191
+	execCmd := exec.Command(cmd, strings.Split(args, " ")...)
192
+	return execCmd.CombinedOutput()
155 193
 }