Skip to content

Commit

Permalink
asserts,daemon: add support for "serials" field in system-user assertion
Browse files Browse the repository at this point in the history
This will allow to hand out system-user assertions limited to a
limited set of serial assertions.

Implements the spec in https://forum.snapcraft.io/t/18163
  • Loading branch information
mvo5 committed Jun 15, 2020
1 parent 591b619 commit 1d20b3c
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 11 deletions.
3 changes: 3 additions & 0 deletions asserts/asserts.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ func init() {
// 3: support for on-store/on-brand/on-model device scope constraints
// 4: support for plug-names/slot-names constraints
maxSupportedFormat[SnapDeclarationType.Name] = 4

// 1: support to limit to device serials
maxSupportedFormat[SystemUserType.Name] = 1
}

func MockMaxSupportedFormat(assertType *AssertionType, maxFormat int) (restore func()) {
Expand Down
3 changes: 3 additions & 0 deletions asserts/asserts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,13 @@ func (as *assertsSuite) TestTypeNames(c *C) {

func (as *assertsSuite) TestMaxSupportedFormats(c *C) {
snapDeclMaxFormat := asserts.SnapDeclarationType.MaxSupportedFormat()
systemUserMaxFormat := asserts.SystemUserType.MaxSupportedFormat()
// sanity
c.Check(snapDeclMaxFormat >= 4, Equals, true)
c.Check(systemUserMaxFormat >= 1, Equals, true)
c.Check(asserts.MaxSupportedFormats(1), DeepEquals, map[string]int{
"snap-declaration": snapDeclMaxFormat,
"system-user": systemUserMaxFormat,
"test-only": 1,
})

Expand Down
18 changes: 18 additions & 0 deletions asserts/system_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type SystemUser struct {
assertionBase
series []string
models []string
serials []string
sshKeys []string
since time.Time
until time.Time
Expand Down Expand Up @@ -63,6 +64,11 @@ func (su *SystemUser) Models() []string {
return su.models
}

// Serials returns the serials that this assertion is valid for.
func (su *SystemUser) Serials() []string {
return su.serials
}

// Name returns the full name of the user (e.g. Random Guy).
func (su *SystemUser) Name() string {
return su.HeaderString("name")
Expand Down Expand Up @@ -230,6 +236,17 @@ func assembleSystemUser(assert assertionBase) (Assertion, error) {
if err != nil {
return nil, err
}
serials, err := checkStringList(assert.headers, "serials")
if err != nil {
return nil, err
}
if len(serials) > 0 && assert.Format() < 1 {
return nil, fmt.Errorf(`the "serials" header is only available for format 1 or greater`)
}
if len(serials) > 0 && len(models) != 1 {
return nil, fmt.Errorf(`the "serials" header must specify exactly one model`)
}

if _, err := checkOptionalString(assert.headers, "name"); err != nil {
return nil, err
}
Expand Down Expand Up @@ -273,6 +290,7 @@ func assembleSystemUser(assert assertionBase) (Assertion, error) {
assertionBase: assert,
series: series,
models: models,
serials: serials,
sshKeys: sshKeys,
since: since,
until: until,
Expand Down
41 changes: 41 additions & 0 deletions asserts/system_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ type systemUserSuite struct {
since time.Time
sinceLine string

formatLine string
modelsLine string

systemUserStr string
}

const systemUserExample = "type: system-user\n" +
"FORMATLINE\n" +
"authority-id: canonical\n" +
"brand-id: canonical\n" +
"email: foo@example.com\n" +
Expand All @@ -68,9 +70,11 @@ func (s *systemUserSuite) SetUpTest(c *C) {
s.until = time.Now().AddDate(0, 1, 0).Truncate(time.Second)
s.untilLine = fmt.Sprintf("until: %s\n", s.until.Format(time.RFC3339))
s.modelsLine = "models:\n - frobinator\n"
s.formatLine = "format: 0\n"
s.systemUserStr = strings.Replace(systemUserExample, "UNTILLINE\n", s.untilLine, 1)
s.systemUserStr = strings.Replace(s.systemUserStr, "SINCELINE\n", s.sinceLine, 1)
s.systemUserStr = strings.Replace(s.systemUserStr, "MODELSLINE\n", s.modelsLine, 1)
s.systemUserStr = strings.Replace(s.systemUserStr, "FORMATLINE\n", s.formatLine, 1)
}

func (s *systemUserSuite) TestDecodeOK(c *C) {
Expand Down Expand Up @@ -183,6 +187,9 @@ func (s *systemUserSuite) TestDecodeInvalid(c *C) {
{s.untilLine, "until: \n", `"until" header should not be empty`},
{s.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`},
{s.untilLine, "until: 1002-11-01T22:08:41+00:00\n", `'until' time cannot be before 'since' time`},
{s.modelsLine, s.modelsLine + "serials: \n", `"serials" header must be a list of strings`},
{s.modelsLine, s.modelsLine + "serials: something\n", `"serials" header must be a list of strings`},
{s.modelsLine, s.modelsLine + "serials:\n - 7c7f435d-ed28-4281-bd77-e271e0846904\n", `the "serials" header is only available for format 1 or greater`},
}

for _, test := range invalidTests {
Expand Down Expand Up @@ -212,3 +219,37 @@ func (s *systemUserSuite) TestUntilWithModels(c *C) {
_, err := asserts.Decode([]byte(su))
c.Check(err, IsNil)
}

// The following tests deal with "format: 1" which adds support for
// tying system-user assertions to device serials.

var serialsLine = "serials:\n - 7c7f435d-ed28-4281-bd77-e271e0846904\n"

func (s *systemUserSuite) TestDecodeInvalidFormat1(c *C) {
s.systemUserStr = strings.Replace(s.systemUserStr, s.formatLine, "format: 1\n", 1)
serialWithMultipleModels := "models:\n - m1\n - m2\n" + serialsLine

invalidTests := []struct{ original, invalid, expectedErr string }{
{s.modelsLine, serialWithMultipleModels, `the "serials" header must specify exactly one model`},
}
for _, test := range invalidTests {
invalid := strings.Replace(s.systemUserStr, test.original, test.invalid, 1)
_, err := asserts.Decode([]byte(invalid))
c.Check(err, ErrorMatches, systemUserErrPrefix+test.expectedErr)
}
}

func (s *systemUserSuite) TestDecodeOKFormat1(c *C) {
s.systemUserStr = strings.Replace(s.systemUserStr, s.formatLine, "format: 1\n", 1)

s.systemUserStr = strings.Replace(s.systemUserStr, s.modelsLine, s.modelsLine+serialsLine, 1)
a, err := asserts.Decode([]byte(s.systemUserStr))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.SystemUserType)
systemUser := a.(*asserts.SystemUser)
// just for sanity, already covered by "format: 0" tests
c.Check(systemUser.BrandID(), Equals, "canonical")
// new in "format: 1"
c.Check(systemUser.Serials(), DeepEquals, []string{"7c7f435d-ed28-4281-bd77-e271e0846904"})

}
29 changes: 24 additions & 5 deletions daemon/api_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,18 @@ func createUser(c *Command, createData postUserCreateData) Response {
return InternalError("cannot create user: cannot get model assertion: %v", err)
}
}
var serial *asserts.Serial
st.Lock()
serial, err = c.d.overlord.DeviceManager().Serial()
st.Unlock()
if err != nil && err != state.ErrNoState {
return InternalError("cannot create user: cannot get serial: %v", err)
}

// special case: the user requested the creation of all known
// system-users
if createData.Email == "" && createKnown {
return createAllKnownSystemUsers(st, model, &createData)
return createAllKnownSystemUsers(st, model, serial, &createData)
}
if createData.Email == "" {
return BadRequest("cannot create user: 'email' field is empty")
Expand All @@ -346,7 +353,7 @@ func createUser(c *Command, createData postUserCreateData) Response {
var username string
var opts *osutil.AddUserOptions
if createKnown {
username, opts, err = getUserDetailsFromAssertion(st, model, createData.Email)
username, opts, err = getUserDetailsFromAssertion(st, model, serial, createData.Email)
} else {
username, opts, err = getUserDetailsFromStore(getStore(c), createData.Email)
}
Expand Down Expand Up @@ -396,7 +403,7 @@ func getUserDetailsFromStore(theStore snapstate.StoreService, email string) (str
return v.Username, opts, nil
}

func createAllKnownSystemUsers(st *state.State, modelAs *asserts.Model, createData *postUserCreateData) Response {
func createAllKnownSystemUsers(st *state.State, modelAs *asserts.Model, serialAs *asserts.Serial, createData *postUserCreateData) Response {
var createdUsers []userResponseData
headers := map[string]string{
"brand-id": modelAs.BrandID(),
Expand All @@ -414,7 +421,7 @@ func createAllKnownSystemUsers(st *state.State, modelAs *asserts.Model, createDa
email := as.(*asserts.SystemUser).Email()
// we need to use getUserDetailsFromAssertion as this verifies
// the assertion against the current brand/model/time
username, opts, err := getUserDetailsFromAssertion(st, modelAs, email)
username, opts, err := getUserDetailsFromAssertion(st, modelAs, serialAs, email)
if err != nil {
logger.Noticef("ignoring system-user assertion for %q: %s", email, err)
continue
Expand Down Expand Up @@ -443,7 +450,7 @@ func createAllKnownSystemUsers(st *state.State, modelAs *asserts.Model, createDa
return SyncResponse(createdUsers, nil)
}

func getUserDetailsFromAssertion(st *state.State, modelAs *asserts.Model, email string) (string, *osutil.AddUserOptions, error) {
func getUserDetailsFromAssertion(st *state.State, modelAs *asserts.Model, serialAs *asserts.Serial, email string) (string, *osutil.AddUserOptions, error) {
errorPrefix := fmt.Sprintf("cannot add system-user %q: ", email)

st.Lock()
Expand Down Expand Up @@ -477,6 +484,18 @@ func getUserDetailsFromAssertion(st *state.State, modelAs *asserts.Model, email
if len(su.Models()) > 0 && !strutil.ListContains(su.Models(), model) {
return "", nil, fmt.Errorf(errorPrefix+"%q not in models %q", model, su.Models())
}
// XXX: should we really be this paranoid here, format check
// is already done on the assertion level
if len(su.Serials()) > 0 && su.Format() > 0 {
if serialAs == nil {
return "", nil, fmt.Errorf(errorPrefix + "bound to serial assertion but no serial assertion found for device")
}
serial := serialAs.Serial()
if !strutil.ListContains(su.Serials(), serial) {
return "", nil, fmt.Errorf(errorPrefix+"%q not in serials %q", serial, su.Serials())
}
}

if !su.ValidAt(time.Now()) {
return "", nil, fmt.Errorf(errorPrefix + "assertion not valid anymore")
}
Expand Down
91 changes: 85 additions & 6 deletions daemon/api_users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,19 @@ func (s *userSuite) makeSystemUsers(c *check.C, systemUsers []map[string]interfa
})
// 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())
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, "")
assertstatetest.AddMany(st, serial)

for _, suMap := range systemUsers {
su, err := s.brands.Signing(suMap["authority-id"].(string)).Sign(asserts.SystemUserType, suMap, nil, "")
Expand All @@ -393,7 +406,7 @@ func (s *userSuite) makeSystemUsers(c *check.C, systemUsers []map[string]interfa
assertstatetest.AddMany(st, su)
}
// create fake device
err := devicestatetest.SetDevice(st, &auth.DeviceState{
err = devicestatetest.SetDevice(st, &auth.DeviceState{
Brand: "my-brand",
Model: "my-model",
Serial: "serialserial",
Expand Down Expand Up @@ -427,6 +440,21 @@ var partnerUser = map[string]interface{}{
"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",
Expand All @@ -441,6 +469,21 @@ var badUser = map[string]interface{}{
"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",
Expand All @@ -466,7 +509,7 @@ func (s *userSuite) TestGetUserDetailsFromAssertionHappy(c *check.C) {

// ensure that if we query the details from the assert DB we get
// the expected user
username, opts, err := getUserDetailsFromAssertion(st, model, "foo@bar.com")
username, opts, err := getUserDetailsFromAssertion(st, model, nil, "foo@bar.com")
c.Check(username, check.Equals, "guy")
c.Check(opts, check.DeepEquals, &osutil.AddUserOptions{
Gecos: "foo@bar.com,Boring Guy",
Expand Down Expand Up @@ -567,7 +610,7 @@ func (s *userSuite) TestPostCreateUserFromAssertionWithForcePasswordChange(c *ch
}

func (s *userSuite) TestPostCreateUserFromAssertionAllKnown(c *check.C) {
s.makeSystemUsers(c, []map[string]interface{}{goodUser, partnerUser, badUser, unknownUser})
s.makeSystemUsers(c, []map[string]interface{}{goodUser, partnerUser, serialUser, badUser, badUserNoMatchingSerial, unknownUser})
created := map[string]bool{}
// mock the calls that create the user
osutilAddUser = func(username string, opts *osutil.AddUserOptions) error {
Expand All @@ -576,6 +619,8 @@ func (s *userSuite) TestPostCreateUserFromAssertionAllKnown(c *check.C) {
c.Check(opts.Gecos, check.Equals, "foo@bar.com,Boring Guy")
case "partnerguy":
c.Check(opts.Gecos, check.Equals, "p@partner.com,Partner Guy")
case "goodserialguy":
c.Check(opts.Gecos, check.Equals, "serial@bar.com,Serial Guy")
default:
c.Logf("unexpected username %q", username)
c.Fail()
Expand Down Expand Up @@ -611,8 +656,9 @@ func (s *userSuite) TestPostCreateUserFromAssertionAllKnown(c *check.C) {
c.Check(u, check.DeepEquals, userResponseData{Username: u.Username})
}
c.Check(seen, check.DeepEquals, map[string]bool{
"guy": true,
"partnerguy": true,
"guy": true,
"partnerguy": true,
"goodserialguy": true,
})

// ensure the user was added to the state
Expand All @@ -621,7 +667,7 @@ func (s *userSuite) TestPostCreateUserFromAssertionAllKnown(c *check.C) {
users, err := auth.Users(st)
c.Assert(err, check.IsNil)
st.Unlock()
c.Check(users, check.HasLen, 2)
c.Check(users, check.HasLen, 3)
}

func (s *userSuite) TestPostCreateUserFromAssertionAllKnownClassicErrors(c *check.C) {
Expand Down Expand Up @@ -683,6 +729,39 @@ func (s *userSuite) TestPostCreateUserFromAssertionAllKnownNoModelError(c *check
c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user: cannot get model assertion: no state entry for key`)
}

func (s *userSuite) TestPostCreateUserFromAssertionNoModel(c *check.C) {
restore := release.MockOnClassic(false)
defer restore()

model := s.brands.Model("my-brand", "other-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc",
"kernel": "pc-kernel",
"system-user-authority": []interface{}{"my-brand", "partner"},
})
s.makeSystemUsers(c, []map[string]interface{}{serialUser})

st := s.d.overlord.State()
st.Lock()
assertstatetest.AddMany(st, model)
err := devicestatetest.SetDevice(st, &auth.DeviceState{
Brand: "my-brand",
Model: "my-model",
Serial: "other-serial-assertion",
})
st.Unlock()

// do it!
buf := bytes.NewBufferString(`{"email":"serial@bar.com", "known":true}`)
req, err := http.NewRequest("POST", "/v2/create-user", buf)
c.Assert(err, check.IsNil)

rsp := postCreateUser(createUserCmd, req, nil).(*resp)

c.Check(rsp.Type, check.Equals, ResponseTypeError)
c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot add system-user "serial@bar.com": bound to serial assertion but no serial assertion found for device`)
}

func (s *userSuite) TestPostCreateUserFromAssertionAllKnownButOwned(c *check.C) {
s.makeSystemUsers(c, []map[string]interface{}{goodUser})

Expand Down

0 comments on commit 1d20b3c

Please sign in to comment.