mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
1113 lines
35 KiB
Go
1113 lines
35 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2014-2021 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package daemon_test
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"os/user"
|
|
"time"
|
|
|
|
"gopkg.in/check.v1"
|
|
|
|
"github.com/snapcore/snapd/asserts"
|
|
"github.com/snapcore/snapd/asserts/assertstest"
|
|
"github.com/snapcore/snapd/client"
|
|
"github.com/snapcore/snapd/daemon"
|
|
"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
|
|
"github.com/snapcore/snapd/overlord/auth"
|
|
"github.com/snapcore/snapd/overlord/configstate/config"
|
|
"github.com/snapcore/snapd/overlord/devicestate"
|
|
"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
|
|
"github.com/snapcore/snapd/overlord/state"
|
|
"github.com/snapcore/snapd/release"
|
|
"github.com/snapcore/snapd/store"
|
|
"github.com/snapcore/snapd/testutil"
|
|
)
|
|
|
|
var _ = check.Suite(&userSuite{})
|
|
|
|
type userSuite struct {
|
|
apiBaseSuite
|
|
|
|
loginUserStoreMacaroon string
|
|
loginUserDischarge string
|
|
|
|
mockUserHome string
|
|
trivialUserLookup func(username string) (*user.User, error)
|
|
}
|
|
|
|
func (s *userSuite) LoginUser(username, password, otp string) (string, string, error) {
|
|
s.pokeStateLock()
|
|
|
|
return s.loginUserStoreMacaroon, s.loginUserDischarge, s.err
|
|
}
|
|
|
|
func (s *userSuite) SetUpTest(c *check.C) {
|
|
s.apiBaseSuite.SetUpTest(c)
|
|
|
|
s.AddCleanup(release.MockOnClassic(false))
|
|
|
|
s.daemonWithStore(c, s)
|
|
|
|
s.expectRootAccess()
|
|
|
|
s.mockUserHome = c.MkDir()
|
|
s.trivialUserLookup = mkUserLookup(s.mockUserHome)
|
|
|
|
s.AddCleanup(daemon.MockHasUserAdmin(true))
|
|
|
|
// make sure we don't call these by accident)
|
|
s.AddCleanup(daemon.MockDeviceStateCreateUser(func(st *state.State, sudoer bool, email string, expiration time.Time) (createdUsers *devicestate.CreatedUser, internalErr error) {
|
|
c.Fatalf("unexpected create user %q call", email)
|
|
return nil, &devicestate.UserError{Err: fmt.Errorf("unexpected create user %q call", email)}
|
|
}))
|
|
|
|
s.AddCleanup(daemon.MockDeviceStateCreateKnownUsers(func(st *state.State, sudoer bool, email string) (createdUsers []*devicestate.CreatedUser, internalErr error) {
|
|
c.Fatalf("unexpected create user %q call", email)
|
|
return nil, &devicestate.UserError{Err: fmt.Errorf("unexpected create user %q call", email)}
|
|
}))
|
|
|
|
s.AddCleanup(daemon.MockDeviceStateRemoveUser(func(st *state.State, username string, opts *devicestate.RemoveUserOptions) (*auth.UserState, error) {
|
|
c.Fatalf("unexpected remove user %q call", username)
|
|
return nil, &devicestate.UserError{Err: fmt.Errorf("unexpected remove user %q call", username)}
|
|
}))
|
|
|
|
s.loginUserStoreMacaroon = ""
|
|
s.loginUserDischarge = ""
|
|
}
|
|
|
|
func mkUserLookup(userHomeDir string) func(string) (*user.User, error) {
|
|
return func(username string) (*user.User, error) {
|
|
cur, err := user.Current()
|
|
cur.Username = username
|
|
cur.HomeDir = userHomeDir
|
|
return cur, err
|
|
}
|
|
}
|
|
|
|
func (s *userSuite) expectLoginAccess() {
|
|
s.expectWriteAccess(daemon.AuthenticatedAccess{Polkit: "io.snapcraft.snapd.login"})
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUser(c *check.C) {
|
|
state := s.d.Overlord().State()
|
|
|
|
s.expectLoginAccess()
|
|
|
|
s.loginUserStoreMacaroon = "user-macaroon"
|
|
s.loginUserDischarge = "the-discharge-macaroon-serialized-data"
|
|
buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, nil)
|
|
|
|
state.Lock()
|
|
user, err := auth.User(state, 1)
|
|
state.Unlock()
|
|
c.Check(err, check.IsNil)
|
|
|
|
expected := daemon.UserResponseData{
|
|
ID: 1,
|
|
Email: "email@.com",
|
|
|
|
Macaroon: user.Macaroon,
|
|
Discharges: user.Discharges,
|
|
}
|
|
|
|
c.Check(rsp.Status, check.Equals, 200)
|
|
c.Assert(rsp.Result, check.FitsTypeOf, expected)
|
|
c.Check(rsp.Result, check.DeepEquals, expected)
|
|
|
|
c.Check(user.ID, check.Equals, 1)
|
|
c.Check(user.Username, check.Equals, "")
|
|
c.Check(user.Email, check.Equals, "email@.com")
|
|
c.Check(user.Discharges, check.IsNil)
|
|
c.Check(user.StoreMacaroon, check.Equals, s.loginUserStoreMacaroon)
|
|
c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"})
|
|
// snapd macaroon was setup too
|
|
snapdMacaroon, err := auth.MacaroonDeserialize(user.Macaroon)
|
|
c.Check(err, check.IsNil)
|
|
c.Check(snapdMacaroon.Id(), check.Equals, "1")
|
|
c.Check(snapdMacaroon.Location(), check.Equals, "snapd")
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserWithUsername(c *check.C) {
|
|
state := s.d.Overlord().State()
|
|
|
|
s.expectLoginAccess()
|
|
|
|
s.loginUserStoreMacaroon = "user-macaroon"
|
|
s.loginUserDischarge = "the-discharge-macaroon-serialized-data"
|
|
buf := bytes.NewBufferString(`{"username": "username", "email": "email@.com", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, nil)
|
|
|
|
state.Lock()
|
|
user, err := auth.User(state, 1)
|
|
state.Unlock()
|
|
c.Check(err, check.IsNil)
|
|
|
|
expected := daemon.UserResponseData{
|
|
ID: 1,
|
|
Username: "username",
|
|
Email: "email@.com",
|
|
Macaroon: user.Macaroon,
|
|
Discharges: user.Discharges,
|
|
}
|
|
c.Check(rsp.Status, check.Equals, 200)
|
|
c.Assert(rsp.Result, check.FitsTypeOf, expected)
|
|
c.Check(rsp.Result, check.DeepEquals, expected)
|
|
|
|
c.Check(user.ID, check.Equals, 1)
|
|
c.Check(user.Username, check.Equals, "username")
|
|
c.Check(user.Email, check.Equals, "email@.com")
|
|
c.Check(user.Discharges, check.IsNil)
|
|
c.Check(user.StoreMacaroon, check.Equals, s.loginUserStoreMacaroon)
|
|
c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"})
|
|
// snapd macaroon was setup too
|
|
snapdMacaroon, err := auth.MacaroonDeserialize(user.Macaroon)
|
|
c.Check(err, check.IsNil)
|
|
c.Check(snapdMacaroon.Id(), check.Equals, "1")
|
|
c.Check(snapdMacaroon.Location(), check.Equals, "snapd")
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserNoEmailWithExistentLocalUser(c *check.C) {
|
|
state := s.d.Overlord().State()
|
|
|
|
s.expectLoginAccess()
|
|
|
|
// setup local-only user
|
|
state.Lock()
|
|
localUser, err := auth.NewUser(state, auth.NewUserParams{
|
|
Username: "username",
|
|
Email: "email@test.com",
|
|
Macaroon: "",
|
|
Discharges: nil,
|
|
})
|
|
state.Unlock()
|
|
c.Assert(err, check.IsNil)
|
|
|
|
s.loginUserStoreMacaroon = "user-macaroon"
|
|
s.loginUserDischarge = "the-discharge-macaroon-serialized-data"
|
|
buf := bytes.NewBufferString(`{"username": "username", "email": "", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, localUser)
|
|
|
|
expected := daemon.UserResponseData{
|
|
ID: 1,
|
|
Username: "username",
|
|
Email: "email@test.com",
|
|
|
|
Macaroon: localUser.Macaroon,
|
|
Discharges: localUser.Discharges,
|
|
}
|
|
c.Check(rsp.Status, check.Equals, 200)
|
|
c.Assert(rsp.Result, check.FitsTypeOf, expected)
|
|
c.Check(rsp.Result, check.DeepEquals, expected)
|
|
|
|
state.Lock()
|
|
user, err := auth.User(state, localUser.ID)
|
|
state.Unlock()
|
|
c.Check(err, check.IsNil)
|
|
c.Check(user.Username, check.Equals, "username")
|
|
c.Check(user.Email, check.Equals, localUser.Email)
|
|
c.Check(user.Macaroon, check.Equals, localUser.Macaroon)
|
|
c.Check(user.Discharges, check.IsNil)
|
|
c.Check(user.StoreMacaroon, check.Equals, s.loginUserStoreMacaroon)
|
|
c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"})
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserWithExistentLocalUser(c *check.C) {
|
|
state := s.d.Overlord().State()
|
|
|
|
s.expectLoginAccess()
|
|
|
|
// setup local-only user
|
|
state.Lock()
|
|
localUser, err := auth.NewUser(state, auth.NewUserParams{
|
|
Username: "username",
|
|
Email: "email@test.com",
|
|
Macaroon: "",
|
|
Discharges: nil,
|
|
})
|
|
state.Unlock()
|
|
c.Assert(err, check.IsNil)
|
|
|
|
s.loginUserStoreMacaroon = "user-macaroon"
|
|
s.loginUserDischarge = "the-discharge-macaroon-serialized-data"
|
|
buf := bytes.NewBufferString(`{"username": "username", "email": "email@test.com", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, localUser)
|
|
|
|
expected := daemon.UserResponseData{
|
|
ID: 1,
|
|
Username: "username",
|
|
Email: "email@test.com",
|
|
|
|
Macaroon: localUser.Macaroon,
|
|
Discharges: localUser.Discharges,
|
|
}
|
|
c.Check(rsp.Status, check.Equals, 200)
|
|
c.Assert(rsp.Result, check.FitsTypeOf, expected)
|
|
c.Check(rsp.Result, check.DeepEquals, expected)
|
|
|
|
state.Lock()
|
|
user, err := auth.User(state, localUser.ID)
|
|
state.Unlock()
|
|
c.Check(err, check.IsNil)
|
|
c.Check(user.Username, check.Equals, "username")
|
|
c.Check(user.Email, check.Equals, localUser.Email)
|
|
c.Check(user.Macaroon, check.Equals, localUser.Macaroon)
|
|
c.Check(user.Discharges, check.IsNil)
|
|
c.Check(user.StoreMacaroon, check.Equals, s.loginUserStoreMacaroon)
|
|
c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"})
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserNewEmailWithExistentLocalUser(c *check.C) {
|
|
state := s.d.Overlord().State()
|
|
|
|
s.expectLoginAccess()
|
|
|
|
// setup local-only user
|
|
state.Lock()
|
|
localUser, err := auth.NewUser(state, auth.NewUserParams{
|
|
Username: "username",
|
|
Email: "email@test.com",
|
|
Macaroon: "",
|
|
Discharges: nil,
|
|
})
|
|
state.Unlock()
|
|
c.Assert(err, check.IsNil)
|
|
|
|
s.loginUserStoreMacaroon = "user-macaroon"
|
|
s.loginUserDischarge = "the-discharge-macaroon-serialized-data"
|
|
// same local user, but using a new SSO account
|
|
buf := bytes.NewBufferString(`{"username": "username", "email": "new.email@test.com", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, localUser)
|
|
|
|
expected := daemon.UserResponseData{
|
|
ID: 1,
|
|
Username: "username",
|
|
Email: "new.email@test.com",
|
|
|
|
Macaroon: localUser.Macaroon,
|
|
Discharges: localUser.Discharges,
|
|
}
|
|
c.Check(rsp.Status, check.Equals, 200)
|
|
c.Assert(rsp.Result, check.FitsTypeOf, expected)
|
|
c.Check(rsp.Result, check.DeepEquals, expected)
|
|
|
|
state.Lock()
|
|
user, err := auth.User(state, localUser.ID)
|
|
state.Unlock()
|
|
c.Check(err, check.IsNil)
|
|
c.Check(user.Username, check.Equals, "username")
|
|
c.Check(user.Email, check.Equals, expected.Email)
|
|
c.Check(user.Macaroon, check.Equals, localUser.Macaroon)
|
|
c.Check(user.Discharges, check.IsNil)
|
|
c.Check(user.StoreMacaroon, check.Equals, s.loginUserStoreMacaroon)
|
|
c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"})
|
|
}
|
|
|
|
func (s *userSuite) TestLogoutUser(c *check.C) {
|
|
state := s.d.Overlord().State()
|
|
|
|
s.expectLoginAccess()
|
|
|
|
state.Lock()
|
|
user, err := auth.NewUser(state, auth.NewUserParams{
|
|
Username: "username",
|
|
Email: "email@test.com",
|
|
Macaroon: "macaroon",
|
|
Discharges: []string{"discharge"},
|
|
})
|
|
state.Unlock()
|
|
c.Assert(err, check.IsNil)
|
|
|
|
req, err := http.NewRequest("POST", "/v2/logout", nil)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, user)
|
|
c.Check(rsp.Status, check.Equals, 200)
|
|
|
|
state.Lock()
|
|
_, err = auth.User(state, user.ID)
|
|
state.Unlock()
|
|
c.Check(err, check.Equals, auth.ErrInvalidUser)
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserBadRequest(c *check.C) {
|
|
s.expectLoginAccess()
|
|
|
|
buf := bytes.NewBufferString(`hello`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Status, check.Equals, 400)
|
|
c.Check(rspe.Message, check.Not(check.Equals), "")
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserNotEmailish(c *check.C) {
|
|
s.expectLoginAccess()
|
|
|
|
buf := bytes.NewBufferString(`{"username": "notemail", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Status, check.Equals, 400)
|
|
c.Check(rspe.Message, testutil.Contains, "please use a valid email address")
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserDeveloperAPIError(c *check.C) {
|
|
s.expectLoginAccess()
|
|
|
|
s.err = fmt.Errorf("error-from-login-user")
|
|
buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Status, check.Equals, 401)
|
|
c.Check(rspe.Message, testutil.Contains, "error-from-login-user")
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserTwoFactorRequiredError(c *check.C) {
|
|
s.expectLoginAccess()
|
|
|
|
s.err = store.ErrAuthenticationNeeds2fa
|
|
buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Status, check.Equals, 401)
|
|
c.Check(rspe.Kind, check.Equals, client.ErrorKindTwoFactorRequired)
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserTwoFactorFailedError(c *check.C) {
|
|
s.expectLoginAccess()
|
|
|
|
s.err = store.Err2faFailed
|
|
buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Status, check.Equals, 401)
|
|
c.Check(rspe.Kind, check.Equals, client.ErrorKindTwoFactorFailed)
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserInvalidCredentialsError(c *check.C) {
|
|
s.expectLoginAccess()
|
|
|
|
s.err = store.ErrInvalidCredentials
|
|
buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Status, check.Equals, 401)
|
|
c.Check(rspe.Message, check.Equals, "invalid credentials")
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserInvalidAuthDataError(c *check.C) {
|
|
s.expectLoginAccess()
|
|
|
|
s.err = store.InvalidAuthDataError{"foo": {"bar"}}
|
|
buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Status, check.Equals, 400)
|
|
c.Check(rspe.Kind, check.Equals, client.ErrorKindInvalidAuthData)
|
|
c.Check(rspe.Value, check.DeepEquals, s.err)
|
|
}
|
|
|
|
func (s *userSuite) TestLoginUserPasswordPolicyError(c *check.C) {
|
|
s.expectLoginAccess()
|
|
|
|
s.err = store.PasswordPolicyError{"foo": {"bar"}}
|
|
buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
|
|
req, err := http.NewRequest("POST", "/v2/login", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Status, check.Equals, 401)
|
|
c.Check(rspe.Kind, check.Equals, client.ErrorKindPasswordPolicy)
|
|
c.Check(rspe.Value, check.DeepEquals, s.err)
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUser(c *check.C) {
|
|
s.testCreateUser(c, true)
|
|
}
|
|
|
|
func (s *userSuite) TestPostUserCreate(c *check.C) {
|
|
s.testCreateUser(c, false)
|
|
}
|
|
|
|
func (s *userSuite) testCreateUser(c *check.C, oldWay bool) {
|
|
expectedUsername := "karl"
|
|
expectedEmail := "popper@lse.ac.uk"
|
|
|
|
var deviceStateCreateUserCalled bool
|
|
defer daemon.MockDeviceStateCreateUser(func(st *state.State, sudoer bool, email string, expiration time.Time) (*devicestate.CreatedUser, error) {
|
|
c.Check(email, check.Equals, expectedEmail)
|
|
c.Check(sudoer, check.Equals, false)
|
|
c.Check(expiration, check.Equals, time.Time{})
|
|
expected := &devicestate.CreatedUser{
|
|
Username: expectedUsername,
|
|
SSHKeys: []string{
|
|
`ssh1 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`,
|
|
`ssh2 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`,
|
|
},
|
|
}
|
|
deviceStateCreateUserCalled = true
|
|
return expected, nil
|
|
})()
|
|
|
|
var req *http.Request
|
|
var expected interface{}
|
|
expectedItem := daemon.UserResponseData{
|
|
Username: expectedUsername,
|
|
SSHKeys: []string{
|
|
`ssh1 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`,
|
|
`ssh2 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`,
|
|
},
|
|
}
|
|
|
|
if oldWay {
|
|
var err error
|
|
buf := bytes.NewBufferString(fmt.Sprintf(`{"email": "%s"}`, expectedEmail))
|
|
req, err = http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
expected = &expectedItem
|
|
} else {
|
|
var err error
|
|
buf := bytes.NewBufferString(fmt.Sprintf(`{"action":"create","email": "%s"}`, expectedEmail))
|
|
req, err = http.NewRequest("POST", "/v2/users", buf)
|
|
c.Assert(err, check.IsNil)
|
|
expected = []daemon.UserResponseData{expectedItem}
|
|
}
|
|
|
|
rsp := s.syncReq(c, req, nil)
|
|
c.Check(rsp.Result, check.FitsTypeOf, expected)
|
|
c.Check(rsp.Result, check.DeepEquals, expected)
|
|
c.Check(deviceStateCreateUserCalled, check.Equals, true)
|
|
}
|
|
|
|
func (s *userSuite) TestPostUserCreateErrBadRequest(c *check.C) {
|
|
s.testCreateUserErr(c, false)
|
|
}
|
|
|
|
func (s *userSuite) TestPostUserCreateErrInternal(c *check.C) {
|
|
s.testCreateUserErr(c, true)
|
|
}
|
|
|
|
func (s *userSuite) testCreateUserErr(c *check.C, internalErr bool) {
|
|
called := 0
|
|
defer daemon.MockDeviceStateCreateKnownUsers(func(st *state.State, sudoer bool, email string) ([]*devicestate.CreatedUser, error) {
|
|
called++
|
|
if internalErr {
|
|
return nil, fmt.Errorf("internal error: wat-internal")
|
|
} else {
|
|
return nil, &devicestate.UserError{Err: fmt.Errorf("wat-badrequest")}
|
|
}
|
|
})()
|
|
|
|
buf := bytes.NewBufferString(`{"email": "foo@bar.com","known":true}`)
|
|
req, err := http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(called, check.Equals, 1)
|
|
if internalErr {
|
|
c.Check(rspe.Status, check.Equals, 500)
|
|
c.Check(rspe.Message, check.Equals, "internal error: wat-internal")
|
|
} else {
|
|
c.Check(rspe.Status, check.Equals, 400)
|
|
c.Check(rspe.Message, check.Equals, "wat-badrequest")
|
|
}
|
|
}
|
|
|
|
func (s *userSuite) TestNoUserAdminCreateUser(c *check.C) { s.testNoUserAdmin(c, "/v2/create-user") }
|
|
|
|
func (s *userSuite) TestNoUserAdminPostUser(c *check.C) { s.testNoUserAdmin(c, "/v2/users") }
|
|
|
|
func (s *userSuite) testNoUserAdmin(c *check.C, endpoint string) {
|
|
defer daemon.MockHasUserAdmin(false)()
|
|
|
|
buf := bytes.NewBufferString("{}")
|
|
req, err := http.NewRequest("POST", endpoint, buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
|
|
const noUserAdmin = "system user administration via snapd is not allowed on this system"
|
|
switch endpoint {
|
|
case "/v2/users":
|
|
c.Check(rspe, check.DeepEquals, daemon.MethodNotAllowed(noUserAdmin))
|
|
case "/v2/create-user":
|
|
c.Check(rspe, check.DeepEquals, daemon.Forbidden(noUserAdmin))
|
|
default:
|
|
c.Fatalf("unknown endpoint %q", endpoint)
|
|
}
|
|
}
|
|
|
|
func (s *userSuite) TestPostUserBadBody(c *check.C) {
|
|
buf := bytes.NewBufferString(`42`)
|
|
req, err := http.NewRequest("POST", "/v2/users", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Message, check.Matches, "cannot decode user action data from request body: .*")
|
|
}
|
|
|
|
func (s *userSuite) TestPostUserBadAfterBody(c *check.C) {
|
|
buf := bytes.NewBufferString(`{}42`)
|
|
req, err := http.NewRequest("POST", "/v2/users", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe, check.DeepEquals, daemon.BadRequest("spurious content after user action"))
|
|
}
|
|
|
|
func (s *userSuite) TestPostUserNoAction(c *check.C) {
|
|
buf := bytes.NewBufferString("{}")
|
|
req, err := http.NewRequest("POST", "/v2/users", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe, check.DeepEquals, daemon.BadRequest("missing user action"))
|
|
}
|
|
|
|
func (s *userSuite) TestPostUserBadAction(c *check.C) {
|
|
buf := bytes.NewBufferString(`{"action":"patatas"}`)
|
|
req, err := http.NewRequest("POST", "/v2/users", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe, check.DeepEquals, daemon.BadRequest(`unsupported user action "patatas"`))
|
|
}
|
|
|
|
func (s *userSuite) TestPostUserActionRemoveDelUserErrBadRequest(c *check.C) {
|
|
s.testpostUserActionRemoveDelUserErr(c, false)
|
|
}
|
|
|
|
func (s *userSuite) TestPostUserActionRemoveDelUserErrInternal(c *check.C) {
|
|
s.testpostUserActionRemoveDelUserErr(c, true)
|
|
}
|
|
|
|
func (s *userSuite) testpostUserActionRemoveDelUserErr(c *check.C, internalErr bool) {
|
|
st := s.d.Overlord().State()
|
|
st.Lock()
|
|
_, err := auth.NewUser(st, auth.NewUserParams{
|
|
Username: "some-user",
|
|
Email: "email@test.com",
|
|
Macaroon: "macaroon",
|
|
Discharges: []string{"discharge"},
|
|
})
|
|
st.Unlock()
|
|
c.Check(err, check.IsNil)
|
|
|
|
called := 0
|
|
defer daemon.MockDeviceStateRemoveUser(func(st *state.State, username string, opts *devicestate.RemoveUserOptions) (*auth.UserState, error) {
|
|
called++
|
|
if internalErr {
|
|
return nil, fmt.Errorf("internal error: wat-internal")
|
|
} else {
|
|
return nil, &devicestate.UserError{Err: fmt.Errorf("wat-badrequest")}
|
|
}
|
|
})()
|
|
|
|
buf := bytes.NewBufferString(`{"action":"remove","username":"some-user"}`)
|
|
req, err := http.NewRequest("POST", "/v2/users", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(called, check.Equals, 1)
|
|
if internalErr {
|
|
c.Check(rspe.Status, check.Equals, 500)
|
|
c.Check(rspe.Message, check.Equals, "internal error: wat-internal")
|
|
} else {
|
|
c.Check(rspe.Status, check.Equals, 400)
|
|
c.Check(rspe.Message, check.Equals, "wat-badrequest")
|
|
}
|
|
}
|
|
|
|
func (s *userSuite) TestPostUserActionRemove(c *check.C) {
|
|
expectedID := 10
|
|
expectedUsername := "some-user"
|
|
expectedEmail := "email@test.com"
|
|
|
|
called := 0
|
|
defer daemon.MockDeviceStateRemoveUser(func(st *state.State, username string, opts *devicestate.RemoveUserOptions) (*auth.UserState, error) {
|
|
called++
|
|
removedUser := &auth.UserState{ID: expectedID, Username: expectedUsername, Email: expectedEmail}
|
|
|
|
return removedUser, nil
|
|
})()
|
|
|
|
buf := bytes.NewBufferString(`{"action":"remove","username":"some-user"}`)
|
|
req, err := http.NewRequest("POST", "/v2/users", buf)
|
|
c.Assert(err, check.IsNil)
|
|
rsp := s.syncReq(c, req, nil)
|
|
c.Check(rsp.Status, check.Equals, 200)
|
|
expected := []daemon.UserResponseData{
|
|
{ID: expectedID, Username: expectedUsername, Email: expectedEmail},
|
|
}
|
|
c.Check(rsp.Result, check.DeepEquals, map[string]interface{}{
|
|
"removed": expected,
|
|
})
|
|
c.Check(called, check.Equals, 1)
|
|
}
|
|
|
|
func (s *userSuite) setupSigner(accountID string, signerPrivKey asserts.PrivateKey) *assertstest.SigningDB {
|
|
st := s.d.Overlord().State()
|
|
|
|
signerSigning := s.Brands.Register(accountID, signerPrivKey, map[string]interface{}{
|
|
"account-id": accountID,
|
|
"verification": "verified",
|
|
})
|
|
acctNKey := s.Brands.AccountsAndKeys(accountID)
|
|
|
|
assertstest.AddMany(s.StoreSigning, acctNKey...)
|
|
assertstatetest.AddMany(st, acctNKey...)
|
|
|
|
return signerSigning
|
|
}
|
|
|
|
var (
|
|
partnerPrivKey, _ = assertstest.GenerateKey(752)
|
|
unknownPrivKey, _ = assertstest.GenerateKey(752)
|
|
)
|
|
|
|
func (s *userSuite) makeSystemUsers(c *check.C, systemUsers []map[string]interface{}) {
|
|
st := s.d.Overlord().State()
|
|
st.Lock()
|
|
defer st.Unlock()
|
|
|
|
assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""))
|
|
|
|
s.setupSigner("my-brand", brandPrivKey)
|
|
s.setupSigner("partner", partnerPrivKey)
|
|
s.setupSigner("unknown", unknownPrivKey)
|
|
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
"system-user-authority": []interface{}{"my-brand", "partner"},
|
|
})
|
|
// now add model related stuff to the system
|
|
assertstatetest.AddMany(st, model)
|
|
// and a serial
|
|
deviceKey, _ := assertstest.GenerateKey(752)
|
|
encDevKey, err := asserts.EncodePublicKey(deviceKey.PublicKey())
|
|
c.Assert(err, check.IsNil)
|
|
serial, err := s.Brands.Signing("my-brand").Sign(asserts.SerialType, map[string]interface{}{
|
|
"authority-id": "my-brand",
|
|
"brand-id": "my-brand",
|
|
"model": "my-model",
|
|
"serial": "serialserial",
|
|
"device-key": string(encDevKey),
|
|
"device-key-sha3-384": deviceKey.PublicKey().ID(),
|
|
"timestamp": time.Now().Format(time.RFC3339),
|
|
}, nil, "")
|
|
c.Assert(err, check.IsNil)
|
|
assertstatetest.AddMany(st, serial)
|
|
|
|
for _, suMap := range systemUsers {
|
|
su, err := s.Brands.Signing(suMap["authority-id"].(string)).Sign(asserts.SystemUserType, suMap, nil, "")
|
|
c.Assert(err, check.IsNil)
|
|
su = su.(*asserts.SystemUser)
|
|
// now add system-user assertion to the system
|
|
assertstatetest.AddMany(st, su)
|
|
}
|
|
// create fake device
|
|
err = devicestatetest.SetDevice(st, &auth.DeviceState{
|
|
Brand: "my-brand",
|
|
Model: "my-model",
|
|
Serial: "serialserial",
|
|
})
|
|
c.Assert(err, check.IsNil)
|
|
}
|
|
|
|
var goodUser = map[string]interface{}{
|
|
"authority-id": "my-brand",
|
|
"brand-id": "my-brand",
|
|
"email": "foo@bar.com",
|
|
"series": []interface{}{"16", "18"},
|
|
"models": []interface{}{"my-model", "other-model"},
|
|
"name": "Boring Guy",
|
|
"username": "guy",
|
|
"password": "$6$salt$hash",
|
|
"since": time.Now().Format(time.RFC3339),
|
|
"until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339),
|
|
}
|
|
|
|
var partnerUser = map[string]interface{}{
|
|
"authority-id": "partner",
|
|
"brand-id": "my-brand",
|
|
"email": "p@partner.com",
|
|
"series": []interface{}{"16", "18"},
|
|
"models": []interface{}{"my-model"},
|
|
"name": "Partner Guy",
|
|
"username": "partnerguy",
|
|
"password": "$6$salt$hash",
|
|
"since": time.Now().Format(time.RFC3339),
|
|
"until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339),
|
|
}
|
|
|
|
var serialUser = map[string]interface{}{
|
|
"format": "1",
|
|
"authority-id": "my-brand",
|
|
"brand-id": "my-brand",
|
|
"email": "serial@bar.com",
|
|
"series": []interface{}{"16", "18"},
|
|
"models": []interface{}{"my-model"},
|
|
"serials": []interface{}{"serialserial"},
|
|
"name": "Serial Guy",
|
|
"username": "goodserialguy",
|
|
"password": "$6$salt$hash",
|
|
"since": time.Now().Format(time.RFC3339),
|
|
"until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339),
|
|
}
|
|
|
|
var badUser = map[string]interface{}{
|
|
// bad user (not valid for this model)
|
|
"authority-id": "my-brand",
|
|
"brand-id": "my-brand",
|
|
"email": "foobar@bar.com",
|
|
"series": []interface{}{"16", "18"},
|
|
"models": []interface{}{"non-of-the-models-i-have"},
|
|
"name": "Random Gal",
|
|
"username": "gal",
|
|
"password": "$6$salt$hash",
|
|
"since": time.Now().Format(time.RFC3339),
|
|
"until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339),
|
|
}
|
|
|
|
var badUserNoMatchingSerial = map[string]interface{}{
|
|
"format": "1",
|
|
"authority-id": "my-brand",
|
|
"brand-id": "my-brand",
|
|
"email": "noserial@bar.com",
|
|
"series": []interface{}{"16", "18"},
|
|
"models": []interface{}{"my-model"},
|
|
"serials": []interface{}{"different-serialserial"},
|
|
"name": "No Serial Guy",
|
|
"username": "noserial",
|
|
"password": "$6$salt$hash",
|
|
"since": time.Now().Format(time.RFC3339),
|
|
"until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339),
|
|
}
|
|
|
|
var unknownUser = map[string]interface{}{
|
|
"authority-id": "unknown",
|
|
"brand-id": "my-brand",
|
|
"email": "x@partner.com",
|
|
"series": []interface{}{"16", "18"},
|
|
"models": []interface{}{"my-model"},
|
|
"name": "XGuy",
|
|
"username": "xguy",
|
|
"password": "$6$salt$hash",
|
|
"since": time.Now().Format(time.RFC3339),
|
|
"until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339),
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUserFromAssertionAllKnownClassicErrors(c *check.C) {
|
|
restore := release.MockOnClassic(true)
|
|
defer restore()
|
|
|
|
s.makeSystemUsers(c, []map[string]interface{}{goodUser})
|
|
|
|
// do it!
|
|
buf := bytes.NewBufferString(`{"known":true}`)
|
|
req, err := http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Message, check.Matches, `cannot create user: device is a classic system`)
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUserFromAssertionAllKnownButOwnedErrors(c *check.C) {
|
|
s.makeSystemUsers(c, []map[string]interface{}{goodUser})
|
|
|
|
st := s.d.Overlord().State()
|
|
st.Lock()
|
|
_, err := auth.NewUser(st, auth.NewUserParams{
|
|
Username: "username",
|
|
Email: "email@test.com",
|
|
Macaroon: "macaroon",
|
|
Discharges: []string{"discharge"},
|
|
})
|
|
st.Unlock()
|
|
c.Check(err, check.IsNil)
|
|
|
|
// do it!
|
|
buf := bytes.NewBufferString(`{"known":true}`)
|
|
req, err := http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Message, check.Matches, `cannot create user: device already managed`)
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUserAutomaticManagedDoesNotActOrError(c *check.C) {
|
|
s.makeSystemUsers(c, []map[string]interface{}{goodUser})
|
|
|
|
st := s.d.Overlord().State()
|
|
st.Lock()
|
|
_, err := auth.NewUser(st, auth.NewUserParams{
|
|
Username: "username",
|
|
Email: "email@test.com",
|
|
Macaroon: "macaroon",
|
|
Discharges: []string{"discharge"},
|
|
})
|
|
st.Unlock()
|
|
c.Check(err, check.IsNil)
|
|
|
|
// do it!
|
|
buf := bytes.NewBufferString(`{"automatic":true}`)
|
|
req, err := http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, nil)
|
|
|
|
// expecting an empty reply
|
|
expected := []daemon.UserResponseData{}
|
|
c.Check(rsp.Result, check.FitsTypeOf, expected)
|
|
c.Check(rsp.Result, check.DeepEquals, expected)
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUserAutomaticDisabled(c *check.C) {
|
|
s.makeSystemUsers(c, []map[string]interface{}{goodUser})
|
|
|
|
// disable automatic user creation
|
|
st := s.d.Overlord().State()
|
|
st.Lock()
|
|
tr := config.NewTransaction(st)
|
|
err := tr.Set("core", "users.create.automatic", false)
|
|
tr.Commit()
|
|
st.Unlock()
|
|
c.Assert(err, check.IsNil)
|
|
|
|
// if there is attempt to create user, test should panic
|
|
|
|
// do it!
|
|
buf := bytes.NewBufferString(`{"automatic": true}`)
|
|
req, err := http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, nil)
|
|
|
|
// empty result
|
|
expected := []daemon.UserResponseData{}
|
|
c.Check(rsp.Result, check.FitsTypeOf, expected)
|
|
c.Check(rsp.Result, check.DeepEquals, expected)
|
|
|
|
// ensure no user was added to the state
|
|
st.Lock()
|
|
users, err := auth.Users(st)
|
|
c.Assert(err, check.IsNil)
|
|
st.Unlock()
|
|
c.Check(users, check.HasLen, 0)
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUserExpirationHappy(c *check.C) {
|
|
expectedUsername := "karl"
|
|
expectedEmail := "popper@lse.ac.uk"
|
|
// strip away subsecond values which are lost during json marshal/unmarshalling
|
|
expectedTime := time.Now().Add(time.Hour * 24).Round(time.Second)
|
|
|
|
var deviceStateCreateUserCalls int
|
|
defer daemon.MockDeviceStateCreateUser(func(st *state.State, sudoer bool, email string, expiration time.Time) (*devicestate.CreatedUser, error) {
|
|
c.Check(email, check.Equals, expectedEmail)
|
|
c.Check(sudoer, check.Equals, false)
|
|
c.Check(expiration.Equal(expectedTime), check.Equals, true)
|
|
expected := &devicestate.CreatedUser{
|
|
Username: expectedUsername,
|
|
SSHKeys: []string{
|
|
`ssh1 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`,
|
|
`ssh2 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`,
|
|
},
|
|
}
|
|
deviceStateCreateUserCalls++
|
|
return expected, nil
|
|
})()
|
|
|
|
buf := bytes.NewBufferString(fmt.Sprintf(`{"email":"%s","expiration":"%s"}`,
|
|
expectedEmail, expectedTime.Format(time.RFC3339)))
|
|
req, err := http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, nil)
|
|
c.Check(rsp.Result, check.DeepEquals, &daemon.UserResponseData{
|
|
Username: expectedUsername,
|
|
SSHKeys: []string{
|
|
`ssh1 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`,
|
|
`ssh2 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`,
|
|
},
|
|
})
|
|
c.Check(deviceStateCreateUserCalls, check.Equals, 1)
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUserExpirationDateSetInPast(c *check.C) {
|
|
buf := bytes.NewBufferString(fmt.Sprintf(`{"expiration":"%s"}`,
|
|
time.Now().Add(-(time.Hour * 24)).Format(time.RFC3339)))
|
|
req, err := http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Message, check.Matches, `cannot create user: expiration date must be set in the future`)
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUserExpirationKnownNotAllowed(c *check.C) {
|
|
buf := bytes.NewBufferString(fmt.Sprintf(`{"known": true, "expiration":"%s"}`,
|
|
time.Now().Add(time.Hour*24).Format(time.RFC3339)))
|
|
req, err := http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Message, check.Matches, `cannot create user: expiration date cannot be provided for known users`)
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUserExpirationAutomaticNotAllowed(c *check.C) {
|
|
// Automatic implies known, which means we should see identical result to
|
|
// the known unit test
|
|
buf := bytes.NewBufferString(fmt.Sprintf(`{"automatic": true, "expiration":"%s"}`,
|
|
time.Now().Add(time.Hour*24).Format(time.RFC3339)))
|
|
req, err := http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rspe := s.errorReq(c, req, nil)
|
|
c.Check(rspe.Message, check.Matches, `cannot create user: expiration date cannot be provided for known users`)
|
|
}
|
|
|
|
func (s *userSuite) TestUsersEmpty(c *check.C) {
|
|
req, err := http.NewRequest("GET", "/v2/users", nil)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, nil)
|
|
|
|
expected := []daemon.UserResponseData{}
|
|
c.Check(rsp.Result, check.FitsTypeOf, expected)
|
|
c.Check(rsp.Result, check.DeepEquals, expected)
|
|
}
|
|
|
|
func (s *userSuite) TestUsersHasUser(c *check.C) {
|
|
st := s.d.Overlord().State()
|
|
st.Lock()
|
|
u, err := auth.NewUser(st, auth.NewUserParams{
|
|
Username: "someuser",
|
|
Email: "email@test.com",
|
|
Macaroon: "macaroon",
|
|
Discharges: []string{"discharge"},
|
|
})
|
|
st.Unlock()
|
|
c.Assert(err, check.IsNil)
|
|
|
|
req, err := http.NewRequest("GET", "/v2/users", nil)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, nil)
|
|
|
|
expected := []daemon.UserResponseData{
|
|
{ID: u.ID, Username: u.Username, Email: u.Email},
|
|
}
|
|
c.Check(rsp.Result, check.FitsTypeOf, expected)
|
|
c.Check(rsp.Result, check.DeepEquals, expected)
|
|
}
|
|
|
|
func (s *userSuite) testPostCreateUserFromAssertion(c *check.C, postData string, expectSudoer bool) {
|
|
s.makeSystemUsers(c, []map[string]interface{}{goodUser, partnerUser, serialUser, badUser, badUserNoMatchingSerial, unknownUser})
|
|
|
|
// mock the calls that create the user
|
|
var deviceStateCreateUserCalled bool
|
|
defer daemon.MockDeviceStateCreateUser(func(st *state.State, sudoer bool, email string, expiration time.Time) (*devicestate.CreatedUser, error) {
|
|
deviceStateCreateUserCalled = true
|
|
return nil, nil
|
|
})()
|
|
defer daemon.MockDeviceStateCreateKnownUsers(func(st *state.State, sudoer bool, email string) ([]*devicestate.CreatedUser, error) {
|
|
c.Check(sudoer, check.Equals, expectSudoer)
|
|
createdUsers := []*devicestate.CreatedUser{
|
|
{
|
|
Username: "goodserialguy",
|
|
},
|
|
{
|
|
Username: "guy",
|
|
},
|
|
{
|
|
Username: "partnerguy",
|
|
},
|
|
}
|
|
return createdUsers, nil
|
|
})()
|
|
|
|
// do it!
|
|
buf := bytes.NewBufferString(postData)
|
|
req, err := http.NewRequest("POST", "/v2/create-user", buf)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rsp := s.syncReq(c, req, nil)
|
|
|
|
// note that we get a list here instead of a single
|
|
// userResponseData item
|
|
c.Check(rsp.Result, check.FitsTypeOf, []daemon.UserResponseData{})
|
|
c.Check(deviceStateCreateUserCalled, check.Equals, false)
|
|
seen := map[string]bool{}
|
|
for _, u := range rsp.Result.([]daemon.UserResponseData) {
|
|
seen[u.Username] = true
|
|
c.Check(u, check.DeepEquals, daemon.UserResponseData{Username: u.Username})
|
|
}
|
|
c.Check(seen, check.DeepEquals, map[string]bool{
|
|
"guy": true,
|
|
"partnerguy": true,
|
|
"goodserialguy": true,
|
|
})
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUserFromAssertionAllKnown(c *check.C) {
|
|
expectSudoer := false
|
|
s.testPostCreateUserFromAssertion(c, `{"known":true}`, expectSudoer)
|
|
}
|
|
|
|
func (s *userSuite) TestPostCreateUserFromAssertionAllAutomatic(c *check.C) {
|
|
// automatic implies "sudoder"
|
|
expectSudoer := true
|
|
s.testPostCreateUserFromAssertion(c, `{"automatic":true}`, expectSudoer)
|
|
}
|