diff --git a/pkg/osbuild/groups_stage.go b/pkg/osbuild/groups_stage.go index 4af420ead2..d771b30781 100644 --- a/pkg/osbuild/groups_stage.go +++ b/pkg/osbuild/groups_stage.go @@ -22,6 +22,10 @@ func NewGroupsStage(options *GroupsStageOptions) *Stage { } func NewGroupsStageOptions(groups []users.Group) *GroupsStageOptions { + if len(groups) == 0 { + return nil + } + options := GroupsStageOptions{ Groups: map[string]GroupsStageOptionsGroup{}, } diff --git a/pkg/osbuild/kickstart_stage.go b/pkg/osbuild/kickstart_stage.go index 221ccbe8b2..52ce7b033c 100644 --- a/pkg/osbuild/kickstart_stage.go +++ b/pkg/osbuild/kickstart_stage.go @@ -137,6 +137,78 @@ func NewKickstartStage(options *KickstartStageOptions) *Stage { } } +// adjustRootUserOptions handles any options relating to the root user for the +// kickstart stage. It returns the [RootPasswordOptions] if necessary and +// modifies the userCustomizations array accordingly. It also validates that no +// unsupported options are set. +// +// User options for the root user have no effect in kickstart. In other +// words, the +// +// user --name "root" ... +// +// line is ignored, because when the users are being processed, the root +// user already exists. To set a root password, the rootpw command must be +// used. If an SSH key is added for the root user however, we need to keep +// the root user account in the kickstart options, because osbuild will use +// it to add the sshkey line for the root user. Unfortunately, this means +// that we will get a bare user line for root that will have no effect. +// See also https://github.com/osbuild/osbuild/issues/2178 +func adjustRootUserOptions(userOptions map[string]UsersStageOptionsUser) (*RootPasswordOptions, error) { + var rootpw *RootPasswordOptions + for name, user := range userOptions { + if name == "root" { + if user.Password != nil { + rootpw = &RootPasswordOptions{ + IsCrypted: true, // NewUserStageOptions() always encrypts plaintext passwords + Password: *user.Password, + } + + // remove the password since the --password option for the user + // kickstart command has no effect on root + user.Password = nil + } + + // return an error if any other field is set (except SSH) + unsupportedOptionsSet := make([]string, 0, 7) + if user.ExpireDate != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "expiredate") + } + if user.ForcePasswordReset != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "force_password_reset") + } + if user.GID != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "gid") + } + if user.Groups != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "groups") + } + if user.Home != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "home") + } + if user.Shell != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "shell") + } + if user.UID != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "uid") + } + if len(unsupportedOptionsSet) > 0 { + return nil, fmt.Errorf("unsupported options for user \"root\": %s", strings.Join(unsupportedOptionsSet, ", ")) + } + + // if the ssh key is set, update the options in the map (unset + // password), otherwise remove it entirely + if user.Key != nil { + userOptions[name] = user + } else { + delete(userOptions, name) + } + return rootpw, nil + } + } + return nil, nil +} + func NewKickstartStageOptions( path string, userCustomizations []users.User, @@ -154,12 +226,18 @@ func NewKickstartStageOptions( groups = groupsOptions.Groups } + rootpw, err := adjustRootUserOptions(users) + if err != nil { + return nil, fmt.Errorf("org.osbuild.kickstart: %w", err) + } + return &KickstartStageOptions{ Path: path, OSTreeCommit: nil, LiveIMG: nil, Users: users, Groups: groups, + RootPassword: rootpw, }, nil } diff --git a/pkg/osbuild/kickstart_stage_test.go b/pkg/osbuild/kickstart_stage_test.go index 63a2768b08..45371ad7fb 100644 --- a/pkg/osbuild/kickstart_stage_test.go +++ b/pkg/osbuild/kickstart_stage_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/osbuild/images/internal/common" + "github.com/osbuild/images/pkg/customizations/users" "github.com/osbuild/images/pkg/osbuild" ) @@ -31,3 +33,176 @@ func TestKickstartStageJsonHappy(t *testing.T) { } }`) } + +func TestKickstartStageUsers(t *testing.T) { + type testCase struct { + users []users.User + expected *osbuild.KickstartStageOptions + expErr string + } + + testCases := map[string]testCase{ + "empty": { + users: nil, + expected: &osbuild.KickstartStageOptions{}, + expErr: "", + }, + "1-user": { + users: []users.User{ + { + Name: "user", + Description: common.ToPtr("I am user"), + Password: common.ToPtr("$6$fakesalt$fakehashedpassword"), + Key: common.ToPtr("ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + Home: common.ToPtr("/var/home/user"), + Shell: common.ToPtr("/usr/bin/fish"), + Groups: []string{"grp1", "wheel"}, + UID: common.ToPtr(1010), + GID: common.ToPtr(1020), + ExpireDate: common.ToPtr(1756486205), + ForcePasswordReset: common.ToPtr(false), + }, + }, + expected: &osbuild.KickstartStageOptions{ + Users: map[string]osbuild.UsersStageOptionsUser{ + "user": { + UID: common.ToPtr(1010), + GID: common.ToPtr(1020), + Groups: []string{"grp1", "wheel"}, + Description: common.ToPtr("I am user"), + Home: common.ToPtr("/var/home/user"), + Shell: common.ToPtr("/usr/bin/fish"), + Password: common.ToPtr("$6$fakesalt$fakehashedpassword"), + Key: common.ToPtr("ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + ExpireDate: common.ToPtr(1756486205), + ForcePasswordReset: common.ToPtr(false), + }, + }, + }, + expErr: "", + }, + "2-user+root": { + users: []users.User{ + { + Name: "user", + Description: common.ToPtr("I am user"), + Password: common.ToPtr("$6$fakesalt$fakehashedpassword"), + Key: common.ToPtr("ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + Home: common.ToPtr("/var/home/user"), + Shell: common.ToPtr("/usr/bin/fish"), + Groups: []string{"grp1", "wheel"}, + UID: common.ToPtr(1010), + GID: common.ToPtr(1020), + ExpireDate: common.ToPtr(1756486205), + ForcePasswordReset: common.ToPtr(false), + }, + { + Name: "root", + Password: common.ToPtr("$6$fakesaltroot$fakehashedpasswordroot"), + Key: common.ToPtr("ssh-ed25519 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"), + }, + }, + expected: &osbuild.KickstartStageOptions{ + Users: map[string]osbuild.UsersStageOptionsUser{ + "user": { + Description: common.ToPtr("I am user"), + Password: common.ToPtr("$6$fakesalt$fakehashedpassword"), + Key: common.ToPtr("ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + Home: common.ToPtr("/var/home/user"), + Shell: common.ToPtr("/usr/bin/fish"), + Groups: []string{"grp1", "wheel"}, + UID: common.ToPtr(1010), + GID: common.ToPtr(1020), + ExpireDate: common.ToPtr(1756486205), + ForcePasswordReset: common.ToPtr(false), + }, + "root": { + Key: common.ToPtr("ssh-ed25519 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"), + }, + }, + RootPassword: &osbuild.RootPasswordOptions{ + Lock: false, + PlainText: false, + IsCrypted: true, + AllowSSH: false, + Password: "$6$fakesaltroot$fakehashedpasswordroot", + }, + }, + }, + "2-user+root-error": { + users: []users.User{ + { + Name: "user", + Description: common.ToPtr("I am user"), + Password: common.ToPtr("$6$fakesalt$fakehashedpassword"), + Key: common.ToPtr("ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + Home: common.ToPtr("/var/home/user"), + Shell: common.ToPtr("/usr/bin/fish"), + Groups: []string{"grp1", "wheel"}, + UID: common.ToPtr(1010), + GID: common.ToPtr(1020), + ExpireDate: common.ToPtr(1756486205), + ForcePasswordReset: common.ToPtr(false), + }, + { + Name: "root", + Description: common.ToPtr("super!"), + Password: common.ToPtr("$6$fakesaltroot$fakehashedpasswordroot"), + Key: common.ToPtr("ssh-ed25519 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"), + Home: common.ToPtr("/rooot"), + Shell: common.ToPtr("/usr/bin/zsh"), + Groups: []string{"wheel?"}, + UID: common.ToPtr(10), + GID: common.ToPtr(20), + ExpireDate: common.ToPtr(1756486205), + ForcePasswordReset: common.ToPtr(false), + }, + }, + expected: &osbuild.KickstartStageOptions{ + Users: map[string]osbuild.UsersStageOptionsUser{ + "user": { + Description: common.ToPtr("I am user"), + Password: common.ToPtr("$6$fakesalt$fakehashedpassword"), + Key: common.ToPtr("ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + Home: common.ToPtr("/var/home/user"), + Shell: common.ToPtr("/usr/bin/fish"), + Groups: []string{"grp1", "wheel"}, + UID: common.ToPtr(1010), + GID: common.ToPtr(1020), + ExpireDate: common.ToPtr(1756486205), + ForcePasswordReset: common.ToPtr(false), + }, + "root": { + Description: common.ToPtr("super!"), + Password: common.ToPtr("$6$fakesaltroot$fakehashedpasswordroot"), + Key: common.ToPtr("ssh-ed25519 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"), + Home: common.ToPtr("/rooot"), + Shell: common.ToPtr("/usr/bin/zsh"), + Groups: []string{"wheel?"}, + UID: common.ToPtr(10), + GID: common.ToPtr(20), + ExpireDate: common.ToPtr(1756486205), + ForcePasswordReset: common.ToPtr(false), + }, + }, + }, + expErr: "org.osbuild.kickstart: unsupported options for user \"root\": expiredate, force_password_reset, gid, groups, home, shell, uid", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + ksOpts, err := osbuild.NewKickstartStageOptions("", tc.users, nil) + if tc.expErr != "" { + assert.EqualError(err, tc.expErr) + return + } + + assert.NoError(err) + assert.Equal(tc.expected, ksOpts) + }) + } + +} diff --git a/test/configs/bigiso.json b/test/configs/bigiso.json index 4453a9530d..a43e3f1d54 100644 --- a/test/configs/bigiso.json +++ b/test/configs/bigiso.json @@ -22,6 +22,11 @@ "customizations": { "hostname": "custombase", "user": [ + { + "name": "root", + "description": "Mr. Root", + "password": "$6$BhyxFBgrEFh0VrPJ$MllG8auiU26x2pmzL4.1maHzPHrA.4gTdCvlATFp8HJU9UPee4zCS9BVl2HOzKaUYD/zEm8r/OF05F2icWB0K/" + }, { "name": "admin", "description": "admin", diff --git a/test/data/manifest-checksums/rhel_9.4-x86_64-image_installer-bigiso b/test/data/manifest-checksums/rhel_9.4-x86_64-image_installer-bigiso index 75916c5dfe..a3b2f1c193 100644 --- a/test/data/manifest-checksums/rhel_9.4-x86_64-image_installer-bigiso +++ b/test/data/manifest-checksums/rhel_9.4-x86_64-image_installer-bigiso @@ -1 +1 @@ -10b7a78a33cc91cdfd161418e23767f4dd4cd7b7 +ffc964d978a7b38963d21407cf823bf60da77cdb