Skip to content

Commit 9331e29

Browse files
committed
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 <[email protected]> (github: tianon)
0 parents  commit 9331e29

3 files changed

Lines changed: 340 additions & 0 deletions

File tree

user/MAINTAINERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Tianon Gravi <[email protected]> (@tianon)

user/user.go

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package user
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"io"
7+
"os"
8+
"reflect"
9+
"strconv"
10+
"strings"
11+
)
12+
13+
type User struct {
14+
Name string
15+
Pass string
16+
Uid int
17+
Gid int
18+
Gecos string
19+
Home string
20+
Shell string
21+
}
22+
23+
type Group struct {
24+
Name string
25+
Pass string
26+
Gid int
27+
List []string
28+
}
29+
30+
func parseLine(line string, v ...interface{}) {
31+
if line == "" {
32+
return
33+
}
34+
35+
parts := strings.Split(line, ":")
36+
for i, p := range parts {
37+
if len(v) <= i {
38+
// if we have more "parts" than we have places to put them, bail for great "tolerance" of naughty configuration files
39+
break
40+
}
41+
42+
t := reflect.TypeOf(v[i])
43+
if t.Kind() != reflect.Ptr {
44+
// panic, because this is a programming/logic error, not a runtime one
45+
panic("parseLine expects only pointers! argument " + strconv.Itoa(i) + " is not a pointer!")
46+
}
47+
48+
switch t.Elem().Kind() {
49+
case reflect.String:
50+
// "root", "adm", "/bin/bash"
51+
*v[i].(*string) = p
52+
case reflect.Int:
53+
// "0", "4", "1000"
54+
*v[i].(*int), _ = strconv.Atoi(p)
55+
// ignore string to int conversion errors, for great "tolerance" of naughty configuration files
56+
case reflect.Slice, reflect.Array:
57+
// "", "root", "root,adm,daemon"
58+
list := []string{}
59+
if p != "" {
60+
list = strings.Split(p, ",")
61+
}
62+
*v[i].(*[]string) = list
63+
}
64+
}
65+
}
66+
67+
func ParsePasswd() ([]*User, error) {
68+
return ParsePasswdFilter(nil)
69+
}
70+
71+
func ParsePasswdFilter(filter func(*User) bool) ([]*User, error) {
72+
f, err := os.Open("/etc/passwd")
73+
if err != nil {
74+
return nil, err
75+
}
76+
defer f.Close()
77+
return parsePasswdFile(f, filter)
78+
}
79+
80+
func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
81+
var (
82+
s = bufio.NewScanner(r)
83+
out = []*User{}
84+
)
85+
86+
for s.Scan() {
87+
if err := s.Err(); err != nil {
88+
return nil, err
89+
}
90+
91+
text := strings.TrimSpace(s.Text())
92+
if text == "" {
93+
continue
94+
}
95+
96+
// see: man 5 passwd
97+
// name:password:UID:GID:GECOS:directory:shell
98+
// Name:Pass:Uid:Gid:Gecos:Home:Shell
99+
// root:x:0:0:root:/root:/bin/bash
100+
// adm:x:3:4:adm:/var/adm:/bin/false
101+
p := &User{}
102+
parseLine(
103+
text,
104+
&p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell,
105+
)
106+
107+
if filter == nil || filter(p) {
108+
out = append(out, p)
109+
}
110+
}
111+
112+
return out, nil
113+
}
114+
115+
func ParseGroup() ([]*Group, error) {
116+
return ParseGroupFilter(nil)
117+
}
118+
119+
func ParseGroupFilter(filter func(*Group) bool) ([]*Group, error) {
120+
f, err := os.Open("/etc/group")
121+
if err != nil {
122+
return nil, err
123+
}
124+
defer f.Close()
125+
return parseGroupFile(f, filter)
126+
}
127+
128+
func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
129+
var (
130+
s = bufio.NewScanner(r)
131+
out = []*Group{}
132+
)
133+
134+
for s.Scan() {
135+
if err := s.Err(); err != nil {
136+
return nil, err
137+
}
138+
139+
text := s.Text()
140+
if text == "" {
141+
continue
142+
}
143+
144+
// see: man 5 group
145+
// group_name:password:GID:user_list
146+
// Name:Pass:Gid:List
147+
// root:x:0:root
148+
// adm:x:4:root,adm,daemon
149+
p := &Group{}
150+
parseLine(
151+
text,
152+
&p.Name, &p.Pass, &p.Gid, &p.List,
153+
)
154+
155+
if filter == nil || filter(p) {
156+
out = append(out, p)
157+
}
158+
}
159+
160+
return out, nil
161+
}
162+
163+
// Given a string like "user", "1000", "user:group", "1000:1000", returns the uid, gid, and list of supplementary group IDs, if possible.
164+
func GetUserGroupSupplementary(userSpec string, defaultUid int, defaultGid int) (int, int, []int, error) {
165+
var (
166+
uid = defaultUid
167+
gid = defaultGid
168+
suppGids = []int{}
169+
170+
userArg, groupArg string
171+
)
172+
173+
// allow for userArg to have either "user" syntax, or optionally "user:group" syntax
174+
parseLine(userSpec, &userArg, &groupArg)
175+
176+
users, err := ParsePasswdFilter(func(u *User) bool {
177+
if userArg == "" {
178+
return u.Uid == uid
179+
}
180+
return u.Name == userArg || strconv.Itoa(u.Uid) == userArg
181+
})
182+
if err != nil && !os.IsNotExist(err) {
183+
if userArg == "" {
184+
userArg = strconv.Itoa(uid)
185+
}
186+
return 0, 0, nil, fmt.Errorf("Unable to find user %v: %v", userArg, err)
187+
}
188+
189+
haveUser := users != nil && len(users) > 0
190+
if haveUser {
191+
// if we found any user entries that matched our filter, let's take the first one as "correct"
192+
uid = users[0].Uid
193+
gid = users[0].Gid
194+
} else if userArg != "" {
195+
// we asked for a user but didn't find them... let's check to see if we wanted a numeric user
196+
uid, err = strconv.Atoi(userArg)
197+
if err != nil {
198+
// not numeric - we have to bail
199+
return 0, 0, nil, fmt.Errorf("Unable to find user %v", userArg)
200+
}
201+
202+
// if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit
203+
}
204+
205+
if groupArg != "" || (haveUser && users[0].Name != "") {
206+
groups, err := ParseGroupFilter(func(g *Group) bool {
207+
if groupArg != "" {
208+
return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg
209+
}
210+
for _, u := range g.List {
211+
if u == users[0].Name {
212+
return true
213+
}
214+
}
215+
return false
216+
})
217+
if err != nil && !os.IsNotExist(err) {
218+
return 0, 0, nil, fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
219+
}
220+
221+
haveGroup := groups != nil && len(groups) > 0
222+
if groupArg != "" {
223+
if haveGroup {
224+
// if we found any group entries that matched our filter, let's take the first one as "correct"
225+
gid = groups[0].Gid
226+
} else {
227+
// we asked for a group but didn't find id... let's check to see if we wanted a numeric group
228+
gid, err = strconv.Atoi(groupArg)
229+
if err != nil {
230+
// not numeric - we have to bail
231+
return 0, 0, nil, fmt.Errorf("Unable to find group %v", groupArg)
232+
}
233+
234+
// if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit
235+
}
236+
} else if haveGroup {
237+
suppGids = make([]int, len(groups))
238+
for i, group := range groups {
239+
suppGids[i] = group.Gid
240+
}
241+
}
242+
}
243+
244+
return uid, gid, suppGids, nil
245+
}

user/user_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package user
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestUserParseLine(t *testing.T) {
9+
var (
10+
a, b string
11+
c []string
12+
d int
13+
)
14+
15+
parseLine("", &a, &b)
16+
if a != "" || b != "" {
17+
t.Fatalf("a and b should be empty ('%v', '%v')", a, b)
18+
}
19+
20+
parseLine("a", &a, &b)
21+
if a != "a" || b != "" {
22+
t.Fatalf("a should be 'a' and b should be empty ('%v', '%v')", a, b)
23+
}
24+
25+
parseLine("bad boys:corny cows", &a, &b)
26+
if a != "bad boys" || b != "corny cows" {
27+
t.Fatalf("a should be 'bad boys' and b should be 'corny cows' ('%v', '%v')", a, b)
28+
}
29+
30+
parseLine("", &c)
31+
if len(c) != 0 {
32+
t.Fatalf("c should be empty (%#v)", c)
33+
}
34+
35+
parseLine("d,e,f:g:h:i,j,k", &c, &a, &b, &c)
36+
if a != "g" || b != "h" || len(c) != 3 || c[0] != "i" || c[1] != "j" || c[2] != "k" {
37+
t.Fatalf("a should be 'g', b should be 'h', and c should be ['i','j','k'] ('%v', '%v', '%#v')", a, b, c)
38+
}
39+
40+
parseLine("::::::::::", &a, &b, &c)
41+
if a != "" || b != "" || len(c) != 0 {
42+
t.Fatalf("a, b, and c should all be empty ('%v', '%v', '%#v')", a, b, c)
43+
}
44+
45+
parseLine("not a number", &d)
46+
if d != 0 {
47+
t.Fatalf("d should be 0 (%v)", d)
48+
}
49+
50+
parseLine("b:12:c", &a, &d, &b)
51+
if a != "b" || b != "c" || d != 12 {
52+
t.Fatalf("a should be 'b' and b should be 'c', and d should be 12 ('%v', '%v', %v)", a, b, d)
53+
}
54+
}
55+
56+
func TestUserParsePasswd(t *testing.T) {
57+
users, err := parsePasswdFile(strings.NewReader(`
58+
root:x:0:0:root:/root:/bin/bash
59+
adm:x:3:4:adm:/var/adm:/bin/false
60+
this is just some garbage data
61+
`), nil)
62+
if err != nil {
63+
t.Fatalf("Unexpected error: %v", err)
64+
}
65+
if len(users) != 3 {
66+
t.Fatalf("Expected 3 users, got %v", len(users))
67+
}
68+
if users[0].Uid != 0 || users[0].Name != "root" {
69+
t.Fatalf("Expected users[0] to be 0 - root, got %v - %v", users[0].Uid, users[0].Name)
70+
}
71+
if users[1].Uid != 3 || users[1].Name != "adm" {
72+
t.Fatalf("Expected users[1] to be 3 - adm, got %v - %v", users[1].Uid, users[1].Name)
73+
}
74+
}
75+
76+
func TestUserParseGroup(t *testing.T) {
77+
groups, err := parseGroupFile(strings.NewReader(`
78+
root:x:0:root
79+
adm:x:4:root,adm,daemon
80+
this is just some garbage data
81+
`), nil)
82+
if err != nil {
83+
t.Fatalf("Unexpected error: %v", err)
84+
}
85+
if len(groups) != 3 {
86+
t.Fatalf("Expected 3 groups, got %v", len(groups))
87+
}
88+
if groups[0].Gid != 0 || groups[0].Name != "root" || len(groups[0].List) != 1 {
89+
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))
90+
}
91+
if groups[1].Gid != 4 || groups[1].Name != "adm" || len(groups[1].List) != 3 {
92+
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))
93+
}
94+
}

0 commit comments

Comments
 (0)