1
2
3
4
19
20 package users
21
22 import (
23 "bytes"
24 "fmt"
25 "io"
26 "os"
27 "path/filepath"
28 "sort"
29 "strconv"
30 "strings"
31 "syscall"
32 "time"
33
34 "github.com/pkg/errors"
35
36 "k8s.io/klog/v2"
37
38 "k8s.io/kubernetes/cmd/kubeadm/app/constants"
39 )
40
41
42 type EntryMap struct {
43 entries map[string]*entry
44 }
45
46
47
48 type UsersAndGroups struct {
49
50 Users *EntryMap
51
52 Groups *EntryMap
53 }
54
55
56
57 type entry struct {
58 name string
59 id int64
60 gid int64
61 userNames []string
62 shell string
63 }
64
65
66 type limits struct {
67 minUID, maxUID, minGID, maxGID int64
68 }
69
70 const (
71
72
73 totalFieldsGroup = 4
74 totalFieldsUser = 7
75
76
77 klogLevel = 5
78
79
80 noshell = "/bin/false"
81
82
83 fileEtcLoginDefs = "/etc/login.defs"
84 fileEtcPasswd = "/etc/passwd"
85 fileEtcGroup = "/etc/group"
86 )
87
88 var (
89
90
91 usersToCreateSpec = []*entry{
92 {name: constants.EtcdUserName},
93 {name: constants.KubeAPIServerUserName},
94 {name: constants.KubeControllerManagerUserName},
95 {name: constants.KubeSchedulerUserName},
96 }
97 groupsToCreateSpec = []*entry{
98 {name: constants.EtcdUserName, userNames: []string{constants.EtcdUserName}},
99 {name: constants.KubeAPIServerUserName, userNames: []string{constants.KubeAPIServerUserName}},
100 {name: constants.KubeControllerManagerUserName, userNames: []string{constants.KubeControllerManagerUserName}},
101 {name: constants.KubeSchedulerUserName, userNames: []string{constants.KubeSchedulerUserName}},
102 {name: constants.ServiceAccountKeyReadersGroupName, userNames: []string{constants.KubeAPIServerUserName, constants.KubeControllerManagerUserName}},
103 }
104
105
106 defaultLimits = &limits{minUID: 100, maxUID: 999, minGID: 100, maxGID: 999}
107 )
108
109
110
111
112
113 func (u *EntryMap) ID(name string) *int64 {
114 entry, ok := u.entries[name]
115 if !ok {
116 return nil
117 }
118 id := entry.id
119 return &id
120 }
121
122
123 func (u *EntryMap) String() string {
124 lines := make([]string, 0, len(u.entries))
125 for k, e := range u.entries {
126 lines = append(lines, fmt.Sprintf("%s{%d,%d};", k, e.id, e.gid))
127 }
128 sort.Strings(lines)
129 return strings.Join(lines, "")
130 }
131
132
133 func AddUsersAndGroups() (*UsersAndGroups, error) {
134 return addUsersAndGroupsImpl(fileEtcLoginDefs, fileEtcPasswd, fileEtcGroup)
135 }
136
137
138
139
140
141
142 func addUsersAndGroupsImpl(pathLoginDef, pathUsers, pathGroups string) (*UsersAndGroups, error) {
143 klog.V(1).Info("Adding managed users and groups")
144 klog.V(klogLevel).Infof("Parsing %q", pathLoginDef)
145
146
147
148
149 var loginDef string
150 f, close, err := openFileWithLock(pathLoginDef)
151 if err != nil {
152 klog.V(1).Infof("Could not open %q, using default system limits: %v", pathLoginDef, err)
153 } else {
154 loginDef, err = readFile(f)
155 if err != nil {
156 klog.V(1).Infof("Could not read %q, using default system limits: %v", pathLoginDef, err)
157 }
158 close()
159 }
160 limits, err := parseLoginDefs(loginDef)
161 if err != nil {
162 return nil, err
163 }
164
165 klog.V(klogLevel).Infof("Using system UID/GID limits: %+v", limits)
166 klog.V(klogLevel).Infof("Parsing %q and %q", pathUsers, pathGroups)
167
168
169 fUsers, close, err := openFileWithLock(pathUsers)
170 if err != nil {
171 return nil, err
172 }
173 defer close()
174 fGroups, close, err := openFileWithLock(pathGroups)
175 if err != nil {
176 return nil, err
177 }
178 defer close()
179
180
181 fileUsers, err := readFile(fUsers)
182 if err != nil {
183 return nil, err
184 }
185 fileGroups, err := readFile(fGroups)
186 if err != nil {
187 return nil, err
188 }
189
190
191 users, err := parseEntries(fileUsers, totalFieldsUser)
192 if err != nil {
193 return nil, errors.Wrapf(err, "could not parse %q", pathUsers)
194 }
195 groups, err := parseEntries(fileGroups, totalFieldsGroup)
196 if err != nil {
197 return nil, errors.Wrapf(err, "could not parse %q", pathGroups)
198 }
199
200 klog.V(klogLevel).Info("Validating existing users and groups")
201
202
203 usersToCreate, groupsToCreate, err := validateEntries(users, groups, limits)
204 if err != nil {
205 return nil, errors.Wrap(err, "error validating existing users and groups")
206 }
207
208
209 allocUIDs, err := allocateIDs(users, limits.minUID, limits.maxUID, len(usersToCreate))
210 if err != nil {
211 return nil, err
212 }
213 allocGIDs, err := allocateIDs(groups, limits.minGID, limits.maxGID, len(groupsToCreate))
214 if err != nil {
215 return nil, err
216 }
217 if err := assignUserAndGroupIDs(groups, usersToCreate, groupsToCreate, allocUIDs, allocGIDs); err != nil {
218 return nil, err
219 }
220
221 if len(usersToCreate) > 0 {
222 klog.V(klogLevel).Infof("Adding users: %s", entriesToString(usersToCreate))
223 }
224 if len(groupsToCreate) > 0 {
225 klog.V(klogLevel).Infof("Adding groups: %s", entriesToString(groupsToCreate))
226 }
227
228
229 fileUsers = addEntries(fileUsers, usersToCreate, createUser)
230 fileGroups = addEntries(fileGroups, groupsToCreate, createGroup)
231
232
233 klog.V(klogLevel).Infof("Writing %q and %q", pathUsers, pathGroups)
234 if err := writeFile(fUsers, fileUsers); err != nil {
235 return nil, err
236 }
237 if err := writeFile(fGroups, fileGroups); err != nil {
238 return nil, err
239 }
240
241
242 usersConcat := append(users, usersToCreate...)
243 mapUsers, err := entriesToEntryMap(usersConcat, usersToCreateSpec)
244 if err != nil {
245 return nil, err
246 }
247 groupsConcat := append(groups, groupsToCreate...)
248 mapGroups, err := entriesToEntryMap(groupsConcat, groupsToCreateSpec)
249 if err != nil {
250 return nil, err
251 }
252 return &UsersAndGroups{Users: mapUsers, Groups: mapGroups}, nil
253 }
254
255
256
257 func RemoveUsersAndGroups() error {
258 return removeUsersAndGroupsImpl(fileEtcPasswd, fileEtcGroup)
259 }
260
261
262
263 func removeUsersAndGroupsImpl(pathUsers, pathGroups string) error {
264 klog.V(1).Info("Removing managed users and groups")
265 klog.V(klogLevel).Infof("Opening %q and %q", pathUsers, pathGroups)
266
267
268 fUsers, close, err := openFileWithLock(pathUsers)
269 if err != nil {
270 return err
271 }
272 defer close()
273 fGroups, close, err := openFileWithLock(pathGroups)
274 if err != nil {
275 return err
276 }
277 defer close()
278
279
280 fileUsers, err := readFile(fUsers)
281 if err != nil {
282 return err
283 }
284 fileGroups, err := readFile(fGroups)
285 if err != nil {
286 return err
287 }
288
289 klog.V(klogLevel).Infof("Removing users: %s", entriesToString(usersToCreateSpec))
290 klog.V(klogLevel).Infof("Removing groups: %s", entriesToString(groupsToCreateSpec))
291
292
293 fileUsers, _ = removeEntries(fileUsers, usersToCreateSpec)
294 fileGroups, _ = removeEntries(fileGroups, groupsToCreateSpec)
295
296 klog.V(klogLevel).Infof("Writing %q and %q", pathUsers, pathGroups)
297
298
299 if err := writeFile(fUsers, fileUsers); err != nil {
300 return err
301 }
302 if err := writeFile(fGroups, fileGroups); err != nil {
303 return err
304 }
305
306 return nil
307 }
308
309
310
311 func parseLoginDefs(file string) (*limits, error) {
312 l := *defaultLimits
313 if len(file) == 0 {
314 return &l, nil
315 }
316 var mapping = map[string]*int64{
317 "SYS_UID_MIN": &l.minUID,
318 "SYS_UID_MAX": &l.maxUID,
319 "SYS_GID_MIN": &l.minGID,
320 "SYS_GID_MAX": &l.maxGID,
321 }
322 lines := strings.Split(file, "\n")
323 for i, line := range lines {
324 for k, v := range mapping {
325
326 if !strings.HasPrefix(line, k) {
327 continue
328 }
329 line = strings.TrimPrefix(line, k)
330 line = strings.TrimSpace(line)
331 val, err := strconv.ParseInt(line, 10, 64)
332 if err != nil {
333 return nil, errors.Wrapf(err, "could not parse value for %s at line %d", k, i)
334 }
335 *v = val
336 }
337 }
338 return &l, nil
339 }
340
341
342
343
344
345 func parseEntries(file string, totalFields int) ([]*entry, error) {
346 if totalFields != totalFieldsUser && totalFields != totalFieldsGroup {
347 return nil, errors.Errorf("unsupported total fields for entry parsing: %d", totalFields)
348 }
349 lines := strings.Split(file, "\n")
350 entries := []*entry{}
351 for i, line := range lines {
352 line = strings.TrimSpace(line)
353 if len(line) == 0 {
354 continue
355 }
356 fields := strings.Split(line, ":")
357 if len(fields) != totalFields {
358 return nil, errors.Errorf("entry must have %d fields separated by ':', "+
359 "got %d at line %d: %s", totalFields, len(fields), i, line)
360 }
361 id, err := strconv.ParseInt(fields[2], 10, 64)
362 if err != nil {
363 return nil, errors.Wrapf(err, "error parsing id at line %d", i)
364 }
365 entry := &entry{name: fields[0], id: id}
366 if totalFields == totalFieldsGroup {
367 entry.userNames = strings.Split(fields[3], ",")
368 } else {
369 gid, err := strconv.ParseInt(fields[3], 10, 64)
370 if err != nil {
371 return nil, errors.Wrapf(err, "error parsing GID at line %d", i)
372 }
373 entry.gid = gid
374 entry.shell = fields[6]
375 }
376 entries = append(entries, entry)
377 }
378 return entries, nil
379 }
380
381
382
383
384 func validateEntries(users, groups []*entry, limits *limits) ([]*entry, []*entry, error) {
385 u := []*entry{}
386 g := []*entry{}
387
388 for _, uc := range usersToCreateSpec {
389 for _, user := range users {
390 if uc.name != user.name {
391 continue
392 }
393
394 if user.id < limits.minUID || user.id > limits.maxUID {
395 return nil, nil, errors.Errorf("UID %d for user %q is outside the system UID range: %d - %d",
396 user.id, user.name, limits.minUID, limits.maxUID)
397 }
398 if user.shell != noshell {
399 return nil, nil, errors.Errorf("user %q has unexpected shell %q; expected %q",
400 user.name, user.shell, noshell)
401 }
402 for _, g := range groups {
403 if g.id != user.gid {
404 continue
405 }
406
407 if g.name != uc.name {
408 return nil, nil, errors.Errorf("user %q has GID %d but the group with that GID is not named %q",
409 uc.name, g.id, uc.name)
410 }
411 goto skipUser
412 }
413 return nil, nil, errors.Errorf("could not find group with GID %d for user %q", user.gid, user.name)
414 }
415 u = append(u, uc)
416 skipUser:
417 }
418
419 for _, gc := range groupsToCreateSpec {
420 for _, group := range groups {
421 if gc.name != group.name {
422 continue
423 }
424 if group.id < limits.minGID || group.id > limits.maxGID {
425 return nil, nil, errors.Errorf("GID %d for user %q is outside the system UID range: %d - %d",
426 group.id, group.name, limits.minGID, limits.maxGID)
427 }
428 u1 := strings.Join(gc.userNames, ",")
429 u2 := strings.Join(group.userNames, ",")
430 if u1 != u2 {
431 return nil, nil, errors.Errorf("expected users %q for group %q; got %q",
432 u1, gc.name, u2)
433 }
434 goto skipGroup
435 }
436 g = append(g, gc)
437 skipGroup:
438 }
439 return u, g, nil
440 }
441
442
443 func allocateIDs(entries []*entry, min, max int64, total int) ([]int64, error) {
444 if total == 0 {
445 return []int64{}, nil
446 }
447 ids := make([]int64, 0, total)
448 for i := min; i < max+1; i++ {
449 i64 := int64(i)
450 for _, e := range entries {
451 if i64 == e.id {
452 goto continueLoop
453 }
454 }
455 ids = append(ids, i64)
456 if len(ids) == total {
457 return ids, nil
458 }
459 continueLoop:
460 }
461 return nil, errors.Errorf("could not allocate %d IDs based on existing entries in the range: %d - %d",
462 total, min, max)
463 }
464
465
466
467 func addEntries(file string, entries []*entry, createEntry func(*entry) string) string {
468 out := file
469 newLines := make([]string, 0, len(entries))
470 for _, e := range entries {
471 newLines = append(newLines, createEntry(e))
472 }
473 newLinesStr := ""
474 if len(newLines) > 0 {
475 if !strings.HasSuffix(out, "\n") {
476 newLinesStr = "\n"
477 }
478 newLinesStr += strings.Join(newLines, "\n") + "\n"
479 }
480 return out + newLinesStr
481 }
482
483
484
485 func removeEntries(file string, entries []*entry) (string, int) {
486 lines := strings.Split(file, "\n")
487 total := len(lines) - len(entries)
488 if total < 0 {
489 total = 0
490 }
491 newLines := make([]string, 0, total)
492 removed := 0
493 for _, line := range lines {
494 for _, entry := range entries {
495 if strings.HasPrefix(line, entry.name+":") {
496 removed++
497 goto continueLoop
498 }
499 }
500 newLines = append(newLines, line)
501 continueLoop:
502 }
503 return strings.Join(newLines, "\n"), removed
504 }
505
506
507
508
509 func assignUserAndGroupIDs(groups, usersToCreate, groupsToCreate []*entry, uids, gids []int64) error {
510 if len(gids) < len(groupsToCreate) {
511 return errors.Errorf("not enough GIDs to assign to groups: have %d, want %d", len(gids), len(groupsToCreate))
512 }
513 if len(uids) < len(usersToCreate) {
514 return errors.Errorf("not enough UIDs to assign to users: have %d, want %d", len(uids), len(usersToCreate))
515 }
516 for i := range groupsToCreate {
517 groupsToCreate[i].id = gids[i]
518 }
519
520 groupsConcat := append([]*entry{}, groups...)
521 groupsConcat = append(groupsConcat, groupsToCreate...)
522 for i := range usersToCreate {
523 usersToCreate[i].id = uids[i]
524 for _, g := range groupsConcat {
525 if usersToCreate[i].name == g.name {
526 usersToCreate[i].gid = g.id
527 break
528 }
529 }
530 }
531 return nil
532 }
533
534
535 func createGroup(e *entry) string {
536 return fmt.Sprintf("%s:x:%d:%s", e.name, e.id, strings.Join(e.userNames, ","))
537 }
538
539
540 func createUser(e *entry) string {
541 return fmt.Sprintf("%s:x:%d:%d:::/bin/false", e.name, e.id, e.gid)
542 }
543
544
545 func entriesToEntryMap(entries, spec []*entry) (*EntryMap, error) {
546 m := map[string]*entry{}
547 for _, spec := range spec {
548 for _, e := range entries {
549 if spec.name == e.name {
550 entry := *e
551 m[e.name] = &entry
552 goto continueLoop
553 }
554 }
555 return nil, errors.Errorf("could not find entry %q in the list", spec.name)
556 continueLoop:
557 }
558 return &EntryMap{entries: m}, nil
559 }
560
561
562 func entriesToString(entries []*entry) string {
563 lines := make([]string, 0, len(entries))
564 for _, e := range entries {
565 lines = append(lines, e.name)
566 }
567 sort.Strings(lines)
568 return strings.Join(lines, ",")
569 }
570
571
572
573
574 func openFileWithLock(path string) (f *os.File, close func(), err error) {
575 f, err = os.OpenFile(path, os.O_RDWR, os.ModePerm)
576 if err != nil {
577 return nil, nil, err
578 }
579 deadline := time.Now().Add(time.Second * 5)
580 for {
581
582
583
584
585
586 lock := syscall.Flock_t{Type: syscall.F_WRLCK}
587 if err = syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &lock); err == nil {
588 break
589 }
590 time.Sleep(200 * time.Millisecond)
591 if time.Now().After(deadline) {
592 err = errors.Wrapf(err, "timeout attempting to obtain lock on file %q", path)
593 break
594 }
595 }
596 if err != nil {
597 f.Close()
598 return nil, nil, err
599 }
600 close = func() {
601
602
603 unlock := syscall.Flock_t{Type: syscall.F_UNLCK}
604 syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &unlock)
605 f.Close()
606 }
607 return f, close, nil
608 }
609
610
611 func readFile(f *os.File) (string, error) {
612 buf := bytes.NewBuffer(nil)
613 if _, err := f.Seek(0, io.SeekStart); err != nil {
614 return "", err
615 }
616 if _, err := io.Copy(buf, f); err != nil {
617 return "", err
618 }
619 return buf.String(), nil
620 }
621
622
623 func writeFile(f *os.File, str string) error {
624 if _, err := f.Seek(0, io.SeekStart); err != nil {
625 return err
626 }
627 if _, err := f.Write([]byte(str)); err != nil {
628 return err
629 }
630 if err := f.Truncate(int64(len(str))); err != nil {
631 return err
632 }
633 return nil
634 }
635
636
637
638 func UpdatePathOwnerAndPermissions(path string, uid, gid int64, perms uint32) error {
639 if err := os.Chown(path, int(uid), int(gid)); err != nil {
640 return errors.Wrapf(err, "failed to update owner of %q to uid: %d and gid: %d", path, uid, gid)
641 }
642 fm := os.FileMode(perms)
643 if err := os.Chmod(path, fm); err != nil {
644 return errors.Wrapf(err, "failed to update permissions of %q to %s", path, fm.String())
645 }
646 return nil
647 }
648
649
650
651 func UpdatePathOwner(dirPath string, uid, gid int64) error {
652 err := filepath.WalkDir(dirPath, func(path string, d os.DirEntry, err error) error {
653 if err := os.Chown(path, int(uid), int(gid)); err != nil {
654 return errors.Wrapf(err, "failed to update owner of %q to uid: %d and gid: %d", path, uid, gid)
655 }
656 return nil
657 })
658 return err
659 }
660
View as plain text