constellation/internal/deploy/user/linux_user.go
Nils Hanke 68092f27dd AB#2046 : Add option to create SSH users for the first coordinator upon initialization (#133)
* Move `file`, `ssh` and `user` packages to internal
* Rename `SSHKey` to `(ssh.)UserKey`
* Rename KeyValue / Publickey to PublicKey
* Rename SSH key file from "debugd" to "ssh-keys"
* Add CreateSSHUsers function to Core
* Call CreateSSHUsers users on first control-plane node, when defined in config

Tests:
* Make StubUserCreator add entries to /etc/passwd
* Add NewLinuxUserManagerFake for unit tests
* Add unit tests & adjust existing ones to changes
2022-05-16 17:32:00 +02:00

148 lines
3.6 KiB
Go

package user
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"github.com/spf13/afero"
)
// ErrUserDoesNotExist is returned by GetLinuxUser if a linux user does not exist yet.
var ErrUserDoesNotExist = errors.New("user does not exist")
type passwdParser interface {
Parse(fs afero.Fs) (Entries, error)
}
type userCreator interface {
CreateUser(ctx context.Context, username string) error
}
// LinuxUser holds relevant information about a linux user (subset of /etc/passwd).
type LinuxUser struct {
Username string
Home string
Uid int
Gid int
}
// LinuxUserManager can retrieve information on linux users and create new users.
type LinuxUserManager struct {
Fs afero.Fs
Passwd passwdParser
Creator userCreator
}
// NewLinuxUserManager creates a new LinuxUserManager.
func NewLinuxUserManager(fs afero.Fs) LinuxUserManager {
return LinuxUserManager{
Fs: fs,
Passwd: Passwd{},
Creator: Unix{},
}
}
// NewLinuxUserManagerFake creates a new LinuxUserManager that is used for unit tests.
func NewLinuxUserManagerFake(fs afero.Fs) LinuxUserManager {
return LinuxUserManager{
Fs: fs,
Passwd: Passwd{},
Creator: &StubUserCreator{fs: fs},
}
}
// StubUserCreator is used for unit tests.
type StubUserCreator struct {
fs afero.Fs
usernames []string
createUserErr error
currentUID int
}
func (s *StubUserCreator) CreateUser(ctx context.Context, username string) error {
if stringInSlice(username, s.usernames) {
return errors.New("username already exists")
}
// We want created users to start at UID 1000
if s.currentUID == 0 {
s.currentUID = 1000
}
if s.createUserErr != nil {
return s.createUserErr
}
// If no predefined error is supposed to happen, increase the UID (unless the file system code fails)
if s.fs != nil {
lineToWrite := fmt.Sprintf("%s:x:%d:%d:%s:/home/%s:/bin/bash\n", username, s.currentUID, s.currentUID, username, username)
file, err := s.fs.OpenFile("/etc/passwd", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
if err != nil {
return err
}
defer file.Close()
n, err := file.WriteString(lineToWrite)
if err != nil {
return err
} else if n != len(lineToWrite) {
return errors.New("written text too short")
}
}
s.currentUID += 1
s.usernames = append(s.usernames, username)
return nil
}
// getLinuxUser tries to find an existing linux user in /etc/passwd.
func (l *LinuxUserManager) getLinuxUser(username string) (LinuxUser, error) {
entries, err := l.Passwd.Parse(l.Fs)
if err != nil {
return LinuxUser{}, err
}
if _, ok := entries[username]; !ok {
return LinuxUser{}, ErrUserDoesNotExist
}
entry := entries[username]
uid, err := strconv.Atoi(entry.Uid)
if err != nil {
return LinuxUser{}, fmt.Errorf("failed to parse users uid: %w", err)
}
gid, err := strconv.Atoi(entry.Gid)
if err != nil {
return LinuxUser{}, fmt.Errorf("failed to parse users gid: %w", err)
}
return LinuxUser{
Username: username,
Home: entry.Home,
Uid: uid,
Gid: gid,
}, nil
}
// EnsureLinuxUserExists will try to create the user specified by username and call GetLinuxUser to retrieve user information.
func (l *LinuxUserManager) EnsureLinuxUserExists(ctx context.Context, username string) (LinuxUser, error) {
// try to create user (even if it already exists)
if err := l.Creator.CreateUser(ctx, username); err != nil {
return LinuxUser{}, err
}
return l.getLinuxUser(username)
}
// stringInSlice checks if a given string exists in a slice of strings.
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}