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 }