diff --git a/config/v3_2_experimental/schema/ignition.json b/config/v3_2_experimental/schema/ignition.json index 495b040f8..944f12d49 100644 --- a/config/v3_2_experimental/schema/ignition.json +++ b/config/v3_2_experimental/schema/ignition.json @@ -514,6 +514,9 @@ }, "shell": { "type": ["string", "null"] + }, + "nonUnique": { + "type": ["boolean", "null"] } }, "required": [ diff --git a/config/v3_2_experimental/types/schema.go b/config/v3_2_experimental/types/schema.go index fe2d3fdb4..1fd7b5175 100644 --- a/config/v3_2_experimental/types/schema.go +++ b/config/v3_2_experimental/types/schema.go @@ -139,6 +139,7 @@ type PasswdUser struct { NoCreateHome *bool `json:"noCreateHome,omitempty"` NoLogInit *bool `json:"noLogInit,omitempty"` NoUserGroup *bool `json:"noUserGroup,omitempty"` + NonUnique *bool `json:"nonUnique,omitempty"` PasswordHash *string `json:"passwordHash,omitempty"` PrimaryGroup *string `json:"primaryGroup,omitempty"` SSHAuthorizedKeys []SSHAuthorizedKey `json:"sshAuthorizedKeys,omitempty"` diff --git a/internal/exec/stages/files/passwd.go b/internal/exec/stages/files/passwd.go index fabb867a2..98e27f45f 100644 --- a/internal/exec/stages/files/passwd.go +++ b/internal/exec/stages/files/passwd.go @@ -15,6 +15,7 @@ package files import ( + "errors" "fmt" "path/filepath" @@ -77,6 +78,23 @@ func (s *stage) createPasswd(config types.Config) error { return nil } +func userUIDConflict(a types.PasswdUser, list []types.PasswdUser) error { + if a.UID == nil { + return nil + } + + for _, b := range list { + if b.UID == nil || a.Name == b.Name { + continue + } + + if uint64(*b.UID) == uint64(*a.UID) && ((*b.NonUnique) == false || (*a.NonUnique) == false) { + return errors.New(fmt.Sprintf("conflicting uid from user: %s with uid: %d", b.Name, *b.UID)) + } + } + return nil +} + // createUsers creates the users as described in config.Passwd.Users. func (s stage) createUsers(config types.Config) error { if len(config.Passwd.Users) == 0 { @@ -86,6 +104,11 @@ func (s stage) createUsers(config types.Config) error { defer s.Logger.PopPrefix() for _, u := range config.Passwd.Users { + if err := userUIDConflict(u, config.Passwd.Users); err != nil { + return fmt.Errorf("failed to create user %q: %v", + u.Name, err) + } + if err := s.EnsureUser(u); err != nil { return fmt.Errorf("failed to create user %q: %v", u.Name, err) diff --git a/internal/exec/util/passwd.go b/internal/exec/util/passwd.go index 1a665c5f2..a0a61a58d 100644 --- a/internal/exec/util/passwd.go +++ b/internal/exec/util/passwd.go @@ -99,6 +99,8 @@ func (u Util) EnsureUser(c types.PasswdUser) error { strconv.FormatUint(uint64(*c.UID), 10)) } + args = appendIfTrue(args, c.NonUnique, "--non-unique") + args = appendIfStringSet(args, "--comment", c.Gecos) args = appendIfStringSet(args, "--gid", c.PrimaryGroup) diff --git a/tests/positive/passwd/users.go b/tests/positive/passwd/users.go index c3fd28fa3..b82c6757b 100644 --- a/tests/positive/passwd/users.go +++ b/tests/positive/passwd/users.go @@ -44,6 +44,16 @@ func AddPasswdUsers() types.Test { { "name": "jenkins", "uid": 1020 + }, + { + "name": "test2", + "uid": 1030, + "nonUnique": true + }, + { + "name": "test3", + "uid": 1030, + "nonUnique": true } ] } @@ -116,28 +126,28 @@ ENCRYPT_METHOD SHA512 Name: "passwd", Directory: "etc", }, - Contents: "root:x:0:0:root:/root:/bin/bash\ncore:x:500:500:CoreOS Admin:/home/core:/bin/bash\nsystemd-coredump:x:998:998:systemd Core Dumper:/:/sbin/nologin\nfleet:x:253:253::/:/sbin/nologin\ntest:x:1000:1000::/home/test:/bin/bash\njenkins:x:1020:1001::/home/jenkins:/bin/bash\n", + Contents: "root:x:0:0:root:/root:/bin/bash\ncore:x:500:500:CoreOS Admin:/home/core:/bin/bash\nsystemd-coredump:x:998:998:systemd Core Dumper:/:/sbin/nologin\nfleet:x:253:253::/:/sbin/nologin\ntest:x:1000:1000::/home/test:/bin/bash\njenkins:x:1020:1001::/home/jenkins:/bin/bash\ntest2:x:1030:1002::/home/test2:/bin/bash\ntest3:x:1030:1003::/home/test3:/bin/bash\n", }, { Node: types.Node{ Name: "group", Directory: "etc", }, - Contents: "root:x:0:root\nwheel:x:10:root,core\nsudo:x:150:\ndocker:x:233:core\nsystemd-coredump:x:998:\nfleet:x:253:core\ncore:x:500:\nrkt-admin:x:999:\nrkt:x:251:core\ntest:x:1000:\njenkins:x:1001:\n", + Contents: "root:x:0:root\nwheel:x:10:root,core\nsudo:x:150:\ndocker:x:233:core\nsystemd-coredump:x:998:\nfleet:x:253:core\ncore:x:500:\nrkt-admin:x:999:\nrkt:x:251:core\ntest:x:1000:\njenkins:x:1001:\ntest2:x:1002:\ntest3:x:1003:\n", }, { Node: types.Node{ Name: "shadow", Directory: "etc", }, - Contents: "root:*:15887:0:::::\ncore:*:15887:0:::::\nsystemd-coredump:!!:17301::::::\nfleet:!!:17301::::::\ntest:zJW/EKqqIk44o:17331:0:99999:7:::\njenkins:*:17331:0:99999:7:::\n", + Contents: "root:*:15887:0:::::\ncore:*:15887:0:::::\nsystemd-coredump:!!:17301::::::\nfleet:!!:17301::::::\ntest:zJW/EKqqIk44o:17331:0:99999:7:::\njenkins:*:17331:0:99999:7:::\ntest2:*:17331:0:99999:7:::\ntest3:*:17331:0:99999:7:::\n", }, { Node: types.Node{ Name: "gshadow", Directory: "etc", }, - Contents: "root:*::root\nusers:*::\nsudo:*::\nwheel:*::root,core\nsudo:*::\ndocker:*::core\nsystemd-coredump:!!::\nfleet:!!::core\nrkt-admin:!!::\nrkt:!!::core\ncore:*::\ntest:!::\njenkins:!::\n", + Contents: "root:*::root\nusers:*::\nsudo:*::\nwheel:*::root,core\nsudo:*::\ndocker:*::core\nsystemd-coredump:!!::\nfleet:!!::core\nrkt-admin:!!::\nrkt:!!::core\ncore:*::\ntest:!::\njenkins:!::\ntest2:!::\ntest3:!::\n", }, { Node: types.Node{ diff --git a/tests/stubs/useradd-stub/main.go b/tests/stubs/useradd-stub/main.go index 089212c9d..bde06e7b7 100644 --- a/tests/stubs/useradd-stub/main.go +++ b/tests/stubs/useradd-stub/main.go @@ -38,6 +38,7 @@ var ( flagGid int flagGroups string flagShell string + flagNonUnique bool ) func main() { @@ -54,6 +55,7 @@ func main() { flag.IntVar(&flagGid, "gid", -1, "The group name or number of the user's initial login group") flag.StringVar(&flagGroups, "groups", "", "A list of supplementary groups which the user is also a member of") flag.StringVar(&flagShell, "shell", "", "The name of the user's login shell") + flag.BoolVar(&flagNonUnique, "non-unique", false, "Allow the creation of a user account with a duplicate (non-unique) UID. ") flag.Parse()