Browse code

Move UserLookup functionality into a separate pkg/user submodule that implements proper parsing of /etc/passwd and /etc/group, and use that to add support for "docker run -u user:group" and for getting supplementary groups (if ":group" is not specified)

Docker-DCO-1.1-Signed-off-by: Andrew Page <admwiggin@gmail.com> (github: tianon)

Tianon Gravi authored on 2013/12/28 02:47:42
Showing 6 changed files
... ...
@@ -4,11 +4,10 @@ import (
4 4
 	"fmt"
5 5
 	"github.com/dotcloud/docker/execdriver"
6 6
 	"github.com/dotcloud/docker/pkg/netlink"
7
-	"github.com/dotcloud/docker/utils"
7
+	"github.com/dotcloud/docker/pkg/user"
8 8
 	"github.com/syndtr/gocapability/capability"
9 9
 	"net"
10 10
 	"os"
11
-	"strconv"
12 11
 	"strings"
13 12
 	"syscall"
14 13
 )
... ...
@@ -79,28 +78,22 @@ func setupWorkingDirectory(args *execdriver.InitArgs) error {
79 79
 
80 80
 // Takes care of dropping privileges to the desired user
81 81
 func changeUser(args *execdriver.InitArgs) error {
82
-	if args.User == "" {
83
-		return nil
84
-	}
85
-	userent, err := utils.UserLookup(args.User)
82
+	uid, gid, suppGids, err := user.GetUserGroupSupplementary(
83
+		args.User,
84
+		syscall.Getuid(), syscall.Getgid(),
85
+	)
86 86
 	if err != nil {
87
-		return fmt.Errorf("Unable to find user %v: %v", args.User, err)
87
+		return err
88 88
 	}
89 89
 
90
-	uid, err := strconv.Atoi(userent.Uid)
91
-	if err != nil {
92
-		return fmt.Errorf("Invalid uid: %v", userent.Uid)
90
+	if err := syscall.Setgroups(suppGids); err != nil {
91
+		return fmt.Errorf("Setgroups failed: %v", err)
93 92
 	}
94
-	gid, err := strconv.Atoi(userent.Gid)
95
-	if err != nil {
96
-		return fmt.Errorf("Invalid gid: %v", userent.Gid)
97
-	}
98
-
99 93
 	if err := syscall.Setgid(gid); err != nil {
100
-		return fmt.Errorf("setgid failed: %v", err)
94
+		return fmt.Errorf("Setgid failed: %v", err)
101 95
 	}
102 96
 	if err := syscall.Setuid(uid); err != nil {
103
-		return fmt.Errorf("setuid failed: %v", err)
97
+		return fmt.Errorf("Setuid failed: %v", err)
104 98
 	}
105 99
 
106 100
 	return nil
... ...
@@ -148,6 +148,86 @@ RUN [ "$(/hello.sh)" = "hello world" ]
148 148
 		nil,
149 149
 	},
150 150
 
151
+	// Users and groups
152
+	{
153
+		`
154
+FROM {IMAGE}
155
+
156
+# Make sure our defaults work
157
+RUN [ "$(id -u):$(id -g)" = '0:0' ]
158
+RUN [ "$(id -un):$(id -gn)" = 'root:root' ]
159
+
160
+# TODO decide if "args.user = strconv.Itoa(syscall.Getuid())" is acceptable behavior for changeUser in sysvinit instead of "return nil" when "USER" isn't specified (so that we get the proper group list even if that is the empty list, even in the default case of not supplying an explicit USER to run as, which implies USER 0)
161
+USER root
162
+RUN [ "$(id -G) -- $(id -Gn)" = '0 -- root' ]
163
+
164
+# Setup dockerio user and group
165
+RUN echo 'dockerio:x:1000:1000::/bin:/bin/false' >> /etc/passwd
166
+RUN echo 'dockerio:x:1000:' >> /etc/group
167
+
168
+# Make sure we can switch to our user and all the information is exactly as we expect it to be
169
+USER dockerio
170
+RUN [ "$(id -u):$(id -g)" = '1000:1000' ]
171
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:dockerio' ]
172
+RUN [ "$(id -G) -- $(id -Gn)" = '1000 -- dockerio' ]
173
+
174
+# Switch back to root and double check that worked exactly as we might expect it to
175
+USER root
176
+RUN [ "$(id -u):$(id -g)" = '0:0' ]
177
+RUN [ "$(id -un):$(id -gn)" = 'root:root' ]
178
+RUN [ "$(id -G) -- $(id -Gn)" = '0 -- root' ]
179
+
180
+# Add a "supplementary" group for our dockerio user
181
+RUN echo 'supplementary:x:1001:dockerio' >> /etc/group
182
+
183
+# ... and then go verify that we get it like we expect
184
+USER dockerio
185
+RUN [ "$(id -u):$(id -g)" = '1000:1000' ]
186
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:dockerio' ]
187
+RUN [ "$(id -G) -- $(id -Gn)" = '1000 1001 -- dockerio supplementary' ]
188
+USER 1000
189
+RUN [ "$(id -u):$(id -g)" = '1000:1000' ]
190
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:dockerio' ]
191
+RUN [ "$(id -G) -- $(id -Gn)" = '1000 1001 -- dockerio supplementary' ]
192
+
193
+# and finally, super test the new "user:group" syntax
194
+USER dockerio:dockerio
195
+RUN [ "$(id -u):$(id -g)" = '1000:1000' ]
196
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:dockerio' ]
197
+RUN [ "$(id -G) -- $(id -Gn)" = '1000 -- dockerio' ]
198
+USER 1000:dockerio
199
+RUN [ "$(id -u):$(id -g)" = '1000:1000' ]
200
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:dockerio' ]
201
+RUN [ "$(id -G) -- $(id -Gn)" = '1000 -- dockerio' ]
202
+USER dockerio:1000
203
+RUN [ "$(id -u):$(id -g)" = '1000:1000' ]
204
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:dockerio' ]
205
+RUN [ "$(id -G) -- $(id -Gn)" = '1000 -- dockerio' ]
206
+USER 1000:1000
207
+RUN [ "$(id -u):$(id -g)" = '1000:1000' ]
208
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:dockerio' ]
209
+RUN [ "$(id -G) -- $(id -Gn)" = '1000 -- dockerio' ]
210
+USER dockerio:supplementary
211
+RUN [ "$(id -u):$(id -g)" = '1000:1001' ]
212
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:supplementary' ]
213
+RUN [ "$(id -G) -- $(id -Gn)" = '1001 -- supplementary' ]
214
+USER dockerio:1001
215
+RUN [ "$(id -u):$(id -g)" = '1000:1001' ]
216
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:supplementary' ]
217
+RUN [ "$(id -G) -- $(id -Gn)" = '1001 -- supplementary' ]
218
+USER 1000:supplementary
219
+RUN [ "$(id -u):$(id -g)" = '1000:1001' ]
220
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:supplementary' ]
221
+RUN [ "$(id -G) -- $(id -Gn)" = '1001 -- supplementary' ]
222
+USER 1000:1001
223
+RUN [ "$(id -u):$(id -g)" = '1000:1001' ]
224
+RUN [ "$(id -un):$(id -gn)" = 'dockerio:supplementary' ]
225
+RUN [ "$(id -G) -- $(id -Gn)" = '1001 -- supplementary' ]
226
+`,
227
+		nil,
228
+		nil,
229
+	},
230
+
151 231
 	// Environment variable
152 232
 	{
153 233
 		`
154 234
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+Tianon Gravi <admwiggin@gmail.com> (@tianon)
0 1
new file mode 100644
... ...
@@ -0,0 +1,245 @@
0
+package user
1
+
2
+import (
3
+	"bufio"
4
+	"fmt"
5
+	"io"
6
+	"os"
7
+	"reflect"
8
+	"strconv"
9
+	"strings"
10
+)
11
+
12
+type User struct {
13
+	Name  string
14
+	Pass  string
15
+	Uid   int
16
+	Gid   int
17
+	Gecos string
18
+	Home  string
19
+	Shell string
20
+}
21
+
22
+type Group struct {
23
+	Name string
24
+	Pass string
25
+	Gid  int
26
+	List []string
27
+}
28
+
29
+func parseLine(line string, v ...interface{}) {
30
+	if line == "" {
31
+		return
32
+	}
33
+
34
+	parts := strings.Split(line, ":")
35
+	for i, p := range parts {
36
+		if len(v) <= i {
37
+			// if we have more "parts" than we have places to put them, bail for great "tolerance" of naughty configuration files
38
+			break
39
+		}
40
+
41
+		t := reflect.TypeOf(v[i])
42
+		if t.Kind() != reflect.Ptr {
43
+			// panic, because this is a programming/logic error, not a runtime one
44
+			panic("parseLine expects only pointers!  argument " + strconv.Itoa(i) + " is not a pointer!")
45
+		}
46
+
47
+		switch t.Elem().Kind() {
48
+		case reflect.String:
49
+			// "root", "adm", "/bin/bash"
50
+			*v[i].(*string) = p
51
+		case reflect.Int:
52
+			// "0", "4", "1000"
53
+			*v[i].(*int), _ = strconv.Atoi(p)
54
+			// ignore string to int conversion errors, for great "tolerance" of naughty configuration files
55
+		case reflect.Slice, reflect.Array:
56
+			// "", "root", "root,adm,daemon"
57
+			list := []string{}
58
+			if p != "" {
59
+				list = strings.Split(p, ",")
60
+			}
61
+			*v[i].(*[]string) = list
62
+		}
63
+	}
64
+}
65
+
66
+func ParsePasswd() ([]*User, error) {
67
+	return ParsePasswdFilter(nil)
68
+}
69
+
70
+func ParsePasswdFilter(filter func(*User) bool) ([]*User, error) {
71
+	f, err := os.Open("/etc/passwd")
72
+	if err != nil {
73
+		return nil, err
74
+	}
75
+	defer f.Close()
76
+	return parsePasswdFile(f, filter)
77
+}
78
+
79
+func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
80
+	var (
81
+		s   = bufio.NewScanner(r)
82
+		out = []*User{}
83
+	)
84
+
85
+	for s.Scan() {
86
+		if err := s.Err(); err != nil {
87
+			return nil, err
88
+		}
89
+
90
+		text := strings.TrimSpace(s.Text())
91
+		if text == "" {
92
+			continue
93
+		}
94
+
95
+		// see: man 5 passwd
96
+		//  name:password:UID:GID:GECOS:directory:shell
97
+		// Name:Pass:Uid:Gid:Gecos:Home:Shell
98
+		//  root:x:0:0:root:/root:/bin/bash
99
+		//  adm:x:3:4:adm:/var/adm:/bin/false
100
+		p := &User{}
101
+		parseLine(
102
+			text,
103
+			&p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell,
104
+		)
105
+
106
+		if filter == nil || filter(p) {
107
+			out = append(out, p)
108
+		}
109
+	}
110
+
111
+	return out, nil
112
+}
113
+
114
+func ParseGroup() ([]*Group, error) {
115
+	return ParseGroupFilter(nil)
116
+}
117
+
118
+func ParseGroupFilter(filter func(*Group) bool) ([]*Group, error) {
119
+	f, err := os.Open("/etc/group")
120
+	if err != nil {
121
+		return nil, err
122
+	}
123
+	defer f.Close()
124
+	return parseGroupFile(f, filter)
125
+}
126
+
127
+func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
128
+	var (
129
+		s   = bufio.NewScanner(r)
130
+		out = []*Group{}
131
+	)
132
+
133
+	for s.Scan() {
134
+		if err := s.Err(); err != nil {
135
+			return nil, err
136
+		}
137
+
138
+		text := s.Text()
139
+		if text == "" {
140
+			continue
141
+		}
142
+
143
+		// see: man 5 group
144
+		//  group_name:password:GID:user_list
145
+		// Name:Pass:Gid:List
146
+		//  root:x:0:root
147
+		//  adm:x:4:root,adm,daemon
148
+		p := &Group{}
149
+		parseLine(
150
+			text,
151
+			&p.Name, &p.Pass, &p.Gid, &p.List,
152
+		)
153
+
154
+		if filter == nil || filter(p) {
155
+			out = append(out, p)
156
+		}
157
+	}
158
+
159
+	return out, nil
160
+}
161
+
162
+// Given a string like "user", "1000", "user:group", "1000:1000", returns the uid, gid, and list of supplementary group IDs, if possible.
163
+func GetUserGroupSupplementary(userSpec string, defaultUid int, defaultGid int) (int, int, []int, error) {
164
+	var (
165
+		uid      = defaultUid
166
+		gid      = defaultGid
167
+		suppGids = []int{}
168
+
169
+		userArg, groupArg string
170
+	)
171
+
172
+	// allow for userArg to have either "user" syntax, or optionally "user:group" syntax
173
+	parseLine(userSpec, &userArg, &groupArg)
174
+
175
+	users, err := ParsePasswdFilter(func(u *User) bool {
176
+		if userArg == "" {
177
+			return u.Uid == uid
178
+		}
179
+		return u.Name == userArg || strconv.Itoa(u.Uid) == userArg
180
+	})
181
+	if err != nil && !os.IsNotExist(err) {
182
+		if userArg == "" {
183
+			userArg = strconv.Itoa(uid)
184
+		}
185
+		return 0, 0, nil, fmt.Errorf("Unable to find user %v: %v", userArg, err)
186
+	}
187
+
188
+	haveUser := users != nil && len(users) > 0
189
+	if haveUser {
190
+		// if we found any user entries that matched our filter, let's take the first one as "correct"
191
+		uid = users[0].Uid
192
+		gid = users[0].Gid
193
+	} else if userArg != "" {
194
+		// we asked for a user but didn't find them...  let's check to see if we wanted a numeric user
195
+		uid, err = strconv.Atoi(userArg)
196
+		if err != nil {
197
+			// not numeric - we have to bail
198
+			return 0, 0, nil, fmt.Errorf("Unable to find user %v", userArg)
199
+		}
200
+
201
+		// if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit
202
+	}
203
+
204
+	if groupArg != "" || (haveUser && users[0].Name != "") {
205
+		groups, err := ParseGroupFilter(func(g *Group) bool {
206
+			if groupArg != "" {
207
+				return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg
208
+			}
209
+			for _, u := range g.List {
210
+				if u == users[0].Name {
211
+					return true
212
+				}
213
+			}
214
+			return false
215
+		})
216
+		if err != nil && !os.IsNotExist(err) {
217
+			return 0, 0, nil, fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
218
+		}
219
+
220
+		haveGroup := groups != nil && len(groups) > 0
221
+		if groupArg != "" {
222
+			if haveGroup {
223
+				// if we found any group entries that matched our filter, let's take the first one as "correct"
224
+				gid = groups[0].Gid
225
+			} else {
226
+				// we asked for a group but didn't find id...  let's check to see if we wanted a numeric group
227
+				gid, err = strconv.Atoi(groupArg)
228
+				if err != nil {
229
+					// not numeric - we have to bail
230
+					return 0, 0, nil, fmt.Errorf("Unable to find group %v", groupArg)
231
+				}
232
+
233
+				// if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit
234
+			}
235
+		} else if haveGroup {
236
+			suppGids = make([]int, len(groups))
237
+			for i, group := range groups {
238
+				suppGids[i] = group.Gid
239
+			}
240
+		}
241
+	}
242
+
243
+	return uid, gid, suppGids, nil
244
+}
0 245
new file mode 100644
... ...
@@ -0,0 +1,94 @@
0
+package user
1
+
2
+import (
3
+	"strings"
4
+	"testing"
5
+)
6
+
7
+func TestUserParseLine(t *testing.T) {
8
+	var (
9
+		a, b string
10
+		c    []string
11
+		d    int
12
+	)
13
+
14
+	parseLine("", &a, &b)
15
+	if a != "" || b != "" {
16
+		t.Fatalf("a and b should be empty ('%v', '%v')", a, b)
17
+	}
18
+
19
+	parseLine("a", &a, &b)
20
+	if a != "a" || b != "" {
21
+		t.Fatalf("a should be 'a' and b should be empty ('%v', '%v')", a, b)
22
+	}
23
+
24
+	parseLine("bad boys:corny cows", &a, &b)
25
+	if a != "bad boys" || b != "corny cows" {
26
+		t.Fatalf("a should be 'bad boys' and b should be 'corny cows' ('%v', '%v')", a, b)
27
+	}
28
+
29
+	parseLine("", &c)
30
+	if len(c) != 0 {
31
+		t.Fatalf("c should be empty (%#v)", c)
32
+	}
33
+
34
+	parseLine("d,e,f:g:h:i,j,k", &c, &a, &b, &c)
35
+	if a != "g" || b != "h" || len(c) != 3 || c[0] != "i" || c[1] != "j" || c[2] != "k" {
36
+		t.Fatalf("a should be 'g', b should be 'h', and c should be ['i','j','k'] ('%v', '%v', '%#v')", a, b, c)
37
+	}
38
+
39
+	parseLine("::::::::::", &a, &b, &c)
40
+	if a != "" || b != "" || len(c) != 0 {
41
+		t.Fatalf("a, b, and c should all be empty ('%v', '%v', '%#v')", a, b, c)
42
+	}
43
+
44
+	parseLine("not a number", &d)
45
+	if d != 0 {
46
+		t.Fatalf("d should be 0 (%v)", d)
47
+	}
48
+
49
+	parseLine("b:12:c", &a, &d, &b)
50
+	if a != "b" || b != "c" || d != 12 {
51
+		t.Fatalf("a should be 'b' and b should be 'c', and d should be 12 ('%v', '%v', %v)", a, b, d)
52
+	}
53
+}
54
+
55
+func TestUserParsePasswd(t *testing.T) {
56
+	users, err := parsePasswdFile(strings.NewReader(`
57
+root:x:0:0:root:/root:/bin/bash
58
+adm:x:3:4:adm:/var/adm:/bin/false
59
+this is just some garbage data
60
+`), nil)
61
+	if err != nil {
62
+		t.Fatalf("Unexpected error: %v", err)
63
+	}
64
+	if len(users) != 3 {
65
+		t.Fatalf("Expected 3 users, got %v", len(users))
66
+	}
67
+	if users[0].Uid != 0 || users[0].Name != "root" {
68
+		t.Fatalf("Expected users[0] to be 0 - root, got %v - %v", users[0].Uid, users[0].Name)
69
+	}
70
+	if users[1].Uid != 3 || users[1].Name != "adm" {
71
+		t.Fatalf("Expected users[1] to be 3 - adm, got %v - %v", users[1].Uid, users[1].Name)
72
+	}
73
+}
74
+
75
+func TestUserParseGroup(t *testing.T) {
76
+	groups, err := parseGroupFile(strings.NewReader(`
77
+root:x:0:root
78
+adm:x:4:root,adm,daemon
79
+this is just some garbage data
80
+`), nil)
81
+	if err != nil {
82
+		t.Fatalf("Unexpected error: %v", err)
83
+	}
84
+	if len(groups) != 3 {
85
+		t.Fatalf("Expected 3 groups, got %v", len(groups))
86
+	}
87
+	if groups[0].Gid != 0 || groups[0].Name != "root" || len(groups[0].List) != 1 {
88
+		t.Fatalf("Expected groups[0] to be 0 - root - 1 member, got %v - %v - %v", groups[0].Gid, groups[0].Name, len(groups[0].List))
89
+	}
90
+	if groups[1].Gid != 4 || groups[1].Name != "adm" || len(groups[1].List) != 3 {
91
+		t.Fatalf("Expected groups[1] to be 4 - adm - 3 members, got %v - %v - %v", groups[1].Gid, groups[1].Name, len(groups[1].List))
92
+	}
93
+}
... ...
@@ -836,37 +836,6 @@ func ParseRepositoryTag(repos string) (string, string) {
836 836
 	return repos, ""
837 837
 }
838 838
 
839
-type User struct {
840
-	Uid      string // user id
841
-	Gid      string // primary group id
842
-	Username string
843
-	Name     string
844
-	HomeDir  string
845
-}
846
-
847
-// UserLookup check if the given username or uid is present in /etc/passwd
848
-// and returns the user struct.
849
-// If the username is not found, an error is returned.
850
-func UserLookup(uid string) (*User, error) {
851
-	file, err := ioutil.ReadFile("/etc/passwd")
852
-	if err != nil {
853
-		return nil, err
854
-	}
855
-	for _, line := range strings.Split(string(file), "\n") {
856
-		data := strings.Split(line, ":")
857
-		if len(data) > 5 && (data[0] == uid || data[2] == uid) {
858
-			return &User{
859
-				Uid:      data[2],
860
-				Gid:      data[3],
861
-				Username: data[0],
862
-				Name:     data[4],
863
-				HomeDir:  data[5],
864
-			}, nil
865
-		}
866
-	}
867
-	return nil, fmt.Errorf("User not found in /etc/passwd")
868
-}
869
-
870 839
 // An StatusError reports an unsuccessful exit by a command.
871 840
 type StatusError struct {
872 841
 	Status     string