github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/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 && createData.Automatic {
   319  			// no users created but no error with the automatic flag
   320  			return SyncResponse([]userResponseData{}, nil)
   321  		}
   322  		if len(users) > 0 {
   323  			return BadRequest("cannot create user: device already managed")
   324  		}
   325  		if release.OnClassic {
   326  			return BadRequest("cannot create user: device is a classic system")
   327  		}
   328  	}
   329  	if createData.Automatic {
   330  		// Automatic implies known/sudoers
   331  		createData.Known = true
   332  		createData.Sudoer = true
   333  	}
   334  
   335  	var model *asserts.Model
   336  	var serial *asserts.Serial
   337  	createKnown := createData.Known
   338  	if createKnown {
   339  		var err error
   340  		st.Lock()
   341  		model, err = c.d.overlord.DeviceManager().Model()
   342  		st.Unlock()
   343  		if err != nil {
   344  			return InternalError("cannot create user: cannot get model assertion: %v", err)
   345  		}
   346  		st.Lock()
   347  		serial, err = c.d.overlord.DeviceManager().Serial()
   348  		st.Unlock()
   349  		if err != nil && err != state.ErrNoState {
   350  			return InternalError("cannot create user: cannot get serial: %v", err)
   351  		}
   352  	}
   353  
   354  	// special case: the user requested the creation of all known
   355  	// system-users
   356  	if createData.Email == "" && createKnown {
   357  		return createAllKnownSystemUsers(st, model, serial, &createData)
   358  	}
   359  	if createData.Email == "" {
   360  		return BadRequest("cannot create user: 'email' field is empty")
   361  	}
   362  
   363  	var username string
   364  	var opts *osutil.AddUserOptions
   365  	if createKnown {
   366  		username, opts, err = getUserDetailsFromAssertion(st, model, serial, createData.Email)
   367  	} else {
   368  		username, opts, err = getUserDetailsFromStore(getStore(c), createData.Email)
   369  	}
   370  	if err != nil {
   371  		return BadRequest("%s", err)
   372  	}
   373  
   374  	// FIXME: duplicated code
   375  	opts.Sudoer = createData.Sudoer
   376  	opts.ExtraUsers = !release.OnClassic
   377  
   378  	if err := osutilAddUser(username, opts); err != nil {
   379  		return BadRequest("cannot create user %s: %s", username, err)
   380  	}
   381  
   382  	if err := setupLocalUser(c.d.overlord.State(), username, createData.Email); err != nil {
   383  		return InternalError("%s", err)
   384  	}
   385  
   386  	result := userResponseData{
   387  		Username: username,
   388  		SSHKeys:  opts.SSHKeys,
   389  	}
   390  
   391  	if createData.singleUserResultCompat {
   392  		// return a single userResponseData in this case
   393  		return SyncResponse(&result, nil)
   394  	} else {
   395  		return SyncResponse([]userResponseData{result}, nil)
   396  	}
   397  }
   398  
   399  func getUserDetailsFromStore(theStore snapstate.StoreService, email string) (string, *osutil.AddUserOptions, error) {
   400  	v, err := theStore.UserInfo(email)
   401  	if err != nil {
   402  		return "", nil, fmt.Errorf("cannot create user %q: %s", email, err)
   403  	}
   404  	if len(v.SSHKeys) == 0 {
   405  		return "", nil, fmt.Errorf("cannot create user for %q: no ssh keys found", email)
   406  	}
   407  
   408  	gecos := fmt.Sprintf("%s,%s", email, v.OpenIDIdentifier)
   409  	opts := &osutil.AddUserOptions{
   410  		SSHKeys: v.SSHKeys,
   411  		Gecos:   gecos,
   412  	}
   413  	return v.Username, opts, nil
   414  }
   415  
   416  func createAllKnownSystemUsers(st *state.State, modelAs *asserts.Model, serialAs *asserts.Serial, createData *postUserCreateData) Response {
   417  	var createdUsers []userResponseData
   418  	headers := map[string]string{
   419  		"brand-id": modelAs.BrandID(),
   420  	}
   421  
   422  	st.Lock()
   423  	db := assertstate.DB(st)
   424  	assertions, err := db.FindMany(asserts.SystemUserType, headers)
   425  	st.Unlock()
   426  	if err != nil && !asserts.IsNotFound(err) {
   427  		return BadRequest("cannot find system-user assertion: %s", err)
   428  	}
   429  
   430  	for _, as := range assertions {
   431  		email := as.(*asserts.SystemUser).Email()
   432  		// we need to use getUserDetailsFromAssertion as this verifies
   433  		// the assertion against the current brand/model/time
   434  		username, opts, err := getUserDetailsFromAssertion(st, modelAs, serialAs, email)
   435  		if err != nil {
   436  			logger.Noticef("ignoring system-user assertion for %q: %s", email, err)
   437  			continue
   438  		}
   439  		// ignore already existing users
   440  		if _, err := userLookup(username); err == nil {
   441  			continue
   442  		}
   443  
   444  		// FIXME: duplicated code
   445  		opts.Sudoer = createData.Sudoer
   446  		opts.ExtraUsers = !release.OnClassic
   447  
   448  		if err := osutilAddUser(username, opts); err != nil {
   449  			return InternalError("cannot add user %q: %s", username, err)
   450  		}
   451  		if err := setupLocalUser(st, username, email); err != nil {
   452  			return InternalError("%s", err)
   453  		}
   454  		createdUsers = append(createdUsers, userResponseData{
   455  			Username: username,
   456  			SSHKeys:  opts.SSHKeys,
   457  		})
   458  	}
   459  
   460  	return SyncResponse(createdUsers, nil)
   461  }
   462  
   463  func getUserDetailsFromAssertion(st *state.State, modelAs *asserts.Model, serialAs *asserts.Serial, email string) (string, *osutil.AddUserOptions, error) {
   464  	errorPrefix := fmt.Sprintf("cannot add system-user %q: ", email)
   465  
   466  	st.Lock()
   467  	db := assertstate.DB(st)
   468  	st.Unlock()
   469  
   470  	brandID := modelAs.BrandID()
   471  	series := modelAs.Series()
   472  	model := modelAs.Model()
   473  
   474  	a, err := db.Find(asserts.SystemUserType, map[string]string{
   475  		"brand-id": brandID,
   476  		"email":    email,
   477  	})
   478  	if err != nil {
   479  		return "", nil, fmt.Errorf(errorPrefix+"%v", err)
   480  	}
   481  	// the asserts package guarantees that this cast will work
   482  	su := a.(*asserts.SystemUser)
   483  
   484  	// cross check that the assertion is valid for the given series/model
   485  
   486  	// check that the signer of the assertion is one of the accepted ones
   487  	sysUserAuths := modelAs.SystemUserAuthority()
   488  	if len(sysUserAuths) > 0 && !strutil.ListContains(sysUserAuths, su.AuthorityID()) {
   489  		return "", nil, fmt.Errorf(errorPrefix+"%q not in accepted authorities %q", email, su.AuthorityID(), sysUserAuths)
   490  	}
   491  	if len(su.Series()) > 0 && !strutil.ListContains(su.Series(), series) {
   492  		return "", nil, fmt.Errorf(errorPrefix+"%q not in series %q", email, series, su.Series())
   493  	}
   494  	if len(su.Models()) > 0 && !strutil.ListContains(su.Models(), model) {
   495  		return "", nil, fmt.Errorf(errorPrefix+"%q not in models %q", model, su.Models())
   496  	}
   497  	if len(su.Serials()) > 0 {
   498  		if serialAs == nil {
   499  			return "", nil, fmt.Errorf(errorPrefix + "bound to serial assertion but device not yet registered")
   500  		}
   501  		serial := serialAs.Serial()
   502  		if !strutil.ListContains(su.Serials(), serial) {
   503  			return "", nil, fmt.Errorf(errorPrefix+"%q not in serials %q", serial, su.Serials())
   504  		}
   505  	}
   506  
   507  	if !su.ValidAt(time.Now()) {
   508  		return "", nil, fmt.Errorf(errorPrefix + "assertion not valid anymore")
   509  	}
   510  
   511  	gecos := fmt.Sprintf("%s,%s", email, su.Name())
   512  	opts := &osutil.AddUserOptions{
   513  		SSHKeys:             su.SSHKeys(),
   514  		Gecos:               gecos,
   515  		Password:            su.Password(),
   516  		ForcePasswordChange: su.ForcePasswordChange(),
   517  	}
   518  	return su.Username(), opts, nil
   519  }
   520  
   521  type postUserData struct {
   522  	Action   string `json:"action"`
   523  	Username string `json:"username"`
   524  	postUserCreateData
   525  	postUserDeleteData
   526  }
   527  
   528  type postUserCreateData struct {
   529  	Email        string `json:"email"`
   530  	Sudoer       bool   `json:"sudoer"`
   531  	Known        bool   `json:"known"`
   532  	ForceManaged bool   `json:"force-managed"`
   533  	Automatic    bool   `json:"automatic"`
   534  
   535  	// singleUserResultCompat indicates whether to preserve
   536  	// backwards compatibility, which results in more clunky
   537  	// return values (userResponseData OR [userResponseData] vs now
   538  	// uniform [userResponseData]); internal, not from JSON.
   539  	singleUserResultCompat bool
   540  }
   541  
   542  type postUserDeleteData struct{}
   543  
   544  var userLookup = user.Lookup
   545  
   546  func setupLocalUser(st *state.State, username, email string) error {
   547  	user, err := userLookup(username)
   548  	if err != nil {
   549  		return fmt.Errorf("cannot lookup user %q: %s", username, err)
   550  	}
   551  	uid, gid, err := osutil.UidGid(user)
   552  	if err != nil {
   553  		return err
   554  	}
   555  	authDataFn := filepath.Join(user.HomeDir, ".snap", "auth.json")
   556  	if err := osutil.MkdirAllChown(filepath.Dir(authDataFn), 0700, uid, gid); err != nil {
   557  		return err
   558  	}
   559  
   560  	// setup new user, local-only
   561  	st.Lock()
   562  	authUser, err := auth.NewUser(st, username, email, "", nil)
   563  	st.Unlock()
   564  	if err != nil {
   565  		return fmt.Errorf("cannot persist authentication details: %v", err)
   566  	}
   567  	// store macaroon auth, user's ID, email and username in auth.json in
   568  	// the new users home dir
   569  	outStr, err := json.Marshal(struct {
   570  		ID       int    `json:"id"`
   571  		Username string `json:"username"`
   572  		Email    string `json:"email"`
   573  		Macaroon string `json:"macaroon"`
   574  	}{
   575  		ID:       authUser.ID,
   576  		Username: authUser.Username,
   577  		Email:    authUser.Email,
   578  		Macaroon: authUser.Macaroon,
   579  	})
   580  	if err != nil {
   581  		return fmt.Errorf("cannot marshal auth data: %s", err)
   582  	}
   583  	if err := osutil.AtomicWriteFileChown(authDataFn, []byte(outStr), 0600, 0, uid, gid); err != nil {
   584  		return fmt.Errorf("cannot write auth file %q: %s", authDataFn, err)
   585  	}
   586  
   587  	return nil
   588  }
   589  
   590  func getUsers(c *Command, r *http.Request, user *auth.UserState) Response {
   591  	st := c.d.overlord.State()
   592  	st.Lock()
   593  	users, err := auth.Users(st)
   594  	st.Unlock()
   595  	if err != nil {
   596  		return InternalError("cannot get users: %s", err)
   597  	}
   598  
   599  	resp := make([]userResponseData, len(users))
   600  	for i, u := range users {
   601  		resp[i] = userResponseData{
   602  			Username: u.Username,
   603  			Email:    u.Email,
   604  			ID:       u.ID,
   605  		}
   606  	}
   607  	return SyncResponse(resp, nil)
   608  }