Skip to content

Commit

Permalink
Added ability to create OS users (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdob authored Nov 17, 2023
1 parent 52147f3 commit 3962567
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 11 deletions.
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 }}
{{ 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 }}
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.

0 comments on commit 3962567

Please sign in to comment.