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