1
2
3
4
19
20 package users
21
22 import (
23 "os"
24 "reflect"
25 "testing"
26 )
27
28 func TestParseLoginDef(t *testing.T) {
29 testCases := []struct {
30 name string
31 input string
32 expectedLimits *limits
33 expectedError bool
34 }{
35 {
36 name: "non number value for tracked limit",
37 input: "SYS_UID_MIN foo\n",
38 expectedError: true,
39 },
40 {
41 name: "empty string must return defaults",
42 expectedLimits: defaultLimits,
43 },
44 {
45 name: "no tracked limits in file must return defaults",
46 input: "# some comment\n",
47 expectedLimits: defaultLimits,
48 },
49 {
50 name: "must parse all valid tracked limits",
51 input: "SYS_UID_MIN 101\nSYS_UID_MAX 998\nSYS_GID_MIN 102\nSYS_GID_MAX 999\n",
52 expectedLimits: &limits{minUID: 101, maxUID: 998, minGID: 102, maxGID: 999},
53 },
54 {
55 name: "must return defaults for missing limits",
56 input: "SYS_UID_MIN 101\n#SYS_UID_MAX 998\nSYS_GID_MIN 102\n#SYS_GID_MAX 999\n",
57 expectedLimits: &limits{minUID: 101, maxUID: defaultLimits.maxUID, minGID: 102, maxGID: defaultLimits.maxGID},
58 },
59 }
60
61 for _, tc := range testCases {
62 t.Run(tc.name, func(t *testing.T) {
63 got, err := parseLoginDefs(tc.input)
64 if err != nil != tc.expectedError {
65 t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
66 }
67 if err == nil && *tc.expectedLimits != *got {
68 t.Fatalf("expected limits %+v, got %+v", tc.expectedLimits, got)
69 }
70 })
71 }
72 }
73
74 func TestParseEntries(t *testing.T) {
75 testCases := []struct {
76 name string
77 file string
78 expectedEntries []*entry
79 totalFields int
80 expectedError bool
81 }{
82 {
83 name: "totalFields must be a known value",
84 expectedError: true,
85 },
86 {
87 name: "unexpected number of fields",
88 file: "foo:x:100::::::",
89 totalFields: totalFieldsUser,
90 expectedError: true,
91 },
92 {
93 name: "cannot parse 'bar' as UID",
94 file: "foo:x:bar:101:::\n",
95 totalFields: totalFieldsUser,
96 expectedError: true,
97 },
98 {
99 name: "cannot parse 'bar' as GID",
100 file: "foo:x:101:bar:::\n",
101 totalFields: totalFieldsUser,
102 expectedError: true,
103 },
104 {
105 name: "valid file for users",
106 file: "\nfoo:x:100:101:foo:/home/foo:/bin/bash\n\nbar:x:102:103:bar::\n",
107 totalFields: totalFieldsUser,
108 expectedEntries: []*entry{
109 {name: "foo", id: 100, gid: 101, shell: "/bin/bash"},
110 {name: "bar", id: 102, gid: 103},
111 },
112 },
113 {
114 name: "valid file for groups",
115 file: "\nfoo:x:100:bar,baz\n\nbar:x:101:baz\n",
116 totalFields: totalFieldsGroup,
117 expectedEntries: []*entry{
118 {name: "foo", id: 100, userNames: []string{"bar", "baz"}},
119 {name: "bar", id: 101, userNames: []string{"baz"}},
120 },
121 },
122 }
123
124 for _, tc := range testCases {
125 t.Run(tc.name, func(t *testing.T) {
126 got, err := parseEntries(tc.file, tc.totalFields)
127 if err != nil != tc.expectedError {
128 t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
129 }
130 if err != nil {
131 return
132 }
133 if len(tc.expectedEntries) != len(got) {
134 t.Fatalf("expected entries %d, got %d", len(tc.expectedEntries), len(got))
135 }
136 for i := range got {
137 if !reflect.DeepEqual(tc.expectedEntries[i], got[i]) {
138 t.Fatalf("expected entry at position %d: %+v, got: %+v", i, tc.expectedEntries[i], got[i])
139 }
140 }
141 })
142 }
143 }
144
145 func TestValidateEntries(t *testing.T) {
146 testCases := []struct {
147 name string
148 users []*entry
149 groups []*entry
150 expectedUsers []*entry
151 expectedGroups []*entry
152 expectedError bool
153 }{
154 {
155 name: "UID for user is outside of system limits",
156 users: []*entry{
157 {name: "kubeadm-etcd", id: 2000, gid: 102, shell: noshell},
158 },
159 groups: []*entry{},
160 expectedError: true,
161 },
162 {
163 name: "user has unexpected shell",
164 users: []*entry{
165 {name: "kubeadm-etcd", id: 102, gid: 102, shell: "foo"},
166 },
167 groups: []*entry{},
168 expectedError: true,
169 },
170 {
171 name: "user is mapped to unknown group",
172 users: []*entry{
173 {name: "kubeadm-etcd", id: 102, gid: 102, shell: noshell},
174 },
175 groups: []*entry{},
176 expectedError: true,
177 },
178 {
179 name: "user and group names do not match",
180 users: []*entry{
181 {name: "kubeadm-etcd", id: 102, gid: 102, shell: noshell},
182 },
183 groups: []*entry{
184 {name: "foo", id: 102},
185 },
186 expectedError: true,
187 },
188 {
189 name: "GID is outside system limits",
190 users: []*entry{},
191 groups: []*entry{
192 {name: "kubeadm-etcd", id: 2000},
193 },
194 expectedError: true,
195 },
196 {
197 name: "group is missing users",
198 users: []*entry{},
199 groups: []*entry{
200 {name: "kubeadm-etcd", id: 100},
201 },
202 expectedError: true,
203 },
204 {
205 name: "empty input must return default users and groups",
206 users: []*entry{},
207 groups: []*entry{},
208 expectedUsers: usersToCreateSpec,
209 expectedGroups: groupsToCreateSpec,
210 },
211 {
212 name: "existing valid users mapped to groups",
213 users: []*entry{
214 {name: "kubeadm-etcd", id: 100, gid: 102, shell: noshell},
215 {name: "kubeadm-kas", id: 101, gid: 103, shell: noshell},
216 },
217 groups: []*entry{
218 {name: "kubeadm-etcd", id: 102, userNames: []string{"kubeadm-etcd"}},
219 {name: "kubeadm-kas", id: 103, userNames: []string{"kubeadm-kas"}},
220 {name: "kubeadm-sa-key-readers", id: 104, userNames: []string{"kubeadm-kas", "kubeadm-kcm"}},
221 },
222 expectedUsers: []*entry{
223 {name: "kubeadm-kcm"},
224 {name: "kubeadm-ks"},
225 },
226 expectedGroups: []*entry{
227 {name: "kubeadm-kcm", userNames: []string{"kubeadm-kcm"}},
228 {name: "kubeadm-ks", userNames: []string{"kubeadm-ks"}},
229 },
230 },
231 }
232
233 for _, tc := range testCases {
234 t.Run(tc.name, func(t *testing.T) {
235 users, groups, err := validateEntries(tc.users, tc.groups, defaultLimits)
236 if err != nil != tc.expectedError {
237 t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
238 }
239 if err != nil {
240 return
241 }
242 if len(tc.expectedUsers) != len(users) {
243 t.Fatalf("expected users %d, got %d", len(tc.expectedUsers), len(users))
244 }
245 for i := range users {
246 if !reflect.DeepEqual(tc.expectedUsers[i], users[i]) {
247 t.Fatalf("expected user at position %d: %+v, got: %+v", i, tc.expectedUsers[i], users[i])
248 }
249 }
250 if len(tc.expectedGroups) != len(groups) {
251 t.Fatalf("expected groups %d, got %d", len(tc.expectedGroups), len(groups))
252 }
253 for i := range groups {
254 if !reflect.DeepEqual(tc.expectedGroups[i], groups[i]) {
255 t.Fatalf("expected group at position %d: %+v, got: %+v", i, tc.expectedGroups[i], groups[i])
256 }
257 }
258 })
259 }
260 }
261
262 func TestAllocateIDs(t *testing.T) {
263 testCases := []struct {
264 name string
265 entries []*entry
266 min int64
267 max int64
268 total int
269 expectedIDs []int64
270 expectedError bool
271 }{
272 {
273 name: "zero total ids returns empty slice",
274 expectedIDs: []int64{},
275 },
276 {
277 name: "not enough free ids in range",
278 entries: []*entry{
279 {name: "foo", id: 101},
280 {name: "bar", id: 103},
281 {name: "baz", id: 105},
282 },
283 min: 100,
284 max: 105,
285 total: 4,
286 expectedError: true,
287 },
288 {
289 name: "successfully allocate ids",
290 entries: []*entry{
291 {name: "foo", id: 101},
292 {name: "bar", id: 103},
293 {name: "baz", id: 105},
294 },
295 min: 100,
296 max: 110,
297 total: 4,
298 expectedIDs: []int64{100, 102, 104, 106},
299 expectedError: false,
300 },
301 }
302
303 for _, tc := range testCases {
304 t.Run(tc.name, func(t *testing.T) {
305 got, err := allocateIDs(tc.entries, tc.min, tc.max, tc.total)
306 if err != nil != tc.expectedError {
307 t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
308 }
309 if err != nil {
310 return
311 }
312 if len(tc.expectedIDs) != len(got) {
313 t.Fatalf("expected id %d, got %d", len(tc.expectedIDs), len(got))
314 }
315 for i := range got {
316 if !reflect.DeepEqual(tc.expectedIDs[i], got[i]) {
317 t.Fatalf("expected id at position %d: %+v, got: %+v", i, tc.expectedIDs[i], got[i])
318 }
319 }
320 })
321 }
322 }
323
324 func TestAddEntries(t *testing.T) {
325 testCases := []struct {
326 name string
327 file string
328 entries []*entry
329 createEntry func(*entry) string
330 expectedOutput string
331 }{
332 {
333 name: "user entries are added",
334 file: "foo:x:101:101:::/bin/false\n",
335 entries: []*entry{
336 {name: "bar", id: 102, gid: 102},
337 {name: "baz", id: 103, gid: 103},
338 },
339 expectedOutput: "foo:x:101:101:::/bin/false\nbar:x:102:102:::/bin/false\nbaz:x:103:103:::/bin/false\n",
340 createEntry: createUser,
341 },
342 {
343 name: "user entries are added (new line is appended)",
344 file: "foo:x:101:101:::/bin/false",
345 entries: []*entry{
346 {name: "bar", id: 102, gid: 102},
347 },
348 expectedOutput: "foo:x:101:101:::/bin/false\nbar:x:102:102:::/bin/false\n",
349 createEntry: createUser,
350 },
351 {
352 name: "group entries are added",
353 file: "foo:x:101:foo\n",
354 entries: []*entry{
355 {name: "bar", id: 102, userNames: []string{"bar"}},
356 {name: "baz", id: 103, userNames: []string{"baz"}},
357 },
358 expectedOutput: "foo:x:101:foo\nbar:x:102:bar\nbaz:x:103:baz\n",
359 createEntry: createGroup,
360 },
361 }
362
363 for _, tc := range testCases {
364 t.Run(tc.name, func(t *testing.T) {
365 got := addEntries(tc.file, tc.entries, tc.createEntry)
366 if tc.expectedOutput != got {
367 t.Fatalf("expected output:\n%s\ngot:\n%s\n", tc.expectedOutput, got)
368 }
369 })
370 }
371 }
372
373 func TestRemoveEntries(t *testing.T) {
374 testCases := []struct {
375 name string
376 file string
377 entries []*entry
378 expectedRemoved int
379 expectedOutput string
380 }{
381 {
382 name: "entries that are missing do not cause an error",
383 file: "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
384 entries: []*entry{},
385 expectedRemoved: 0,
386 expectedOutput: "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
387 },
388 {
389 name: "user entry is removed",
390 file: "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
391 entries: []*entry{
392 {name: "bar"},
393 },
394 expectedRemoved: 1,
395 expectedOutput: "foo:x:102:102:::/bin/false\n",
396 },
397 {
398 name: "group entry is removed",
399 file: "foo:x:102:foo\nbar:x:102:bar\n",
400 entries: []*entry{
401 {name: "bar"},
402 },
403 expectedRemoved: 1,
404 expectedOutput: "foo:x:102:foo\n",
405 },
406 }
407
408 for _, tc := range testCases {
409 t.Run(tc.name, func(t *testing.T) {
410 got, removed := removeEntries(tc.file, tc.entries)
411 if tc.expectedRemoved != removed {
412 t.Fatalf("expected entries to be removed: %v, got: %v", tc.expectedRemoved, removed)
413 }
414 if tc.expectedOutput != got {
415 t.Fatalf("expected output:\n%s\ngot:\n%s\n", tc.expectedOutput, got)
416 }
417 })
418 }
419 }
420
421 func TestAssignUserAndGroupIDs(t *testing.T) {
422 testCases := []struct {
423 name string
424 users []*entry
425 groups []*entry
426 usersToCreate []*entry
427 groupsToCreate []*entry
428 uids []int64
429 gids []int64
430 expectedUsers []*entry
431 expectedGroups []*entry
432 expectedError bool
433 }{
434 {
435 name: "not enough UIDs",
436 usersToCreate: []*entry{
437 {name: "foo"},
438 {name: "bar"},
439 },
440 uids: []int64{100},
441 expectedError: true,
442 },
443 {
444 name: "not enough GIDs",
445 groupsToCreate: []*entry{
446 {name: "foo"},
447 {name: "bar"},
448 },
449 gids: []int64{100},
450 expectedError: true,
451 },
452 {
453 name: "valid UIDs and GIDs are assigned to input",
454 groups: []*entry{
455 {name: "foo", id: 110},
456 {name: "bar", id: 111},
457 },
458 usersToCreate: []*entry{
459 {name: "foo"},
460 {name: "bar"},
461 {name: "baz"},
462 },
463 groupsToCreate: []*entry{
464 {name: "baz"},
465 },
466 uids: []int64{100, 101, 102},
467 gids: []int64{112},
468 expectedUsers: []*entry{
469 {name: "foo", id: 100, gid: 110},
470 {name: "bar", id: 101, gid: 111},
471 {name: "baz", id: 102, gid: 112},
472 },
473 expectedGroups: []*entry{
474 {name: "baz", id: 112},
475 },
476 },
477 }
478
479 for _, tc := range testCases {
480 t.Run(tc.name, func(t *testing.T) {
481 err := assignUserAndGroupIDs(tc.groups, tc.usersToCreate, tc.groupsToCreate, tc.uids, tc.gids)
482 if err != nil != tc.expectedError {
483 t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
484 }
485 if err != nil {
486 return
487 }
488 if len(tc.expectedUsers) != len(tc.usersToCreate) {
489 t.Fatalf("expected users %d, got %d", len(tc.expectedUsers), len(tc.usersToCreate))
490 }
491 for i := range tc.usersToCreate {
492 if !reflect.DeepEqual(tc.expectedUsers[i], tc.usersToCreate[i]) {
493 t.Fatalf("expected user at position %d: %+v, got: %+v", i, tc.expectedUsers[i], tc.usersToCreate[i])
494 }
495 }
496 if len(tc.expectedGroups) != len(tc.groupsToCreate) {
497 t.Fatalf("expected groups %d, got %d", len(tc.expectedGroups), len(tc.groupsToCreate))
498 }
499 for i := range tc.groupsToCreate {
500 if !reflect.DeepEqual(tc.expectedGroups[i], tc.groupsToCreate[i]) {
501 t.Fatalf("expected group at position %d: %+v, got: %+v", i, tc.expectedGroups[i], tc.groupsToCreate[i])
502 }
503 }
504 })
505 }
506 }
507
508 func TestID(t *testing.T) {
509 e := &entry{name: "foo", id: 101}
510 m := &EntryMap{entries: map[string]*entry{
511 "foo": e,
512 }}
513 id := m.ID("foo")
514 if *id != 101 {
515 t.Fatalf("expected: id=%d; got: id=%d", 101, *id)
516 }
517 id = m.ID("bar")
518 if id != nil {
519 t.Fatalf("expected nil for unknown entry")
520 }
521 }
522
523 func TestAddUsersAndGroupsImpl(t *testing.T) {
524 const (
525 loginDef = "SYS_UID_MIN 101\nSYS_UID_MAX 998\nSYS_GID_MIN 102\nSYS_GID_MAX 999\n"
526 passwd = "root:x:0:0:::/bin/bash\nkubeadm-etcd:x:101:102:::/bin/false\n"
527 group = "root:x:0:root\nkubeadm-etcd:x:102:kubeadm-etcd\n"
528 expectedUsers = "kubeadm-etcd{101,102};kubeadm-kas{102,103};kubeadm-kcm{103,104};kubeadm-ks{104,105};"
529 expectedGroups = "kubeadm-etcd{102,0};kubeadm-kas{103,0};kubeadm-kcm{104,0};kubeadm-ks{105,0};kubeadm-sa-key-readers{106,0};"
530 )
531 fileLoginDef, close := writeTempFile(t, loginDef)
532 defer close()
533 filePasswd, close := writeTempFile(t, passwd)
534 defer close()
535 fileGroup, close := writeTempFile(t, group)
536 defer close()
537 got, err := addUsersAndGroupsImpl(fileLoginDef, filePasswd, fileGroup)
538 if err != nil {
539 t.Fatalf("AddUsersAndGroups failed: %v", err)
540 }
541 if expectedUsers != got.Users.String() {
542 t.Fatalf("expected users: %q, got: %q", expectedUsers, got.Users.String())
543 }
544 if expectedGroups != got.Groups.String() {
545 t.Fatalf("expected groups: %q, got: %q", expectedGroups, got.Groups.String())
546 }
547 }
548
549 func TestRemoveUsersAndGroups(t *testing.T) {
550 const (
551 passwd = "root:x:0:0:::/bin/bash\nkubeadm-etcd:x:101:102:::/bin/false\n"
552 group = "root:x:0:root\nkubeadm-etcd:x:102:kubeadm-etcd\n"
553 expectedPasswd = "root:x:0:0:::/bin/bash\n"
554 expectedGroup = "root:x:0:root\n"
555 )
556 filePasswd, close := writeTempFile(t, passwd)
557 defer close()
558 fileGroup, close := writeTempFile(t, group)
559 defer close()
560 if err := removeUsersAndGroupsImpl(filePasswd, fileGroup); err != nil {
561 t.Fatalf("RemoveUsersAndGroups failed: %v", err)
562 }
563 contentsPasswd := readTempFile(t, filePasswd)
564 if expectedPasswd != contentsPasswd {
565 t.Fatalf("expected passwd:\n%s\ngot:\n%s\n", expectedPasswd, contentsPasswd)
566 }
567 contentsGroup := readTempFile(t, fileGroup)
568 if expectedGroup != contentsGroup {
569 t.Fatalf("expected passwd:\n%s\ngot:\n%s\n", expectedGroup, contentsGroup)
570 }
571 }
572
573 func writeTempFile(t *testing.T, contents string) (string, func()) {
574 file, err := os.CreateTemp("", "")
575 if err != nil {
576 t.Fatalf("could not create file: %v", err)
577 }
578 if err := os.WriteFile(file.Name(), []byte(contents), os.ModePerm); err != nil {
579 t.Fatalf("could not write file: %v", err)
580 }
581 close := func() {
582 os.Remove(file.Name())
583 }
584 return file.Name(), close
585 }
586
587 func readTempFile(t *testing.T, path string) string {
588 b, err := os.ReadFile(path)
589 if err != nil {
590 t.Fatalf("could not read file: %v", err)
591 }
592 return string(b)
593 }
594
View as plain text