github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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  		// TODO: groupdel doesn't currently support --extrausers, so
   154  		// don't try to clean up when it is specified (LP: #1840375)
   155  		if !extraUsers {
   156  			delCmdStr = append(delCmdStr, name)
   157  			cmd = exec.Command(delCmdStr[0], delCmdStr[1:]...)
   158  			if output2, err2 := cmd.CombinedOutput(); err2 != nil {
   159  				return fmt.Errorf("groupdel failed with: %s (after %s)", OutputErr(output2, err2), useraddErrStr)
   160  			}
   161  		}
   162  		return fmt.Errorf(useraddErrStr)
   163  	}
   164  
   165  	return nil
   166  }
   167  
   168  func sudoersFile(name string) string {
   169  	// Must escape "." as files containing it are ignored in sudoers.d.
   170  	return filepath.Join(sudoersDotD, "create-user-"+strings.Replace(name, ".", "%2E", -1))
   171  }
   172  
   173  // AddUser uses the Debian/Ubuntu/derivative 'adduser' command for creating
   174  // regular login users on Ubuntu Core. 'adduser' is not portable cross-distro
   175  // but is convenient for creating regular login users.
   176  func AddUser(name string, opts *AddUserOptions) error {
   177  	if opts == nil {
   178  		opts = &AddUserOptions{}
   179  	}
   180  
   181  	if !IsValidUsername(name) {
   182  		return fmt.Errorf("cannot add user %q: name contains invalid characters", name)
   183  	}
   184  
   185  	cmdStr := []string{
   186  		"adduser",
   187  		"--force-badname",
   188  		"--gecos", opts.Gecos,
   189  		"--disabled-password",
   190  	}
   191  	if opts.ExtraUsers {
   192  		cmdStr = append(cmdStr, "--extrausers")
   193  	}
   194  	cmdStr = append(cmdStr, name)
   195  
   196  	cmd := exec.Command(cmdStr[0], cmdStr[1:]...)
   197  	if output, err := cmd.CombinedOutput(); err != nil {
   198  		return fmt.Errorf("adduser failed with: %s", OutputErr(output, err))
   199  	}
   200  
   201  	if opts.Sudoer {
   202  		if err := AtomicWriteFile(sudoersFile(name), []byte(fmt.Sprintf(sudoersTemplate, name)), 0400, 0); err != nil {
   203  			return fmt.Errorf("cannot create file under sudoers.d: %s", err)
   204  		}
   205  	}
   206  
   207  	if opts.Password != "" {
   208  		cmdStr := []string{
   209  			"usermod",
   210  			"--password", opts.Password,
   211  			// no --extrauser required, see LP: #1562872
   212  			name,
   213  		}
   214  		if output, err := exec.Command(cmdStr[0], cmdStr[1:]...).CombinedOutput(); err != nil {
   215  			return fmt.Errorf("setting password failed: %s", OutputErr(output, err))
   216  		}
   217  	}
   218  	if opts.ForcePasswordChange {
   219  		if opts.Password == "" {
   220  			return fmt.Errorf("cannot force password change when no password is provided")
   221  		}
   222  		cmdStr := []string{
   223  			"passwd",
   224  			"--expire",
   225  			// no --extrauser required, see LP: #1562872
   226  			name,
   227  		}
   228  		if output, err := exec.Command(cmdStr[0], cmdStr[1:]...).CombinedOutput(); err != nil {
   229  			return fmt.Errorf("cannot force password change: %s", OutputErr(output, err))
   230  		}
   231  	}
   232  
   233  	u, err := userLookup(name)
   234  	if err != nil {
   235  		return fmt.Errorf("cannot find user %q: %s", name, err)
   236  	}
   237  
   238  	uid, gid, err := UidGid(u)
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	sshDir := filepath.Join(u.HomeDir, ".ssh")
   244  	if err := MkdirAllChown(sshDir, 0700, uid, gid); err != nil {
   245  		return fmt.Errorf("cannot create %s: %s", sshDir, err)
   246  	}
   247  	authKeys := filepath.Join(sshDir, "authorized_keys")
   248  	authKeysContent := strings.Join(opts.SSHKeys, "\n")
   249  	if err := AtomicWriteFileChown(authKeys, []byte(authKeysContent), 0600, 0, uid, gid); err != nil {
   250  		return fmt.Errorf("cannot write %s: %s", authKeys, err)
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  type DelUserOptions struct {
   257  	ExtraUsers bool
   258  }
   259  
   260  // DelUser removes a "regular login user" from the system, including their
   261  // home. Unlike AddUser, it does this by calling userdel(8) directly
   262  // (deluser doesn't support extrausers).
   263  // Additionally this will remove the user from sudoers if found.
   264  func DelUser(name string, opts *DelUserOptions) error {
   265  	if opts == nil {
   266  		opts = new(DelUserOptions)
   267  	}
   268  	cmdStr := []string{"--remove"}
   269  	if opts.ExtraUsers {
   270  		cmdStr = append(cmdStr, "--extrausers")
   271  	}
   272  	cmdStr = append(cmdStr, name)
   273  
   274  	if output, err := exec.Command("userdel", cmdStr...).CombinedOutput(); err != nil {
   275  		return fmt.Errorf("cannot delete user %q: %v", name, OutputErr(output, err))
   276  	}
   277  
   278  	if err := os.Remove(sudoersFile(name)); err != nil && !os.IsNotExist(err) {
   279  		return fmt.Errorf("cannot remove sudoers file for user %q: %v", name, err)
   280  	}
   281  
   282  	return nil
   283  }
   284  
   285  // RealUser finds the user behind a sudo invocation when root, if applicable
   286  // and possible.
   287  //
   288  // Don't check SUDO_USER when not root and simply return the current uid
   289  // to properly support sudo'ing from root to a non-root user
   290  func RealUser() (*user.User, error) {
   291  	cur, err := userCurrent()
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  
   296  	// not root, so no sudo invocation we care about
   297  	if cur.Uid != "0" {
   298  		return cur, nil
   299  	}
   300  
   301  	realName := os.Getenv("SUDO_USER")
   302  	if realName == "" {
   303  		// not sudo; current is correct
   304  		return cur, nil
   305  	}
   306  
   307  	real, err := user.Lookup(realName)
   308  	// can happen when sudo is used to enter a chroot (e.g. pbuilder)
   309  	if _, ok := err.(user.UnknownUserError); ok {
   310  		return cur, nil
   311  	}
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  
   316  	return real, nil
   317  }
   318  
   319  // UidGid returns the uid and gid of the given user, as uint32s
   320  //
   321  // XXX this should go away soon
   322  func UidGid(u *user.User) (sys.UserID, sys.GroupID, error) {
   323  	// XXX this will be wrong for high uids on 32-bit arches (for now)
   324  	uid, err := strconv.Atoi(u.Uid)
   325  	if err != nil {
   326  		return sys.FlagID, sys.FlagID, fmt.Errorf("cannot parse user id %s: %s", u.Uid, err)
   327  	}
   328  	gid, err := strconv.Atoi(u.Gid)
   329  	if err != nil {
   330  		return sys.FlagID, sys.FlagID, fmt.Errorf("cannot parse group id %s: %s", u.Gid, err)
   331  	}
   332  
   333  	return sys.UserID(uid), sys.GroupID(gid), nil
   334  }