github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/osutil/user.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2015 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package osutil
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"os/exec"
    26  	"os/user"
    27  	"path/filepath"
    28  	"regexp"
    29  	"strconv"
    30  	"strings"
    31  
    32  	"github.com/snapcore/snapd/osutil/sys"
    33  )
    34  
    35  var sudoersTemplate = `
    36  # Created by snap create-user
    37  
    38  # User rules for %[1]s
    39  %[1]s ALL=(ALL) NOPASSWD:ALL
    40  `
    41  
    42  type AddUserOptions struct {
    43  	Sudoer     bool
    44  	ExtraUsers bool
    45  	Gecos      string
    46  	SSHKeys    []string
    47  	// crypt(3) compatible password of the form $id$salt$hash
    48  	Password string
    49  	// force a password change by the user on login
    50  	ForcePasswordChange bool
    51  }
    52  
    53  // we check the (user)name ourselves, adduser is a bit too
    54  // strict (i.e. no `.`) - this regexp is in sync with that SSO
    55  // allows as valid usernames
    56  var IsValidUsername = regexp.MustCompile(`^[a-z0-9][-a-z0-9+._]*$`).MatchString
    57  
    58  // EnsureUserGroup uses the standard shadow utilities' 'useradd' and 'groupadd'
    59  // commands for creating non-login system users and groups that is portable
    60  // cross-distro. It will create the group with groupname 'name' and gid 'id' as
    61  // well as the user with username 'name' and uid 'id'. Importantly, 'useradd'
    62  // and 'groupadd' will use NSS to determine if a uid/gid is already assigned
    63  // (so LDAP, etc are consulted), but will themselves only add to local files,
    64  // which is exactly what we want since we don't want snaps to be blocked on
    65  // LDAP, etc when performing lookups.
    66  func EnsureUserGroup(name string, id uint32, extraUsers bool) error {
    67  	if !IsValidUsername(name) {
    68  		return fmt.Errorf(`cannot add user/group %q: name contains invalid characters`, name)
    69  	}
    70  
    71  	// Perform uid and gid lookups
    72  	uid, uidErr := FindUid(name)
    73  	if uidErr != nil && !IsUnknownUser(uidErr) {
    74  		return uidErr
    75  	}
    76  
    77  	gid, gidErr := FindGid(name)
    78  	if gidErr != nil && !IsUnknownGroup(gidErr) {
    79  		return gidErr
    80  	}
    81  
    82  	if uidErr == nil && gidErr == nil {
    83  		if uid != uint64(id) {
    84  			return fmt.Errorf(`found unexpected uid for user %q: %d`, name, uid)
    85  		} else if gid != uint64(id) {
    86  			return fmt.Errorf(`found unexpected gid for group %q: %d`, name, gid)
    87  		}
    88  		// found the user and group with expected values
    89  		return nil
    90  	}
    91  
    92  	// If the user and group do not exist, snapd will create both, so if
    93  	// the admin removed one of them, error and don't assume we can just
    94  	// add the missing one
    95  	if uidErr != nil && gidErr == nil {
    96  		return fmt.Errorf(`cannot add user/group %q: group exists and user does not`, name)
    97  	} else if uidErr == nil && gidErr != nil {
    98  		return fmt.Errorf(`cannot add user/group %q: user exists and group does not`, name)
    99  	}
   100  
   101  	// At this point, we know that the user and group don't exist, so
   102  	// create them.
   103  
   104  	// First create the group. useradd --user-group will choose a gid from
   105  	// the range defined in login.defs, so first call groupadd and use
   106  	// --gid with useradd.
   107  	groupCmdStr := []string{
   108  		"groupadd",
   109  		"--system",
   110  		"--gid", strconv.FormatUint(uint64(id), 10),
   111  	}
   112  
   113  	if extraUsers {
   114  		groupCmdStr = append(groupCmdStr, "--extrausers")
   115  	}
   116  	groupCmdStr = append(groupCmdStr, name)
   117  
   118  	cmd := exec.Command(groupCmdStr[0], groupCmdStr[1:]...)
   119  	if output, err := cmd.CombinedOutput(); err != nil {
   120  		return fmt.Errorf("groupadd failed with: %s", OutputErr(output, err))
   121  	}
   122  
   123  	// Now call useradd with the group we just created. As a non-login
   124  	// system user, we choose:
   125  	// - no password or aging (use --system without --password)
   126  	// - a non-existent home directory (--home-dir /nonexistent and
   127  	//   --no-create-home)
   128  	// - a non-functional shell (--shell .../nologin)
   129  	// - use the above group (--gid with --no-user-group)
   130  	userCmdStr := []string{
   131  		"useradd",
   132  		"--system",
   133  		"--home-dir", "/nonexistent", "--no-create-home",
   134  		"--shell", LookPathDefault("false", "/bin/false"),
   135  		"--gid", strconv.FormatUint(uint64(id), 10), "--no-user-group",
   136  		"--uid", strconv.FormatUint(uint64(id), 10),
   137  	}
   138  
   139  	if extraUsers {
   140  		userCmdStr = append(userCmdStr, "--extrausers")
   141  	}
   142  	userCmdStr = append(userCmdStr, name)
   143  
   144  	cmd = exec.Command(userCmdStr[0], userCmdStr[1:]...)
   145  	if output, err := cmd.CombinedOutput(); err != nil {
   146  		useraddErrStr := fmt.Sprintf("useradd failed with: %s", OutputErr(output, err))
   147  
   148  		delCmdStr := []string{"groupdel"}
   149  		if extraUsers {
   150  			delCmdStr = append(delCmdStr, "--extrausers")
   151  		}
   152  
   153  		delCmdStr = append(delCmdStr, name)
   154  		cmd = exec.Command(delCmdStr[0], delCmdStr[1:]...)
   155  		if output2, err2 := cmd.CombinedOutput(); err2 != nil {
   156  			groupdelErrStr := OutputErr(output2, err2)
   157  			return fmt.Errorf(`errors encountered ensuring user %s exists:
   158  - %s
   159  - %s`, name, useraddErrStr, groupdelErrStr)
   160  		}
   161  		return fmt.Errorf(useraddErrStr)
   162  	}
   163  
   164  	return nil
   165  }
   166  
   167  func sudoersFile(name string) string {
   168  	// Must escape "." as files containing it are ignored in sudoers.d.
   169  	return filepath.Join(sudoersDotD, "create-user-"+strings.Replace(name, ".", "%2E", -1))
   170  }
   171  
   172  // AddUser uses the Debian/Ubuntu/derivative 'adduser' command for creating
   173  // regular login users on Ubuntu Core. 'adduser' is not portable cross-distro
   174  // but is convenient for creating regular login users.
   175  func AddUser(name string, opts *AddUserOptions) error {
   176  	if opts == nil {
   177  		opts = &AddUserOptions{}
   178  	}
   179  
   180  	if !IsValidUsername(name) {
   181  		return fmt.Errorf("cannot add user %q: name contains invalid characters", name)
   182  	}
   183  
   184  	cmdStr := []string{
   185  		"adduser",
   186  		"--force-badname",
   187  		"--gecos", opts.Gecos,
   188  		"--disabled-password",
   189  	}
   190  	if opts.ExtraUsers {
   191  		cmdStr = append(cmdStr, "--extrausers")
   192  	}
   193  	cmdStr = append(cmdStr, name)
   194  
   195  	cmd := exec.Command(cmdStr[0], cmdStr[1:]...)
   196  	if output, err := cmd.CombinedOutput(); err != nil {
   197  		return fmt.Errorf("adduser failed with: %s", OutputErr(output, err))
   198  	}
   199  
   200  	if opts.Sudoer {
   201  		if err := AtomicWriteFile(sudoersFile(name), []byte(fmt.Sprintf(sudoersTemplate, name)), 0400, 0); err != nil {
   202  			return fmt.Errorf("cannot create file under sudoers.d: %s", err)
   203  		}
   204  	}
   205  
   206  	if opts.Password != "" {
   207  		cmdStr := []string{
   208  			"usermod",
   209  			"--password", opts.Password,
   210  			// no --extrauser required, see LP: #1562872
   211  			name,
   212  		}
   213  		if output, err := exec.Command(cmdStr[0], cmdStr[1:]...).CombinedOutput(); err != nil {
   214  			return fmt.Errorf("setting password failed: %s", OutputErr(output, err))
   215  		}
   216  	}
   217  	if opts.ForcePasswordChange {
   218  		if opts.Password == "" {
   219  			return fmt.Errorf("cannot force password change when no password is provided")
   220  		}
   221  		cmdStr := []string{
   222  			"passwd",
   223  			"--expire",
   224  			// no --extrauser required, see LP: #1562872
   225  			name,
   226  		}
   227  		if output, err := exec.Command(cmdStr[0], cmdStr[1:]...).CombinedOutput(); err != nil {
   228  			return fmt.Errorf("cannot force password change: %s", OutputErr(output, err))
   229  		}
   230  	}
   231  
   232  	u, err := userLookup(name)
   233  	if err != nil {
   234  		return fmt.Errorf("cannot find user %q: %s", name, err)
   235  	}
   236  
   237  	uid, gid, err := UidGid(u)
   238  	if err != nil {
   239  		return err
   240  	}
   241  
   242  	sshDir := filepath.Join(u.HomeDir, ".ssh")
   243  	if err := MkdirAllChown(sshDir, 0700, uid, gid); err != nil {
   244  		return fmt.Errorf("cannot create %s: %s", sshDir, err)
   245  	}
   246  	authKeys := filepath.Join(sshDir, "authorized_keys")
   247  	authKeysContent := strings.Join(opts.SSHKeys, "\n")
   248  	if err := AtomicWriteFileChown(authKeys, []byte(authKeysContent), 0600, 0, uid, gid); err != nil {
   249  		return fmt.Errorf("cannot write %s: %s", authKeys, err)
   250  	}
   251  
   252  	return nil
   253  }
   254  
   255  type DelUserOptions struct {
   256  	ExtraUsers bool
   257  }
   258  
   259  // DelUser removes a "regular login user" from the system, including their
   260  // home. Unlike AddUser, it does this by calling userdel(8) directly
   261  // (deluser doesn't support extrausers).
   262  // Additionally this will remove the user from sudoers if found.
   263  func DelUser(name string, opts *DelUserOptions) error {
   264  	if opts == nil {
   265  		opts = new(DelUserOptions)
   266  	}
   267  	cmdStr := []string{"--remove"}
   268  	if opts.ExtraUsers {
   269  		cmdStr = append(cmdStr, "--extrausers")
   270  	}
   271  	cmdStr = append(cmdStr, name)
   272  
   273  	if output, err := exec.Command("userdel", cmdStr...).CombinedOutput(); err != nil {
   274  		return fmt.Errorf("cannot delete user %q: %v", name, OutputErr(output, err))
   275  	}
   276  
   277  	if err := os.Remove(sudoersFile(name)); err != nil && !os.IsNotExist(err) {
   278  		return fmt.Errorf("cannot remove sudoers file for user %q: %v", name, err)
   279  	}
   280  
   281  	return nil
   282  }
   283  
   284  // UserMaybeSudoUser finds the user behind a sudo invocation when root, if
   285  // applicable and possible. Otherwise the current user is returned.
   286  //
   287  // Don't check SUDO_USER when not root and simply return the current uid
   288  // to properly support sudo'ing from root to a non-root user
   289  func UserMaybeSudoUser() (*user.User, error) {
   290  	cur, err := userCurrent()
   291  	if err != nil {
   292  		return nil, err
   293  	}
   294  
   295  	// not root, so no sudo invocation we care about
   296  	if cur.Uid != "0" {
   297  		return cur, nil
   298  	}
   299  
   300  	realName := os.Getenv("SUDO_USER")
   301  	if realName == "" {
   302  		// not sudo; current is correct
   303  		return cur, nil
   304  	}
   305  
   306  	real, err := user.Lookup(realName)
   307  
   308  	// Note: comparing err here with UnknownUserError is inherently flawed and
   309  	// may end up missing some legitimate unknown user errors, see the comment
   310  	// on findGidNoGetentFallback in group.go for more details.
   311  	// however in this case the effect is not worrisome, because if we fail to
   312  	// identify the error as unknown user, we will just fail here and won't
   313  	// inadvertently raise or lower permissions, as the current user is already
   314  	// root in this codepath
   315  	if _, ok := err.(user.UnknownUserError); ok {
   316  		return cur, nil
   317  	}
   318  	if err != nil {
   319  		return nil, err
   320  	}
   321  
   322  	return real, nil
   323  }
   324  
   325  // UidGid returns the uid and gid of the given user, as uint32s
   326  //
   327  // XXX this should go away soon
   328  func UidGid(u *user.User) (sys.UserID, sys.GroupID, error) {
   329  	// XXX this will be wrong for high uids on 32-bit arches (for now)
   330  	uid, err := strconv.Atoi(u.Uid)
   331  	if err != nil {
   332  		return sys.FlagID, sys.FlagID, fmt.Errorf("cannot parse user id %s: %s", u.Uid, err)
   333  	}
   334  	gid, err := strconv.Atoi(u.Gid)
   335  	if err != nil {
   336  		return sys.FlagID, sys.FlagID, fmt.Errorf("cannot parse group id %s: %s", u.Gid, err)
   337  	}
   338  
   339  	return sys.UserID(uid), sys.GroupID(gid), nil
   340  }