github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/daemon/api_users.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2015-2019 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 daemon 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "net/http" 26 "os/user" 27 "path/filepath" 28 "regexp" 29 "time" 30 31 "github.com/snapcore/snapd/asserts" 32 "github.com/snapcore/snapd/client" 33 "github.com/snapcore/snapd/logger" 34 "github.com/snapcore/snapd/osutil" 35 "github.com/snapcore/snapd/overlord/assertstate" 36 "github.com/snapcore/snapd/overlord/auth" 37 "github.com/snapcore/snapd/overlord/snapstate" 38 "github.com/snapcore/snapd/overlord/state" 39 "github.com/snapcore/snapd/release" 40 "github.com/snapcore/snapd/store" 41 "github.com/snapcore/snapd/strutil" 42 ) 43 44 var ( 45 loginCmd = &Command{ 46 Path: "/v2/login", 47 POST: loginUser, 48 PolkitOK: "io.snapcraft.snapd.login", 49 } 50 51 logoutCmd = &Command{ 52 Path: "/v2/logout", 53 POST: logoutUser, 54 PolkitOK: "io.snapcraft.snapd.login", 55 } 56 57 // backwards compat; to-be-deprecated 58 createUserCmd = &Command{ 59 Path: "/v2/create-user", 60 POST: postCreateUser, 61 RootOnly: true, 62 } 63 64 usersCmd = &Command{ 65 Path: "/v2/users", 66 GET: getUsers, 67 POST: postUsers, 68 RootOnly: true, 69 } 70 ) 71 72 var ( 73 osutilAddUser = osutil.AddUser 74 osutilDelUser = osutil.DelUser 75 ) 76 77 // userResponseData contains the data releated to user creation/login/query 78 type userResponseData struct { 79 ID int `json:"id,omitempty"` 80 Username string `json:"username,omitempty"` 81 Email string `json:"email,omitempty"` 82 SSHKeys []string `json:"ssh-keys,omitempty"` 83 84 Macaroon string `json:"macaroon,omitempty"` 85 Discharges []string `json:"discharges,omitempty"` 86 } 87 88 var isEmailish = regexp.MustCompile(`.@.*\..`).MatchString 89 90 func loginUser(c *Command, r *http.Request, user *auth.UserState) Response { 91 var loginData struct { 92 Username string `json:"username"` 93 Email string `json:"email"` 94 Password string `json:"password"` 95 Otp string `json:"otp"` 96 } 97 98 decoder := json.NewDecoder(r.Body) 99 if err := decoder.Decode(&loginData); err != nil { 100 return BadRequest("cannot decode login data from request body: %v", err) 101 } 102 103 if loginData.Email == "" && isEmailish(loginData.Username) { 104 // for backwards compatibility, if no email is provided assume username is the email 105 loginData.Email = loginData.Username 106 loginData.Username = "" 107 } 108 109 if loginData.Email == "" && user != nil && user.Email != "" { 110 loginData.Email = user.Email 111 } 112 113 // the "username" needs to look a lot like an email address 114 if !isEmailish(loginData.Email) { 115 return SyncResponse(&resp{ 116 Type: ResponseTypeError, 117 Result: &errorResult{ 118 Message: "please use a valid email address.", 119 Kind: client.ErrorKindInvalidAuthData, 120 Value: map[string][]string{"email": {"invalid"}}, 121 }, 122 Status: 400, 123 }, nil) 124 } 125 126 overlord := c.d.overlord 127 st := overlord.State() 128 theStore := getStore(c) 129 macaroon, discharge, err := theStore.LoginUser(loginData.Email, loginData.Password, loginData.Otp) 130 switch err { 131 case store.ErrAuthenticationNeeds2fa: 132 return SyncResponse(&resp{ 133 Type: ResponseTypeError, 134 Result: &errorResult{ 135 Kind: client.ErrorKindTwoFactorRequired, 136 Message: err.Error(), 137 }, 138 Status: 401, 139 }, nil) 140 case store.Err2faFailed: 141 return SyncResponse(&resp{ 142 Type: ResponseTypeError, 143 Result: &errorResult{ 144 Kind: client.ErrorKindTwoFactorFailed, 145 Message: err.Error(), 146 }, 147 Status: 401, 148 }, nil) 149 default: 150 switch err := err.(type) { 151 case store.InvalidAuthDataError: 152 return SyncResponse(&resp{ 153 Type: ResponseTypeError, 154 Result: &errorResult{ 155 Message: err.Error(), 156 Kind: client.ErrorKindInvalidAuthData, 157 Value: err, 158 }, 159 Status: 400, 160 }, nil) 161 case store.PasswordPolicyError: 162 return SyncResponse(&resp{ 163 Type: ResponseTypeError, 164 Result: &errorResult{ 165 Message: err.Error(), 166 Kind: client.ErrorKindPasswordPolicy, 167 Value: err, 168 }, 169 Status: 401, 170 }, nil) 171 } 172 return Unauthorized(err.Error()) 173 case nil: 174 // continue 175 } 176 st.Lock() 177 if user != nil { 178 // local user logged-in, set its store macaroons 179 user.StoreMacaroon = macaroon 180 user.StoreDischarges = []string{discharge} 181 // user's email address authenticated by the store 182 user.Email = loginData.Email 183 err = auth.UpdateUser(st, user) 184 } else { 185 user, err = auth.NewUser(st, loginData.Username, loginData.Email, macaroon, []string{discharge}) 186 } 187 st.Unlock() 188 if err != nil { 189 return InternalError("cannot persist authentication details: %v", err) 190 } 191 192 result := userResponseData{ 193 ID: user.ID, 194 Username: user.Username, 195 Email: user.Email, 196 Macaroon: user.Macaroon, 197 Discharges: user.Discharges, 198 } 199 return SyncResponse(result, nil) 200 } 201 202 func logoutUser(c *Command, r *http.Request, user *auth.UserState) Response { 203 state := c.d.overlord.State() 204 state.Lock() 205 defer state.Unlock() 206 207 if user == nil { 208 return BadRequest("not logged in") 209 } 210 _, err := auth.RemoveUser(state, user.ID) 211 if err != nil { 212 return InternalError(err.Error()) 213 } 214 215 return SyncResponse(nil, nil) 216 } 217 218 // this might need to become a function, if having user admin becomes a config option 219 var hasUserAdmin = !release.OnClassic 220 221 const noUserAdmin = "system user administration via snapd is not allowed on this system" 222 223 func postUsers(c *Command, r *http.Request, user *auth.UserState) Response { 224 if !hasUserAdmin { 225 return MethodNotAllowed(noUserAdmin) 226 } 227 228 var postData postUserData 229 230 decoder := json.NewDecoder(r.Body) 231 if err := decoder.Decode(&postData); err != nil { 232 return BadRequest("cannot decode user action data from request body: %v", err) 233 } 234 if decoder.More() { 235 return BadRequest("spurious content after user action") 236 } 237 switch postData.Action { 238 case "create": 239 return createUser(c, postData.postUserCreateData) 240 case "remove": 241 return removeUser(c, postData.Username, postData.postUserDeleteData) 242 case "": 243 return BadRequest("missing user action") 244 } 245 return BadRequest("unsupported user action %q", postData.Action) 246 } 247 248 func removeUser(c *Command, username string, opts postUserDeleteData) Response { 249 // TODO: allow to remove user entries by email as well 250 251 // catch silly errors 252 if username == "" { 253 return BadRequest("need a username to remove") 254 } 255 // check the user is known to snapd 256 st := c.d.overlord.State() 257 st.Lock() 258 _, err := auth.UserByUsername(st, username) 259 st.Unlock() 260 if err == auth.ErrInvalidUser { 261 return BadRequest("user %q is not known", username) 262 } 263 if err != nil { 264 return InternalError(err.Error()) 265 } 266 267 // first remove the system user 268 if err := osutilDelUser(username, &osutil.DelUserOptions{ExtraUsers: !release.OnClassic}); err != nil { 269 return InternalError(err.Error()) 270 } 271 272 // then the UserState 273 st.Lock() 274 u, err := auth.RemoveUserByUsername(st, username) 275 st.Unlock() 276 // ErrInvalidUser means "not found" in this case 277 if err != nil && err != auth.ErrInvalidUser { 278 return InternalError(err.Error()) 279 } 280 281 result := map[string]interface{}{ 282 "removed": []userResponseData{ 283 {ID: u.ID, Username: u.Username, Email: u.Email}, 284 }, 285 } 286 return SyncResponse(result, nil) 287 } 288 289 func postCreateUser(c *Command, r *http.Request, user *auth.UserState) Response { 290 if !hasUserAdmin { 291 return Forbidden(noUserAdmin) 292 } 293 var createData postUserCreateData 294 295 decoder := json.NewDecoder(r.Body) 296 if err := decoder.Decode(&createData); err != nil { 297 return BadRequest("cannot decode create-user data from request body: %v", err) 298 } 299 300 // this is /v2/create-user, meaning we want the 301 // backwards-compatible wackiness 302 createData.singleUserResultCompat = true 303 304 return createUser(c, createData) 305 } 306 307 func createUser(c *Command, createData postUserCreateData) Response { 308 // verify request 309 st := c.d.overlord.State() 310 st.Lock() 311 users, err := auth.Users(st) 312 st.Unlock() 313 if err != nil { 314 return InternalError("cannot get user count: %s", err) 315 } 316 317 if !createData.ForceManaged { 318 if len(users) > 0 { 319 return BadRequest("cannot create user: device already managed") 320 } 321 if release.OnClassic { 322 return BadRequest("cannot create user: device is a classic system") 323 } 324 } 325 326 var model *asserts.Model 327 var serial *asserts.Serial 328 createKnown := createData.Known 329 if createKnown { 330 var err error 331 st.Lock() 332 model, err = c.d.overlord.DeviceManager().Model() 333 st.Unlock() 334 if err != nil { 335 return InternalError("cannot create user: cannot get model assertion: %v", err) 336 } 337 st.Lock() 338 serial, err = c.d.overlord.DeviceManager().Serial() 339 st.Unlock() 340 if err != nil && err != state.ErrNoState { 341 return InternalError("cannot create user: cannot get serial: %v", err) 342 } 343 } 344 345 // special case: the user requested the creation of all known 346 // system-users 347 if createData.Email == "" && createKnown { 348 return createAllKnownSystemUsers(st, model, serial, &createData) 349 } 350 if createData.Email == "" { 351 return BadRequest("cannot create user: 'email' field is empty") 352 } 353 354 var username string 355 var opts *osutil.AddUserOptions 356 if createKnown { 357 username, opts, err = getUserDetailsFromAssertion(st, model, serial, createData.Email) 358 } else { 359 username, opts, err = getUserDetailsFromStore(getStore(c), createData.Email) 360 } 361 if err != nil { 362 return BadRequest("%s", err) 363 } 364 365 // FIXME: duplicated code 366 opts.Sudoer = createData.Sudoer 367 opts.ExtraUsers = !release.OnClassic 368 369 if err := osutilAddUser(username, opts); err != nil { 370 return BadRequest("cannot create user %s: %s", username, err) 371 } 372 373 if err := setupLocalUser(c.d.overlord.State(), username, createData.Email); err != nil { 374 return InternalError("%s", err) 375 } 376 377 result := userResponseData{ 378 Username: username, 379 SSHKeys: opts.SSHKeys, 380 } 381 382 if createData.singleUserResultCompat { 383 // return a single userResponseData in this case 384 return SyncResponse(&result, nil) 385 } else { 386 return SyncResponse([]userResponseData{result}, nil) 387 } 388 } 389 390 func getUserDetailsFromStore(theStore snapstate.StoreService, email string) (string, *osutil.AddUserOptions, error) { 391 v, err := theStore.UserInfo(email) 392 if err != nil { 393 return "", nil, fmt.Errorf("cannot create user %q: %s", email, err) 394 } 395 if len(v.SSHKeys) == 0 { 396 return "", nil, fmt.Errorf("cannot create user for %q: no ssh keys found", email) 397 } 398 399 gecos := fmt.Sprintf("%s,%s", email, v.OpenIDIdentifier) 400 opts := &osutil.AddUserOptions{ 401 SSHKeys: v.SSHKeys, 402 Gecos: gecos, 403 } 404 return v.Username, opts, nil 405 } 406 407 func createAllKnownSystemUsers(st *state.State, modelAs *asserts.Model, serialAs *asserts.Serial, createData *postUserCreateData) Response { 408 var createdUsers []userResponseData 409 headers := map[string]string{ 410 "brand-id": modelAs.BrandID(), 411 } 412 413 st.Lock() 414 db := assertstate.DB(st) 415 assertions, err := db.FindMany(asserts.SystemUserType, headers) 416 st.Unlock() 417 if err != nil && !asserts.IsNotFound(err) { 418 return BadRequest("cannot find system-user assertion: %s", err) 419 } 420 421 for _, as := range assertions { 422 email := as.(*asserts.SystemUser).Email() 423 // we need to use getUserDetailsFromAssertion as this verifies 424 // the assertion against the current brand/model/time 425 username, opts, err := getUserDetailsFromAssertion(st, modelAs, serialAs, email) 426 if err != nil { 427 logger.Noticef("ignoring system-user assertion for %q: %s", email, err) 428 continue 429 } 430 // ignore already existing users 431 if _, err := userLookup(username); err == nil { 432 continue 433 } 434 435 // FIXME: duplicated code 436 opts.Sudoer = createData.Sudoer 437 opts.ExtraUsers = !release.OnClassic 438 439 if err := osutilAddUser(username, opts); err != nil { 440 return InternalError("cannot add user %q: %s", username, err) 441 } 442 if err := setupLocalUser(st, username, email); err != nil { 443 return InternalError("%s", err) 444 } 445 createdUsers = append(createdUsers, userResponseData{ 446 Username: username, 447 SSHKeys: opts.SSHKeys, 448 }) 449 } 450 451 return SyncResponse(createdUsers, nil) 452 } 453 454 func getUserDetailsFromAssertion(st *state.State, modelAs *asserts.Model, serialAs *asserts.Serial, email string) (string, *osutil.AddUserOptions, error) { 455 errorPrefix := fmt.Sprintf("cannot add system-user %q: ", email) 456 457 st.Lock() 458 db := assertstate.DB(st) 459 st.Unlock() 460 461 brandID := modelAs.BrandID() 462 series := modelAs.Series() 463 model := modelAs.Model() 464 465 a, err := db.Find(asserts.SystemUserType, map[string]string{ 466 "brand-id": brandID, 467 "email": email, 468 }) 469 if err != nil { 470 return "", nil, fmt.Errorf(errorPrefix+"%v", err) 471 } 472 // the asserts package guarantees that this cast will work 473 su := a.(*asserts.SystemUser) 474 475 // cross check that the assertion is valid for the given series/model 476 477 // check that the signer of the assertion is one of the accepted ones 478 sysUserAuths := modelAs.SystemUserAuthority() 479 if len(sysUserAuths) > 0 && !strutil.ListContains(sysUserAuths, su.AuthorityID()) { 480 return "", nil, fmt.Errorf(errorPrefix+"%q not in accepted authorities %q", email, su.AuthorityID(), sysUserAuths) 481 } 482 if len(su.Series()) > 0 && !strutil.ListContains(su.Series(), series) { 483 return "", nil, fmt.Errorf(errorPrefix+"%q not in series %q", email, series, su.Series()) 484 } 485 if len(su.Models()) > 0 && !strutil.ListContains(su.Models(), model) { 486 return "", nil, fmt.Errorf(errorPrefix+"%q not in models %q", model, su.Models()) 487 } 488 if len(su.Serials()) > 0 { 489 if serialAs == nil { 490 return "", nil, fmt.Errorf(errorPrefix + "bound to serial assertion but device not yet registered") 491 } 492 serial := serialAs.Serial() 493 if !strutil.ListContains(su.Serials(), serial) { 494 return "", nil, fmt.Errorf(errorPrefix+"%q not in serials %q", serial, su.Serials()) 495 } 496 } 497 498 if !su.ValidAt(time.Now()) { 499 return "", nil, fmt.Errorf(errorPrefix + "assertion not valid anymore") 500 } 501 502 gecos := fmt.Sprintf("%s,%s", email, su.Name()) 503 opts := &osutil.AddUserOptions{ 504 SSHKeys: su.SSHKeys(), 505 Gecos: gecos, 506 Password: su.Password(), 507 ForcePasswordChange: su.ForcePasswordChange(), 508 } 509 return su.Username(), opts, nil 510 } 511 512 type postUserData struct { 513 Action string `json:"action"` 514 Username string `json:"username"` 515 postUserCreateData 516 postUserDeleteData 517 } 518 519 type postUserCreateData struct { 520 Email string `json:"email"` 521 Sudoer bool `json:"sudoer"` 522 Known bool `json:"known"` 523 ForceManaged bool `json:"force-managed"` 524 525 // singleUserResultCompat indicates whether to preserve 526 // backwards compatibility, which results in more clunky 527 // return values (userResponseData OR [userResponseData] vs now 528 // uniform [userResponseData]); internal, not from JSON. 529 singleUserResultCompat bool 530 } 531 532 type postUserDeleteData struct{} 533 534 var userLookup = user.Lookup 535 536 func setupLocalUser(st *state.State, username, email string) error { 537 user, err := userLookup(username) 538 if err != nil { 539 return fmt.Errorf("cannot lookup user %q: %s", username, err) 540 } 541 uid, gid, err := osutil.UidGid(user) 542 if err != nil { 543 return err 544 } 545 authDataFn := filepath.Join(user.HomeDir, ".snap", "auth.json") 546 if err := osutil.MkdirAllChown(filepath.Dir(authDataFn), 0700, uid, gid); err != nil { 547 return err 548 } 549 550 // setup new user, local-only 551 st.Lock() 552 authUser, err := auth.NewUser(st, username, email, "", nil) 553 st.Unlock() 554 if err != nil { 555 return fmt.Errorf("cannot persist authentication details: %v", err) 556 } 557 // store macaroon auth, user's ID, email and username in auth.json in 558 // the new users home dir 559 outStr, err := json.Marshal(struct { 560 ID int `json:"id"` 561 Username string `json:"username"` 562 Email string `json:"email"` 563 Macaroon string `json:"macaroon"` 564 }{ 565 ID: authUser.ID, 566 Username: authUser.Username, 567 Email: authUser.Email, 568 Macaroon: authUser.Macaroon, 569 }) 570 if err != nil { 571 return fmt.Errorf("cannot marshal auth data: %s", err) 572 } 573 if err := osutil.AtomicWriteFileChown(authDataFn, []byte(outStr), 0600, 0, uid, gid); err != nil { 574 return fmt.Errorf("cannot write auth file %q: %s", authDataFn, err) 575 } 576 577 return nil 578 } 579 580 func getUsers(c *Command, r *http.Request, user *auth.UserState) Response { 581 st := c.d.overlord.State() 582 st.Lock() 583 users, err := auth.Users(st) 584 st.Unlock() 585 if err != nil { 586 return InternalError("cannot get users: %s", err) 587 } 588 589 resp := make([]userResponseData, len(users)) 590 for i, u := range users { 591 resp[i] = userResponseData{ 592 Username: u.Username, 593 Email: u.Email, 594 ID: u.ID, 595 } 596 } 597 return SyncResponse(resp, nil) 598 }