Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to create OS users #44

Merged
merged 3 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/building-images.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions pkg/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions pkg/build/scripts/users/add-users.sh.tpl
Original file line number Diff line number Diff line change
@@ -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 }}
Comment on lines +13 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the same for both cases can we extract it down below in order to avoid the repetition?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO that makes it less readable, but that may be a personal distaste for the templating syntax. I like the model here where we have distinct section on root v. non-root and, IMO, that's worth the minimal duplication.

{{ 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") }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason not to {{ else }} instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing specific. My head was in a different place editing a template v. code so I just wasn't thinking of it as a if/else construct so much as a flag for inclusion (not sure if that made sense).

{{ if .Password }}
echo '{{.Username}}:{{.Password}}' | chpasswd -e
{{ end }}
{{ if .SSHKey }}
mkdir -pm700 /{{.Username}}/.ssh/
echo '{{.SSHKey}}' >> /{{.Username}}/.ssh/authorized_keys
{{ end }}
{{ end }}

{{ end }}
35 changes: 35 additions & 0 deletions pkg/build/users.go
Original file line number Diff line number Diff line change
@@ -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
}
119 changes: 119 additions & 0 deletions pkg/build/users_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
9 changes: 8 additions & 1 deletion pkg/config/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
14 changes: 13 additions & 1 deletion pkg/config/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions pkg/config/testdata/full-valid-example.yaml
Original file line number Diff line number Diff line change
@@ -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= [email protected]
- 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= [email protected]
9 changes: 0 additions & 9 deletions pkg/config/testdata/valid_example.yaml

This file was deleted.