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  }