From 3962567cbe2bfe64a20e2c98f383cb825c9da8f2 Mon Sep 17 00:00:00 2001 From: Jason Dobies Date: Fri, 17 Nov 2023 10:32:01 -0500 Subject: [PATCH] Added ability to create OS users (#44) --- docs/building-images.md | 15 +++ pkg/build/build.go | 5 + pkg/build/scripts/users/add-users.sh.tpl | 34 ++++++ pkg/build/users.go | 35 ++++++ pkg/build/users_test.go | 119 ++++++++++++++++++++ pkg/config/image.go | 9 +- pkg/config/image_test.go | 14 ++- pkg/config/testdata/full-valid-example.yaml | 17 +++ pkg/config/testdata/valid_example.yaml | 9 -- 9 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 pkg/build/scripts/users/add-users.sh.tpl create mode 100644 pkg/build/users.go create mode 100644 pkg/build/users_test.go create mode 100644 pkg/config/testdata/full-valid-example.yaml delete mode 100644 pkg/config/testdata/valid_example.yaml diff --git a/docs/building-images.md b/docs/building-images.md index 1512380e..d652c9e8 100644 --- a/docs/building-images.md +++ b/docs/building-images.md @@ -38,9 +38,24 @@ operatingSystem: kernelArgs: - arg1 - arg2 + users: + - username: user1 + password: 123 + sshKey: user1Key + - username: user2 + password: 456 + - username: user3 + sshKey: user3Key ``` * `kernelArgs` - Optional; Provides a list of flags that should be passed to the kernel on boot. +* `users` - Optional; Defines a list of operating system users to be created. Each entry is made up of + the following fields: + * `username` - Required; Username of the user to create. To set the password or SSH key for the root user, + use the value `root` for this field. + * `password` - Optional; Encrypted password to set for the use (for example, using `openssl passwd -6 $PASSWORD` + to generate the value for this field). + * `sshKey` - Optional; Full public SSH key to configure for the user. ## Image Configuration Directory diff --git a/pkg/build/build.go b/pkg/build/build.go index 6bbe2716..a7df8187 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -39,6 +39,11 @@ func (b *Builder) Build() error { return fmt.Errorf("configuring custom scripts: %w", err) } + err = b.configureUsers() + if err != nil { + return fmt.Errorf("configuring users: %w", err) + } + err = b.generateCombustionScript() if err != nil { return fmt.Errorf("generating combustion script: %w", err) diff --git a/pkg/build/scripts/users/add-users.sh.tpl b/pkg/build/scripts/users/add-users.sh.tpl new file mode 100644 index 00000000..5f2225a4 --- /dev/null +++ b/pkg/build/scripts/users/add-users.sh.tpl @@ -0,0 +1,34 @@ +#!/bin/bash +set -euo pipefail + +# Without this, the script will run successfully during combustion, but when /home +# is mounted it will hide the /home used during these user creations. +mount /home + +{{ range . }} + +{{/* Non-root users */}} +{{ if (ne .Username "root") }} + useradd -m {{.Username}} + {{ if .Password }} + echo '{{.Username}}:{{.Password}}' | chpasswd -e + {{ end }} + {{ if .SSHKey }} + mkdir -pm700 /home/{{.Username}}/.ssh/ + echo '{{.SSHKey}}' >> /home/{{.Username}}/.ssh/authorized_keys + chown -R {{.Username}} /home/{{.Username}}/.ssh + {{ end }} +{{ end }} + +{{/* Root */}} +{{ if (eq .Username "root") }} + {{ if .Password }} + echo '{{.Username}}:{{.Password}}' | chpasswd -e + {{ end }} + {{ if .SSHKey }} + mkdir -pm700 /{{.Username}}/.ssh/ + echo '{{.SSHKey}}' >> /{{.Username}}/.ssh/authorized_keys + {{ end }} +{{ end }} + +{{ end }} \ No newline at end of file diff --git a/pkg/build/users.go b/pkg/build/users.go new file mode 100644 index 00000000..7294c5b0 --- /dev/null +++ b/pkg/build/users.go @@ -0,0 +1,35 @@ +package build + +import ( + _ "embed" + "fmt" + "os" +) + +const ( + usersScriptName = "add-users.sh" + userScriptMode = 0o744 +) + +//go:embed scripts/users/add-users.sh.tpl +var usersScript string + +func (b *Builder) configureUsers() error { + // Punch out early if there are no users + if len(b.imageConfig.OperatingSystem.Users) == 0 { + return nil + } + + filename, err := b.writeCombustionFile(usersScriptName, usersScript, b.imageConfig.OperatingSystem.Users) + if err != nil { + return fmt.Errorf("writing %s to the combustion directory: %w", usersScriptName, err) + } + err = os.Chmod(filename, userScriptMode) + if err != nil { + return fmt.Errorf("modifying permissions for script %s: %w", filename, err) + } + + b.registerCombustionScript(usersScriptName) + + return nil +} diff --git a/pkg/build/users_test.go b/pkg/build/users_test.go new file mode 100644 index 00000000..0c4097eb --- /dev/null +++ b/pkg/build/users_test.go @@ -0,0 +1,119 @@ +package build + +import ( + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/suse-edge/edge-image-builder/pkg/config" +) + +func TestConfigureUsers(t *testing.T) { + // Setup + imageConfig := &config.ImageConfig{ + OperatingSystem: config.OperatingSystem{ + Users: []config.OperatingSystemUser{ + { + Username: "alpha", + Password: "alpha123", + SSHKey: "alphakey", + }, + { + Username: "beta", + Password: "beta123", + }, + { + Username: "gamma", + SSHKey: "gammakey", + }, + { + Username: "root", + Password: "root123", + SSHKey: "rootkey", + }, + }, + }, + } + + context, err := NewContext("", "", true) + require.NoError(t, err) + defer func() { + assert.NoError(t, CleanUpBuildDir(context)) + }() + + builder := &Builder{ + imageConfig: imageConfig, + context: context, + } + + // Test + err = builder.configureUsers() + + // Verify + require.NoError(t, err) + + expectedFilename := filepath.Join(context.CombustionDir, usersScriptName) + foundBytes, err := os.ReadFile(expectedFilename) + require.NoError(t, err) + + stats, err := os.Stat(expectedFilename) + require.NoError(t, err) + assert.Equal(t, fs.FileMode(userScriptMode), stats.Mode()) + + foundContents := string(foundBytes) + + // - All fields specified + assert.Contains(t, foundContents, "useradd -m alpha") + assert.Contains(t, foundContents, "echo 'alpha:alpha123' | chpasswd -e\n") + assert.Contains(t, foundContents, "mkdir -pm700 /home/alpha/.ssh/") + assert.Contains(t, foundContents, "echo 'alphakey' >> /home/alpha/.ssh/authorized_keys") + assert.Contains(t, foundContents, "chown -R alpha /home/alpha/.ssh") + + // - Only a password set + assert.Contains(t, foundContents, "useradd -m beta") + assert.Contains(t, foundContents, "echo 'beta:beta123' | chpasswd -e\n") + assert.NotContains(t, foundContents, "mkdir -pm700 /home/beta/.ssh/") + assert.NotContains(t, foundContents, "/home/beta/.ssh/authorized_keys") + assert.NotContains(t, foundContents, "chown -R beta /home/beta/.ssh") + + // - Only an SSH key specified + assert.Contains(t, foundContents, "useradd -m gamma") + assert.NotContains(t, foundContents, "echo 'gamma:") + assert.Contains(t, foundContents, "mkdir -pm700 /home/gamma/.ssh/") + assert.Contains(t, foundContents, "echo 'gammakey' >> /home/gamma/.ssh/authorized_keys") + assert.Contains(t, foundContents, "chown -R gamma /home/gamma/.ssh") + + // - Special handling for root + assert.NotContains(t, foundContents, "useradd -m root") + assert.Contains(t, foundContents, "echo 'root:root123' | chpasswd -e\n") + assert.Contains(t, foundContents, "mkdir -pm700 /root/.ssh/") + assert.Contains(t, foundContents, "echo 'rootkey' >> /root/.ssh/authorized_keys") + assert.NotContains(t, foundContents, "chown -R root") +} + +func TestConfigureUsers_NoUsers(t *testing.T) { + // Setup + context, err := NewContext("", "", true) + require.NoError(t, err) + defer func() { + assert.NoError(t, CleanUpBuildDir(context)) + }() + + builder := &Builder{ + imageConfig: &config.ImageConfig{}, + context: context, + } + + // Test + err = builder.configureUsers() + + // Verify + require.NoError(t, err) + + expectedFilename := filepath.Join(context.CombustionDir, usersScriptName) + _, err = os.ReadFile(expectedFilename) + require.ErrorIs(t, err, os.ErrNotExist) +} diff --git a/pkg/config/image.go b/pkg/config/image.go index 1c618590..1cf2f078 100644 --- a/pkg/config/image.go +++ b/pkg/config/image.go @@ -24,7 +24,14 @@ type Image struct { } type OperatingSystem struct { - KernelArgs []string `yaml:"kernelArgs"` + KernelArgs []string `yaml:"kernelArgs"` + Users []OperatingSystemUser `yaml:"users"` +} + +type OperatingSystemUser struct { + Username string `yaml:"username"` + Password string `yaml:"password"` + SSHKey string `yaml:"sshKey"` } func Parse(data []byte) (*ImageConfig, error) { diff --git a/pkg/config/image_test.go b/pkg/config/image_test.go index b7488b20..a8587b45 100644 --- a/pkg/config/image_test.go +++ b/pkg/config/image_test.go @@ -10,7 +10,7 @@ import ( func TestParse(t *testing.T) { // Setup - filename := "./testdata/valid_example.yaml" + filename := "./testdata/full-valid-example.yaml" configData, err := os.ReadFile(filename) require.NoError(t, err) @@ -29,6 +29,18 @@ func TestParse(t *testing.T) { "beta=bar", } assert.Equal(t, expectedKernelArgs, imageConfig.OperatingSystem.KernelArgs) + + userConfigs := imageConfig.OperatingSystem.Users + require.Len(t, userConfigs, 3) + assert.Equal(t, "alpha", userConfigs[0].Username) + assert.Equal(t, "$6$bZfTI3Wj05fdxQcB$W1HJQTKw/MaGTCwK75ic9putEquJvYO7vMnDBVAfuAMFW58/79abky4mx9.8znK0UZwSKng9dVosnYQR1toH71", userConfigs[0].Password) + assert.Contains(t, userConfigs[0].SSHKey, "ssh-rsa AAAAB3") + assert.Equal(t, "beta", userConfigs[1].Username) + assert.Equal(t, "$6$GHjiVHm2AT.Qxznz$1CwDuEBM1546E/sVE1Gn1y4JoGzW58wrckyx3jj2QnphFmceS6b/qFtkjw1cp7LSJNW1OcLe/EeIxDDHqZU6o1", userConfigs[1].Password) + assert.Equal(t, "", userConfigs[1].SSHKey) + assert.Equal(t, "gamma", userConfigs[2].Username) + assert.Equal(t, "", userConfigs[2].Password) + assert.Contains(t, userConfigs[2].SSHKey, "ssh-rsa BBBBB3") } func TestParseBadConfig(t *testing.T) { diff --git a/pkg/config/testdata/full-valid-example.yaml b/pkg/config/testdata/full-valid-example.yaml new file mode 100644 index 00000000..c194230b --- /dev/null +++ b/pkg/config/testdata/full-valid-example.yaml @@ -0,0 +1,17 @@ +apiVersion: 1.0 +image: + imageType: iso + baseImage: slemicro5.5.iso + outputImageName: eibimage.iso +operatingSystem: + kernelArgs: + - alpha=foo + - beta=bar + users: + - username: alpha + password: $6$bZfTI3Wj05fdxQcB$W1HJQTKw/MaGTCwK75ic9putEquJvYO7vMnDBVAfuAMFW58/79abky4mx9.8znK0UZwSKng9dVosnYQR1toH71 + sshKey: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDnb80jkq8jYqC7EeXdtmdMLoQ/qeCzFPRrNyA5H5iB3k21Oc8ccBR2nIbteam39E0p4mwR2MVNACOR0cixgWskIb5bR8KqiqLMdj4PKMLX5r1jbtcB3/6beBKPqOpk0N2NwTy5BUH8NMwRpdzcq0QeY60f1z+PLJ4vTb0mcdyRkO4m0mqGa/LrBn9H5V3AAW6TdLO9LKjvUqHX+6vWKiWu2wJffTQQAxY9rsT+JoBVk8zes06zh+CVd7bGozJXp1t6SHQjJ7V9pLNfdMO4TJFpi3mVh3RLsg24RGoMVRNCjfYaBQkUJununzpPB9O9esOhfffM2puumAkspPALMiODcYK5bzF26YvDM124e5VQJo50GqbTNJEXB7PsZF4TezivS5xCuGoO6sSrk+heWKzgnLK7/qHI55XuExBbzfTawwWpGrSOw4YYCkrCa0bPYsY8Ef5iIQMwFseWz0i57eZp2pJfn65p4osM+r08R+X8BwEvK+BsyW/wtCI06xwFtdM= root@localhost.localdomain + - username: beta + password: $6$GHjiVHm2AT.Qxznz$1CwDuEBM1546E/sVE1Gn1y4JoGzW58wrckyx3jj2QnphFmceS6b/qFtkjw1cp7LSJNW1OcLe/EeIxDDHqZU6o1 + - username: gamma + sshKey: ssh-rsa BBBBB3NzaC1yc2EAAAADAQABAAABgQDnb80jkq8jYqC7EeXdtmdMLoQ/qeCzFPRrNyA5H5iB3k21Oc8ccBR2nIbteam39E0p4mwR2MVNACOR0cixgWskIb5bR8KqiqLMdj4PKMLX5r1jbtcB3/6beBKPqOpk0N2NwTy5BUH8NMwRpdzcq0QeY60f1z+PLJ4vTb0mcdyRkO4m0mqGa/LrBn9H5V3AAW6TdLO9LKjvUqHX+6vWKiWu2wJffTQQAxY9rsT+JoBVk8zes06zh+CVd7bGozJXp1t6SHQjJ7V9pLNfdMO4TJFpi3mVh3RLsg24RGoMVRNCjfYaBQkUJununzpPB9O9esOhfffM2puumAkspPALMiODcYK5bzF26YvDM124e5VQJo50GqbTNJEXB7PsZF4TezivS5xCuGoO6sSrk+heWKzgnLK7/qHI55XuExBbzfTawwWpGrSOw4YYCkrCa0bPYsY8Ef5iIQMwFseWz0i57eZp2pJfn65p4osM+r08R+X8BwEvK+BsyW/wtCI06xwFtdM= root@localhost.localdomain diff --git a/pkg/config/testdata/valid_example.yaml b/pkg/config/testdata/valid_example.yaml deleted file mode 100644 index 7cc89d92..00000000 --- a/pkg/config/testdata/valid_example.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: 1.0 -image: - imageType: iso - baseImage: slemicro5.5.iso - outputImageName: eibimage.iso -operatingSystem: - kernelArgs: - - alpha=foo - - beta=bar