]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob
e6375ea4dd53ffd7423f8fa7693801024ae8b063
[github/fretlink/terraform-provider-statuscake.git] /
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 }