github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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 }