Files
clr-installer/user/user.go
T
Mark D Horn dad433e744 user: Allow upper case and periods in login name
Fixes: #432

Reverts commit 3b284e0865
plus allows periods in the login name.

Recent change to Clear Linux adduser now allows upper case
letters as well as periods in the login name.
See https://github.com/clearlinux/distribution/issues/829

Signed-off-by: Mark D Horn <mark.d.horn@intel.com>
2019-06-25 17:52:37 -07:00

466 lines
10 KiB
Go

// Copyright © 2018 Intel Corporation
//
// SPDX-License-Identifier: GPL-3.0-only
package user
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/clearlinux/clr-installer/cmd"
"github.com/clearlinux/clr-installer/conf"
"github.com/clearlinux/clr-installer/encrypt"
"github.com/clearlinux/clr-installer/errors"
"github.com/clearlinux/clr-installer/log"
"github.com/clearlinux/clr-installer/progress"
"github.com/clearlinux/clr-installer/utils"
)
// User abstracts a target system definition
type User struct {
Login string `yaml:"login,omitempty"`
UserName string `yaml:"username,omitempty,flow"`
Password string `yaml:"password,omitempty,flow"`
Admin bool `yaml:"admin,omitempty,flow"`
SSHKeys []string `yaml:"ssh-keys,omitempty,flow"`
}
const (
defaultUsersFile = "/usr/share/defaults/etc/passwd"
// MaxUsernameLength is the longest possible username
MaxUsernameLength = 64
// MaxLoginLength is the longest possible login
MaxLoginLength = 31
// MinPasswordLength is the shortest possible password
MinPasswordLength = 8
// MaxPasswordLength is the shortest possible password
MaxPasswordLength = 255
// RequiredBundle the bundle needed to enable non-root user accounts
RequiredBundle = "sysadmin-basic"
)
var (
usernameExp = regexp.MustCompile("^([a-zA-Z]+[0-9a-zA-Z-_ ,'.]*|)$")
loginExp = regexp.MustCompile("^[a-zA-Z]+[0-9a-zA-Z-_.]*$")
sysDefaultUsers = []string{}
)
// IsSysDefaultUser checks if a given login is in the list of default users
func IsSysDefaultUser(login string) (bool, error) {
if login == "" {
return false, nil
}
if err := loadSysDefaultUsers(); err != nil {
return false, err
}
for _, curr := range sysDefaultUsers {
if curr == login {
return true, nil
}
}
return false, nil
}
func loadSysDefaultUsers() error {
if len(sysDefaultUsers) != 0 {
return nil
}
content, err := ioutil.ReadFile(defaultUsersFile)
if err != nil {
return err
}
for _, line := range strings.Split(string(content), "\n") {
tks := strings.Split(line, ":")
if len(tks) == 0 {
return errors.Errorf("Could not parse passwd file, line: %s", line)
}
sysDefaultUsers = append(sysDefaultUsers, tks[0])
}
return nil
}
// NewUser creates/allocates a new user handle
func NewUser(login string, username string, pwd string, admin bool) (*User, error) {
hashed, err := encrypt.Crypt(pwd)
if err != nil {
return nil, err
}
return &User{
Login: login,
UserName: username,
Password: hashed,
Admin: admin,
}, nil
}
// SetPassword sets a users password
func (u *User) SetPassword(pwd string) error {
hashed, err := encrypt.Crypt(pwd)
if err != nil {
return err
}
u.Password = hashed
return nil
}
// Equals returns true if u and usr point to the same struct or if both have
// the same Login string
func (u *User) Equals(usr *User) bool {
return u == usr || u.Login == usr.Login
}
// setTempTargetPAMConfig copy the temporary chpasswd PAM config to target system
// this is required for changing user's password into target system.
func setTempTargetPAMConfig(rootDir string) error {
var chpasswdFile string
var err error
pamDir := filepath.Join(rootDir, "etc", "pam.d")
if err = utils.MkdirAll(pamDir, 0755); err != nil {
return err
}
if chpasswdFile, err = conf.LookupChpasswdConfig(); err != nil {
return err
}
targetPamFile := filepath.Join(pamDir, conf.ChpasswdPAMFile)
if err = utils.CopyFile(chpasswdFile, targetPamFile); err != nil {
return err
}
return nil
}
// Apply creates the user and sets their password into chroot'ed rootDir
func Apply(rootDir string, users []*User) error {
if len(users) == 0 {
return nil
}
prg := progress.NewLoop(utils.Locale.Get("Adding extra users"))
if err := setTempTargetPAMConfig(rootDir); err != nil {
prg.Failure()
return err
}
// Should we lock out the root account?
haveAdmins := false
rootPassSet := false
rootSSHOnly := false
for _, usr := range users {
log.Info("Adding extra user '%s'", usr.Login)
if err := usr.apply(rootDir); err != nil {
prg.Failure()
return err
}
if usr.Admin {
haveAdmins = true
}
// This should not be possible in the TUI as all system accounts
// are not allowed to be defined, but is possible via the command
// line (aka mass installer)
if usr.Login == "root" {
if usr.Password == "" {
if len(usr.SSHKeys) > 0 {
rootSSHOnly = true
}
} else {
rootPassSet = true
}
}
}
// If the root account is not defined with an encrypted password and
// we have user account which are Admin (sudo)
// OR
// The root account is defined with SSH Keys, no password
if (!rootPassSet && haveAdmins) || rootSSHOnly {
log.Info("Disabling the 'root' account.")
if err := disableRoot(rootDir); err != nil {
prg.Failure()
return err
}
}
prg.Success()
return nil
}
// disableRoot will lockout the root account
// should be called only when adding an account which
// has been granted admin privileges (sudo)
func disableRoot(rootDir string) error {
// Lock the account
args := []string{
"chroot",
rootDir,
"usermod",
"--lock",
"root",
}
if err := cmd.RunAndLog(args...); err != nil {
return errors.Wrap(err)
}
// How many days since the beginning of (UNIX) time
beginning := time.Date(1970, time.Month(1), 1, 0, 0, 0, 0, time.UTC)
now := time.Now()
days := fmt.Sprintf("%d", int64(now.Sub(beginning).Hours()/24))
// Set a password change date so we are not prompted
// when sudo'ing to root account or when ssh'ing at root
args = []string{
"chroot",
rootDir,
"chage",
"--lastday",
days,
"root",
}
if err := cmd.RunAndLog(args...); err != nil {
return errors.Wrap(err)
}
return nil
}
// userExist will lockout the root account
// should be called only when adding an account which
// has been granted admin privileges (sudo)
func (u *User) userExist(rootDir string) bool {
exists := true
args := []string{
"chroot",
rootDir,
"getent",
"passwd",
u.Login,
}
if err := cmd.RunAndLog(args...); err != nil {
exists = false
}
return exists
}
// getUserHome returns the home directory of the user
// on the installation target
func (u *User) getUserHome(rootDir string) string {
home := filepath.Join("/home", u.Login)
// Ask for the accounts passwd entry and parse the home directory
args := []string{
"chroot",
rootDir,
"getent",
"passwd",
u.Login,
}
w := bytes.NewBuffer(nil)
err := cmd.Run(w, args...)
if err != nil {
return home
}
getent := bytes.Split(w.Bytes(), []byte(":"))
homeDir := string(getent[len(getent)-2])
if len(homeDir) > 0 {
home = homeDir
}
return home
}
// apply applies the user configuration to the target install
func (u *User) apply(rootDir string) error {
accountAdded := false
if u.userExist(rootDir) {
log.Info("Account '%s' already a defined system account, skipping add.", u.Login)
} else {
args := []string{
"chroot",
rootDir,
"useradd",
"--comment",
u.UserName,
u.Login,
}
if u.Admin {
args = append(args, []string{
"-G",
"wheel",
}...)
}
if err := cmd.RunAndLog(args...); err != nil {
return errors.Wrap(err)
}
accountAdded = true
}
if u.Password != "" {
if !accountAdded {
// Unlock the account
// This is hack to ensure the account gets added to the
// /etc/passwd file before trying to set the password with
// chpasswd as the ch* commands only look in /etc
args := []string{
"chroot",
rootDir,
"usermod",
"--unlock",
u.Login,
}
if err := cmd.RunAndLog(args...); err != nil {
return errors.Wrap(err)
}
}
args := []string{
"chroot",
rootDir,
"chpasswd",
"-e",
}
pwd := fmt.Sprintf("%s:%s", u.Login, u.Password)
if err := cmd.PipeRunAndLog(pwd, args...); err != nil {
return errors.Wrap(err)
}
}
if len(u.SSHKeys) > 0 {
if err := writeSSHKey(rootDir, u); err != nil {
return err
}
}
return nil
}
func writeSSHKey(rootDir string, u *User) error {
sshDir := filepath.Join(u.getUserHome(rootDir), ".ssh")
dpath := filepath.Join(rootDir, sshDir)
fpath := filepath.Join(dpath, "authorized_keys")
if err := utils.MkdirAll(dpath, 0700); err != nil {
return err
}
f, err := os.OpenFile(fpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()
cnt := fmt.Sprintf("%s\n", strings.Join(u.SSHKeys, "\n"))
bt := []byte(cnt)
n, err := f.Write(bt)
if err != nil {
return err
}
if n != len(bt) {
return errors.Errorf("Failed to write ssh key, wrote %d of %d bytes", n, len(bt))
}
args := []string{
"chroot",
rootDir,
"/usr/bin/chown",
"-R",
fmt.Sprintf("%s:%s", u.Login, u.Login),
sshDir,
}
if err := cmd.RunAndLog(args...); err != nil {
return err
}
return nil
}
// IsValidUsername checks the username restrictions
func IsValidUsername(username string) (bool, string) {
if !usernameExp.MatchString(username) {
return false, utils.Locale.Get("Username must contain only numbers, letters, commas, - or _")
}
if len(username) > MaxUsernameLength {
return false, utils.Locale.Get("UserName maximum length is %d", MaxUsernameLength)
}
return true, ""
}
// IsValidLogin checks the minimum login requirements
func IsValidLogin(login string) (bool, string) {
if login == "" {
return false, utils.Locale.Get("Login is required")
}
if len(login) > MaxLoginLength {
return false, utils.Locale.Get("Login maximum length is %d", MaxLoginLength)
}
if !loginExp.MatchString(login) {
return false, utils.Locale.Get("Login must contain only numbers, letters, -, _ or .")
}
return true, ""
}
// IsValidPassword checks the minimum password requirements
func IsValidPassword(pwd string) (bool, string) {
if pwd == "" {
return false, utils.Locale.Get("Password is required")
}
if len(pwd) < MinPasswordLength {
return false, utils.Locale.Get("Password must be at least %d characters long", MinPasswordLength)
}
if len(pwd) > MaxPasswordLength {
return false, utils.Locale.Get("Password may be at most %d characters long", MaxPasswordLength)
}
return true, ""
}