]>
Commit | Line | Data |
---|---|---|
9b12e4fe JC |
1 | package user |
2 | ||
3 | import ( | |
4 | "bufio" | |
5 | "fmt" | |
6 | "io" | |
7 | "os" | |
8 | "strconv" | |
9 | "strings" | |
10 | ) | |
11 | ||
12 | const ( | |
13 | minId = 0 | |
14 | maxId = 1<<31 - 1 //for 32-bit systems compatibility | |
15 | ) | |
16 | ||
17 | var ( | |
18 | ErrRange = fmt.Errorf("Uids and gids must be in range %d-%d", minId, maxId) | |
19 | ) | |
20 | ||
21 | type User struct { | |
22 | Name string | |
23 | Pass string | |
24 | Uid int | |
25 | Gid int | |
26 | Gecos string | |
27 | Home string | |
28 | Shell string | |
29 | } | |
30 | ||
31 | type Group struct { | |
32 | Name string | |
33 | Pass string | |
34 | Gid int | |
35 | List []string | |
36 | } | |
37 | ||
38 | func parseLine(line string, v ...interface{}) { | |
39 | if line == "" { | |
40 | return | |
41 | } | |
42 | ||
43 | parts := strings.Split(line, ":") | |
44 | for i, p := range parts { | |
45 | if len(v) <= i { | |
46 | // if we have more "parts" than we have places to put them, bail for great "tolerance" of naughty configuration files | |
47 | break | |
48 | } | |
49 | ||
50 | switch e := v[i].(type) { | |
51 | case *string: | |
52 | // "root", "adm", "/bin/bash" | |
53 | *e = p | |
54 | case *int: | |
55 | // "0", "4", "1000" | |
56 | // ignore string to int conversion errors, for great "tolerance" of naughty configuration files | |
57 | *e, _ = strconv.Atoi(p) | |
58 | case *[]string: | |
59 | // "", "root", "root,adm,daemon" | |
60 | if p != "" { | |
61 | *e = strings.Split(p, ",") | |
62 | } else { | |
63 | *e = []string{} | |
64 | } | |
65 | default: | |
66 | // panic, because this is a programming/logic error, not a runtime one | |
67 | panic("parseLine expects only pointers! argument " + strconv.Itoa(i) + " is not a pointer!") | |
68 | } | |
69 | } | |
70 | } | |
71 | ||
72 | func ParsePasswdFile(path string) ([]User, error) { | |
73 | passwd, err := os.Open(path) | |
74 | if err != nil { | |
75 | return nil, err | |
76 | } | |
77 | defer passwd.Close() | |
78 | return ParsePasswd(passwd) | |
79 | } | |
80 | ||
81 | func ParsePasswd(passwd io.Reader) ([]User, error) { | |
82 | return ParsePasswdFilter(passwd, nil) | |
83 | } | |
84 | ||
85 | func ParsePasswdFileFilter(path string, filter func(User) bool) ([]User, error) { | |
86 | passwd, err := os.Open(path) | |
87 | if err != nil { | |
88 | return nil, err | |
89 | } | |
90 | defer passwd.Close() | |
91 | return ParsePasswdFilter(passwd, filter) | |
92 | } | |
93 | ||
94 | func ParsePasswdFilter(r io.Reader, filter func(User) bool) ([]User, error) { | |
95 | if r == nil { | |
96 | return nil, fmt.Errorf("nil source for passwd-formatted data") | |
97 | } | |
98 | ||
99 | var ( | |
100 | s = bufio.NewScanner(r) | |
101 | out = []User{} | |
102 | ) | |
103 | ||
104 | for s.Scan() { | |
105 | if err := s.Err(); err != nil { | |
106 | return nil, err | |
107 | } | |
108 | ||
109 | text := strings.TrimSpace(s.Text()) | |
110 | if text == "" { | |
111 | continue | |
112 | } | |
113 | ||
114 | // see: man 5 passwd | |
115 | // name:password:UID:GID:GECOS:directory:shell | |
116 | // Name:Pass:Uid:Gid:Gecos:Home:Shell | |
117 | // root:x:0:0:root:/root:/bin/bash | |
118 | // adm:x:3:4:adm:/var/adm:/bin/false | |
119 | p := User{} | |
120 | parseLine( | |
121 | text, | |
122 | &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell, | |
123 | ) | |
124 | ||
125 | if filter == nil || filter(p) { | |
126 | out = append(out, p) | |
127 | } | |
128 | } | |
129 | ||
130 | return out, nil | |
131 | } | |
132 | ||
133 | func ParseGroupFile(path string) ([]Group, error) { | |
134 | group, err := os.Open(path) | |
135 | if err != nil { | |
136 | return nil, err | |
137 | } | |
138 | defer group.Close() | |
139 | return ParseGroup(group) | |
140 | } | |
141 | ||
142 | func ParseGroup(group io.Reader) ([]Group, error) { | |
143 | return ParseGroupFilter(group, nil) | |
144 | } | |
145 | ||
146 | func ParseGroupFileFilter(path string, filter func(Group) bool) ([]Group, error) { | |
147 | group, err := os.Open(path) | |
148 | if err != nil { | |
149 | return nil, err | |
150 | } | |
151 | defer group.Close() | |
152 | return ParseGroupFilter(group, filter) | |
153 | } | |
154 | ||
155 | func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) { | |
156 | if r == nil { | |
157 | return nil, fmt.Errorf("nil source for group-formatted data") | |
158 | } | |
159 | ||
160 | var ( | |
161 | s = bufio.NewScanner(r) | |
162 | out = []Group{} | |
163 | ) | |
164 | ||
165 | for s.Scan() { | |
166 | if err := s.Err(); err != nil { | |
167 | return nil, err | |
168 | } | |
169 | ||
170 | text := s.Text() | |
171 | if text == "" { | |
172 | continue | |
173 | } | |
174 | ||
175 | // see: man 5 group | |
176 | // group_name:password:GID:user_list | |
177 | // Name:Pass:Gid:List | |
178 | // root:x:0:root | |
179 | // adm:x:4:root,adm,daemon | |
180 | p := Group{} | |
181 | parseLine( | |
182 | text, | |
183 | &p.Name, &p.Pass, &p.Gid, &p.List, | |
184 | ) | |
185 | ||
186 | if filter == nil || filter(p) { | |
187 | out = append(out, p) | |
188 | } | |
189 | } | |
190 | ||
191 | return out, nil | |
192 | } | |
193 | ||
194 | type ExecUser struct { | |
195 | Uid, Gid int | |
196 | Sgids []int | |
197 | Home string | |
198 | } | |
199 | ||
200 | // GetExecUserPath is a wrapper for GetExecUser. It reads data from each of the | |
201 | // given file paths and uses that data as the arguments to GetExecUser. If the | |
202 | // files cannot be opened for any reason, the error is ignored and a nil | |
203 | // io.Reader is passed instead. | |
204 | func GetExecUserPath(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) { | |
205 | passwd, err := os.Open(passwdPath) | |
206 | if err != nil { | |
207 | passwd = nil | |
208 | } else { | |
209 | defer passwd.Close() | |
210 | } | |
211 | ||
212 | group, err := os.Open(groupPath) | |
213 | if err != nil { | |
214 | group = nil | |
215 | } else { | |
216 | defer group.Close() | |
217 | } | |
218 | ||
219 | return GetExecUser(userSpec, defaults, passwd, group) | |
220 | } | |
221 | ||
222 | // GetExecUser parses a user specification string (using the passwd and group | |
223 | // readers as sources for /etc/passwd and /etc/group data, respectively). In | |
224 | // the case of blank fields or missing data from the sources, the values in | |
225 | // defaults is used. | |
226 | // | |
227 | // GetExecUser will return an error if a user or group literal could not be | |
228 | // found in any entry in passwd and group respectively. | |
229 | // | |
230 | // Examples of valid user specifications are: | |
231 | // * "" | |
232 | // * "user" | |
233 | // * "uid" | |
234 | // * "user:group" | |
235 | // * "uid:gid | |
236 | // * "user:gid" | |
237 | // * "uid:group" | |
238 | func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) { | |
239 | var ( | |
240 | userArg, groupArg string | |
241 | name string | |
242 | ) | |
243 | ||
244 | if defaults == nil { | |
245 | defaults = new(ExecUser) | |
246 | } | |
247 | ||
248 | // Copy over defaults. | |
249 | user := &ExecUser{ | |
250 | Uid: defaults.Uid, | |
251 | Gid: defaults.Gid, | |
252 | Sgids: defaults.Sgids, | |
253 | Home: defaults.Home, | |
254 | } | |
255 | ||
256 | // Sgids slice *cannot* be nil. | |
257 | if user.Sgids == nil { | |
258 | user.Sgids = []int{} | |
259 | } | |
260 | ||
261 | // allow for userArg to have either "user" syntax, or optionally "user:group" syntax | |
262 | parseLine(userSpec, &userArg, &groupArg) | |
263 | ||
264 | users, err := ParsePasswdFilter(passwd, func(u User) bool { | |
265 | if userArg == "" { | |
266 | return u.Uid == user.Uid | |
267 | } | |
268 | return u.Name == userArg || strconv.Itoa(u.Uid) == userArg | |
269 | }) | |
270 | if err != nil && passwd != nil { | |
271 | if userArg == "" { | |
272 | userArg = strconv.Itoa(user.Uid) | |
273 | } | |
274 | return nil, fmt.Errorf("Unable to find user %v: %v", userArg, err) | |
275 | } | |
276 | ||
277 | haveUser := users != nil && len(users) > 0 | |
278 | if haveUser { | |
279 | // if we found any user entries that matched our filter, let's take the first one as "correct" | |
280 | name = users[0].Name | |
281 | user.Uid = users[0].Uid | |
282 | user.Gid = users[0].Gid | |
283 | user.Home = users[0].Home | |
284 | } else if userArg != "" { | |
285 | // we asked for a user but didn't find them... let's check to see if we wanted a numeric user | |
286 | user.Uid, err = strconv.Atoi(userArg) | |
287 | if err != nil { | |
288 | // not numeric - we have to bail | |
289 | return nil, fmt.Errorf("Unable to find user %v", userArg) | |
290 | } | |
291 | ||
292 | // Must be inside valid uid range. | |
293 | if user.Uid < minId || user.Uid > maxId { | |
294 | return nil, ErrRange | |
295 | } | |
296 | ||
297 | // if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit | |
298 | } | |
299 | ||
300 | if groupArg != "" || name != "" { | |
301 | groups, err := ParseGroupFilter(group, func(g Group) bool { | |
302 | // Explicit group format takes precedence. | |
303 | if groupArg != "" { | |
304 | return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg | |
305 | } | |
306 | ||
307 | // Check if user is a member. | |
308 | for _, u := range g.List { | |
309 | if u == name { | |
310 | return true | |
311 | } | |
312 | } | |
313 | ||
314 | return false | |
315 | }) | |
316 | if err != nil && group != nil { | |
317 | return nil, fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err) | |
318 | } | |
319 | ||
320 | haveGroup := groups != nil && len(groups) > 0 | |
321 | if groupArg != "" { | |
322 | if haveGroup { | |
323 | // if we found any group entries that matched our filter, let's take the first one as "correct" | |
324 | user.Gid = groups[0].Gid | |
325 | } else { | |
326 | // we asked for a group but didn't find id... let's check to see if we wanted a numeric group | |
327 | user.Gid, err = strconv.Atoi(groupArg) | |
328 | if err != nil { | |
329 | // not numeric - we have to bail | |
330 | return nil, fmt.Errorf("Unable to find group %v", groupArg) | |
331 | } | |
332 | ||
333 | // Ensure gid is inside gid range. | |
334 | if user.Gid < minId || user.Gid > maxId { | |
335 | return nil, ErrRange | |
336 | } | |
337 | ||
338 | // if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit | |
339 | } | |
340 | } else if haveGroup { | |
341 | // If implicit group format, fill supplementary gids. | |
342 | user.Sgids = make([]int, len(groups)) | |
343 | for i, group := range groups { | |
344 | user.Sgids[i] = group.Gid | |
345 | } | |
346 | } | |
347 | } | |
348 | ||
349 | return user, nil | |
350 | } | |
351 | ||
352 | // GetAdditionalGroups looks up a list of groups by name or group id | |
353 | // against the given /etc/group formatted data. If a group name cannot | |
354 | // be found, an error will be returned. If a group id cannot be found, | |
355 | // or the given group data is nil, the id will be returned as-is | |
356 | // provided it is in the legal range. | |
357 | func GetAdditionalGroups(additionalGroups []string, group io.Reader) ([]int, error) { | |
358 | var groups = []Group{} | |
359 | if group != nil { | |
360 | var err error | |
361 | groups, err = ParseGroupFilter(group, func(g Group) bool { | |
362 | for _, ag := range additionalGroups { | |
363 | if g.Name == ag || strconv.Itoa(g.Gid) == ag { | |
364 | return true | |
365 | } | |
366 | } | |
367 | return false | |
368 | }) | |
369 | if err != nil { | |
370 | return nil, fmt.Errorf("Unable to find additional groups %v: %v", additionalGroups, err) | |
371 | } | |
372 | } | |
373 | ||
374 | gidMap := make(map[int]struct{}) | |
375 | for _, ag := range additionalGroups { | |
376 | var found bool | |
377 | for _, g := range groups { | |
378 | // if we found a matched group either by name or gid, take the | |
379 | // first matched as correct | |
380 | if g.Name == ag || strconv.Itoa(g.Gid) == ag { | |
381 | if _, ok := gidMap[g.Gid]; !ok { | |
382 | gidMap[g.Gid] = struct{}{} | |
383 | found = true | |
384 | break | |
385 | } | |
386 | } | |
387 | } | |
388 | // we asked for a group but didn't find it. let's check to see | |
389 | // if we wanted a numeric group | |
390 | if !found { | |
391 | gid, err := strconv.Atoi(ag) | |
392 | if err != nil { | |
393 | return nil, fmt.Errorf("Unable to find group %s", ag) | |
394 | } | |
395 | // Ensure gid is inside gid range. | |
396 | if gid < minId || gid > maxId { | |
397 | return nil, ErrRange | |
398 | } | |
399 | gidMap[gid] = struct{}{} | |
400 | } | |
401 | } | |
402 | gids := []int{} | |
403 | for gid := range gidMap { | |
404 | gids = append(gids, gid) | |
405 | } | |
406 | return gids, nil | |
407 | } | |
408 | ||
409 | // GetAdditionalGroupsPath is a wrapper around GetAdditionalGroups | |
410 | // that opens the groupPath given and gives it as an argument to | |
411 | // GetAdditionalGroups. | |
412 | func GetAdditionalGroupsPath(additionalGroups []string, groupPath string) ([]int, error) { | |
413 | group, err := os.Open(groupPath) | |
414 | if err == nil { | |
415 | defer group.Close() | |
416 | } | |
417 | return GetAdditionalGroups(additionalGroups, group) | |
418 | } |