Files
snapd/asserts/model_test.go
2024-03-22 19:06:32 +00:00

1734 lines
60 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016-2023 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 asserts_test
import (
"fmt"
"strings"
"time"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap/naming"
"github.com/snapcore/snapd/testutil"
)
type modelSuite struct {
ts time.Time
tsLine string
}
var (
_ = Suite(&modelSuite{})
)
func (mods *modelSuite) SetUpSuite(c *C) {
mods.ts = time.Now().Truncate(time.Second).UTC()
mods.tsLine = "timestamp: " + mods.ts.Format(time.RFC3339) + "\n"
}
const (
reqSnaps = "required-snaps:\n - foo\n - bar\n"
sysUserAuths = "system-user-authority: *\n"
serialAuths = "serial-authority:\n - generic\n"
preseedAuths = "preseed-authority:\n - preseed-delegate\n"
)
const (
modelExample = "type: model\n" +
"authority-id: brand-id1\n" +
"series: 16\n" +
"brand-id: brand-id1\n" +
"model: baz-3000\n" +
"display-name: Baz 3000\n" +
"architecture: amd64\n" +
"gadget: brand-gadget\n" +
"base: core18\n" +
"kernel: baz-linux\n" +
"store: brand-store\n" +
serialAuths +
sysUserAuths +
reqSnaps +
preseedAuths +
"TSLINE" +
"body-length: 0\n" +
"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
"\n\n" +
"AXNpZw=="
classicModelExample = "type: model\n" +
"authority-id: brand-id1\n" +
"series: 16\n" +
"brand-id: brand-id1\n" +
"model: baz-3000\n" +
"display-name: Baz 3000\n" +
"classic: true\n" +
"architecture: amd64\n" +
"gadget: brand-gadget\n" +
"store: brand-store\n" +
reqSnaps +
"TSLINE" +
"body-length: 0\n" +
"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
"\n\n" +
"AXNpZw=="
core20ModelExample = `type: model
authority-id: brand-id1
series: 16
brand-id: brand-id1
model: baz-3000
display-name: Baz 3000
architecture: amd64
system-user-authority: *
base: core20
store: brand-store
snaps:
-
name: baz-linux
id: bazlinuxidididididididididididid
type: kernel
default-channel: 20
-
name: brand-gadget
id: brandgadgetdidididididididididid
type: gadget
-
name: other-base
id: otherbasedididididididididididid
type: base
modes:
- run
presence: required
-
name: nm
id: nmididididididididididididididid
modes:
- ephemeral
- run
default-channel: 1.0
-
name: myapp
id: myappdididididididididididididid
type: app
default-channel: 2.0
-
name: myappopt
id: myappoptidididididididididididid
type: app
presence: optional
OTHERgrade: secured
storage-safety: encrypted
` + "TSLINE" +
"body-length: 0\n" +
"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
"\n\n" +
"AXNpZw=="
classicModelWithSnapsExample = `type: model
authority-id: brand-id1
series: 16
brand-id: brand-id1
model: baz-3000
display-name: Baz 3000
architecture: amd64
system-user-authority: *
base: core20
classic: true
distribution: ubuntu
store: brand-store
snaps:
-
name: baz-linux
id: bazlinuxidididididididididididid
type: kernel
default-channel: 20
-
name: brand-gadget
id: brandgadgetdidididididididididid
type: gadget
-
name: other-base
id: otherbasedididididididididididid
type: base
modes:
- run
presence: required
-
name: nm
id: nmididididididididididididididid
modes:
- ephemeral
- run
default-channel: 1.0
-
name: myapp
id: myappdididididididididididididid
type: app
default-channel: 2.0
-
name: myappopt
id: myappoptidididididididididididid
type: app
presence: optional
classic: true
OTHERgrade: secured
storage-safety: encrypted
` + "TSLINE" +
"body-length: 0\n" +
"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
"\n\n" +
"AXNpZw=="
coreModelWithComponentsExample = `type: model
authority-id: brand-id1
series: 16
brand-id: brand-id1
model: baz-3000
display-name: Baz 3000
architecture: amd64
system-user-authority: *
base: core24
store: brand-store
snaps:
-
name: baz-linux
id: bazlinuxidididididididididididid
type: kernel
default-channel: 20
-
name: brand-gadget
id: brandgadgetdidididididididididid
type: gadget
-
name: other-base
id: otherbasedididididididididididid
type: base
presence: required
-
name: nm
id: nmididididididididididididididid
modes:
- ephemeral
- run
default-channel: 1.0
components:
comp1:
presence: optional
modes:
- ephemeral
comp2: required
-
name: myapp
id: myappdididididididididididididid
type: app
default-channel: 2.0
presence: optional
modes:
- ephemeral
- run
components:
comp1:
presence: optional
modes:
- ephemeral
- run
comp2: required
-
name: myappopt
id: myappoptidididididididididididid
type: app
presence: required
components:
comp1:
presence: optional
comp2: required
OTHERgrade: secured
storage-safety: encrypted
` + "TSLINE" +
"body-length: 0\n" +
"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
"\n\n" +
"AXNpZw=="
)
func (mods *modelSuite) TestDecodeOK(c *C) {
encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.AuthorityID(), Equals, "brand-id1")
c.Check(model.Timestamp(), Equals, mods.ts)
c.Check(model.Series(), Equals, "16")
c.Check(model.BrandID(), Equals, "brand-id1")
c.Check(model.Model(), Equals, "baz-3000")
c.Check(model.DisplayName(), Equals, "Baz 3000")
c.Check(model.Architecture(), Equals, "amd64")
c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
Name: "brand-gadget",
SnapType: "gadget",
Modes: []string{"run"},
Presence: "required",
})
c.Check(model.Gadget(), Equals, "brand-gadget")
c.Check(model.GadgetTrack(), Equals, "")
c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{
Name: "baz-linux",
SnapType: "kernel",
Modes: []string{"run"},
Presence: "required",
})
c.Check(model.Kernel(), Equals, "baz-linux")
c.Check(model.KernelTrack(), Equals, "")
c.Check(model.Base(), Equals, "core18")
c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{
Name: "core18",
SnapType: "base",
Modes: []string{"run"},
Presence: "required",
})
c.Check(model.Store(), Equals, "brand-store")
c.Check(model.Grade(), Equals, asserts.ModelGradeUnset)
c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyUnset)
essentialSnaps := model.EssentialSnaps()
c.Check(essentialSnaps, DeepEquals, []*asserts.ModelSnap{
model.KernelSnap(),
model.BaseSnap(),
model.GadgetSnap(),
})
snaps := model.SnapsWithoutEssential()
c.Check(snaps, DeepEquals, []*asserts.ModelSnap{
{
Name: "foo",
Modes: []string{"run"},
Presence: "required",
},
{
Name: "bar",
Modes: []string{"run"},
Presence: "required",
},
})
// essential snaps included
reqSnaps := naming.NewSnapSet(model.RequiredWithEssentialSnaps())
for _, e := range essentialSnaps {
c.Check(reqSnaps.Contains(e), Equals, true)
}
for _, s := range snaps {
c.Check(reqSnaps.Contains(s), Equals, true)
}
c.Check(reqSnaps.Size(), Equals, len(essentialSnaps)+len(snaps))
// essential snaps excluded
noEssential := naming.NewSnapSet(model.RequiredNoEssentialSnaps())
for _, e := range essentialSnaps {
c.Check(noEssential.Contains(e), Equals, false)
}
for _, s := range snaps {
c.Check(noEssential.Contains(s), Equals, true)
}
c.Check(noEssential.Size(), Equals, len(snaps))
c.Check(model.SystemUserAuthority(), HasLen, 0)
c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1", "generic"})
c.Check(model.PreseedAuthority(), DeepEquals, []string{"brand-id1", "preseed-delegate"})
}
func (mods *modelSuite) TestDecodeStoreIsOptional(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, "store: brand-store\n", "store: \n", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
c.Check(model.Store(), Equals, "")
encoded = strings.Replace(withTimestamp, "store: brand-store\n", "", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model = a.(*asserts.Model)
c.Check(model.Store(), Equals, "")
}
func (mods *modelSuite) TestDecodeBaseIsOptional(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, "base: core18\n", "base: \n", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
c.Check(model.Base(), Equals, "")
encoded = strings.Replace(withTimestamp, "base: core18\n", "", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model = a.(*asserts.Model)
c.Check(model.Base(), Equals, "")
c.Check(model.BaseSnap(), IsNil)
}
func (mods *modelSuite) TestDecodeDisplayNameIsOptional(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, "display-name: Baz 3000\n", "display-name: \n", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
// optional but we fallback to Model
c.Check(model.DisplayName(), Equals, "baz-3000")
encoded = strings.Replace(withTimestamp, "display-name: Baz 3000\n", "", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model = a.(*asserts.Model)
// optional but we fallback to Model
c.Check(model.DisplayName(), Equals, "baz-3000")
}
func (mods *modelSuite) TestDecodeRequiredSnapsAreOptional(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, reqSnaps, "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
c.Check(model.RequiredNoEssentialSnaps(), HasLen, 0)
}
func (mods *modelSuite) TestDecodeValidatesSnapNames(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n - foo_bar\n - bar\n", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(a, IsNil)
c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: foo_bar`)
encoded = strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n - foo\n - bar-;;''\n", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(a, IsNil)
c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: bar-;;''`)
encoded = strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux_instance\n", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(a, IsNil)
c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: baz-linux_instance`)
encoded = strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget_instance\n", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(a, IsNil)
c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "gadget" header: brand-gadget_instance`)
encoded = strings.Replace(withTimestamp, "base: core18\n", "base: core18_instance\n", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(a, IsNil)
c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "base" header: core18_instance`)
}
func (mods modelSuite) TestDecodeValidSnapNames(c *C) {
// reuse test cases for snap.ValidateName()
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
validNames := []string{
"aa", "aaa", "aaaa",
"a-a", "aa-a", "a-aa", "a-b-c",
"a0", "a-0", "a-0a",
"01game", "1-or-2",
// a regexp stresser
"u-94903713687486543234157734673284536758",
}
for _, name := range validNames {
encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
c.Check(model.Kernel(), Equals, name)
}
invalidNames := []string{
// name cannot be empty, never reaches snap name validation
"",
// too short (min 2 chars)
"a",
// names cannot be too long
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx",
"1111111111111111111111111111111111111111x",
"x1111111111111111111111111111111111111111",
"x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x",
// a regexp stresser
"u-9490371368748654323415773467328453675-",
// dashes alone are not a name
"-", "--",
// double dashes in a name are not allowed
"a--a",
// name should not end with a dash
"a-",
// name cannot have any spaces in it
"a ", " a", "a a",
// a number alone is not a name
"0", "123",
// identifier must be plain ASCII
"日本語", "한글", "ру́сский язы́к",
// instance names are invalid too
"foo_bar", "x_1",
}
for _, name := range invalidNames {
encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(a, IsNil)
if name != "" {
c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: .*`)
} else {
c.Assert(err, ErrorMatches, `assertion model: "kernel" header should not be empty`)
}
}
}
func (mods *modelSuite) TestDecodeSerialAuthorityIsOptional(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, serialAuths, "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
// the default is just to accept the brand itself
c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1"})
encoded = strings.Replace(withTimestamp, serialAuths, "serial-authority:\n - foo\n - bar\n", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model = a.(*asserts.Model)
// the brand is always added implicitly
c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1", "foo", "bar"})
}
func (mods *modelSuite) TestDecodeSystemUserAuthorityIsOptional(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, sysUserAuths, "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
// the default is just to accept the brand itself
c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1"})
encoded = strings.Replace(withTimestamp, sysUserAuths, "system-user-authority:\n - foo\n - bar\n", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model = a.(*asserts.Model)
// the brand is always added implicitly, it can always sign
// a new revision of the model anyway
c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1", "foo", "bar"})
}
func (mods *modelSuite) TestDecodePreseedAuthorityIsOptional(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, preseedAuths, "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
// the default is just to accept the brand itself
c.Check(model.PreseedAuthority(), DeepEquals, []string{"brand-id1"})
encoded = strings.Replace(withTimestamp, preseedAuths, "preseed-authority:\n - foo\n - bar\n", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model = a.(*asserts.Model)
// the brand is always added implicitly
c.Check(model.PreseedAuthority(), DeepEquals, []string{"brand-id1", "foo", "bar"})
}
func (mods *modelSuite) TestDecodeKernelTrack(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux=18\n", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{
Name: "baz-linux",
SnapType: "kernel",
Modes: []string{"run"},
PinnedTrack: "18",
Presence: "required",
})
c.Check(model.Kernel(), Equals, "baz-linux")
c.Check(model.KernelTrack(), Equals, "18")
}
func (mods *modelSuite) TestDecodeGadgetTrack(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget=18\n", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
Name: "brand-gadget",
SnapType: "gadget",
Modes: []string{"run"},
PinnedTrack: "18",
Presence: "required",
})
c.Check(model.Gadget(), Equals, "brand-gadget")
c.Check(model.GadgetTrack(), Equals, "18")
}
const (
modelErrPrefix = "assertion model: "
)
func (mods *modelSuite) TestDecodeInvalid(c *C) {
encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
invalidTests := []struct{ original, invalid, expectedErr string }{
{"series: 16\n", "", `"series" header is mandatory`},
{"series: 16\n", "series: \n", `"series" header should not be empty`},
{"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
{"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
{"brand-id: brand-id1\n", "brand-id: random\n", `authority-id and brand-id must match, model assertions are expected to be signed by the brand: "brand-id1" != "random"`},
{"model: baz-3000\n", "", `"model" header is mandatory`},
{"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
{"model: baz-3000\n", "model: baz/3000\n", `"model" primary key header cannot contain '/'`},
// lift this restriction at a later point
{"model: baz-3000\n", "model: BAZ-3000\n", `"model" header cannot contain uppercase letters`},
{"display-name: Baz 3000\n", "display-name:\n - xyz\n", `"display-name" header must be a string`},
{"architecture: amd64\n", "", `"architecture" header is mandatory`},
{"architecture: amd64\n", "architecture: \n", `"architecture" header should not be empty`},
{"gadget: brand-gadget\n", "", `"gadget" header is mandatory`},
{"gadget: brand-gadget\n", "gadget: \n", `"gadget" header should not be empty`},
{"gadget: brand-gadget\n", "gadget: brand-gadget=x/x/x\n", `"gadget" channel selector must be a track name only`},
{"gadget: brand-gadget\n", "gadget: brand-gadget=stable\n", `"gadget" channel selector must be a track name`},
{"gadget: brand-gadget\n", "gadget: brand-gadget=18/beta\n", `"gadget" channel selector must be a track name only`},
{"gadget: brand-gadget\n", "gadget:\n - xyz \n", `"gadget" header must be a string`},
{"kernel: baz-linux\n", "", `"kernel" header is mandatory`},
{"kernel: baz-linux\n", "kernel: \n", `"kernel" header should not be empty`},
{"kernel: baz-linux\n", "kernel: baz-linux=x/x/x\n", `"kernel" channel selector must be a track name only`},
{"kernel: baz-linux\n", "kernel: baz-linux=stable\n", `"kernel" channel selector must be a track name`},
{"kernel: baz-linux\n", "kernel: baz-linux=18/beta\n", `"kernel" channel selector must be a track name only`},
{"kernel: baz-linux\n", "kernel:\n - xyz \n", `"kernel" header must be a string`},
{"base: core18\n", "base:\n - xyz \n", `"base" header must be a string`},
{"store: brand-store\n", "store:\n - xyz\n", `"store" header must be a string`},
{mods.tsLine, "", `"timestamp" header is mandatory`},
{mods.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
{mods.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
{reqSnaps, "required-snaps: foo\n", `"required-snaps" header must be a list of strings`},
{reqSnaps, "required-snaps:\n -\n - nested\n", `"required-snaps" header must be a list of strings`},
{serialAuths, "serial-authority:\n a: 1\n", `"serial-authority" header must be a list of account ids`},
{serialAuths, "serial-authority:\n - 5_6\n", `"serial-authority" header must be a list of account ids`},
{serialAuths, "serial-authority: *\n", `"serial-authority" header must be a list of account ids`},
{sysUserAuths, "system-user-authority:\n a: 1\n", `"system-user-authority" header must be '\*' or a list of account ids`},
{sysUserAuths, "system-user-authority:\n - 5_6\n", `"system-user-authority" header must be '\*' or a list of account ids`},
{preseedAuths, "preseed-authority:\n a: 1\n", `"preseed-authority" header must be a list of account ids`},
{preseedAuths, "preseed-authority:\n - 5_6\n", `"preseed-authority" header must be a list of account ids`},
{preseedAuths, "preseed-authority: *\n", `"preseed-authority" header must be a list of account ids`},
{reqSnaps, "grade: dangerous\n", `cannot specify a grade for model without the extended snaps header`},
}
for _, test := range invalidTests {
invalid := strings.Replace(encoded, test.original, test.invalid, 1)
_, err := asserts.Decode([]byte(invalid))
c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
}
}
func (mods *modelSuite) TestModelCheck(c *C) {
ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)))
c.Assert(err, IsNil)
storeDB, db := makeStoreAndCheckDB(c)
brandDB := setup3rdPartySigning(c, "brand-id1", storeDB, db)
headers := ex.Headers()
headers["brand-id"] = brandDB.AuthorityID
headers["timestamp"] = time.Now().Format(time.RFC3339)
model, err := brandDB.Sign(asserts.ModelType, headers, nil, "")
c.Assert(err, IsNil)
err = db.Check(model)
c.Assert(err, IsNil)
}
func (mods *modelSuite) TestModelCheckInconsistentTimestamp(c *C) {
ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)))
c.Assert(err, IsNil)
storeDB, db := makeStoreAndCheckDB(c)
brandDB := setup3rdPartySigning(c, "brand-id1", storeDB, db)
headers := ex.Headers()
headers["brand-id"] = brandDB.AuthorityID
headers["timestamp"] = "2011-01-01T14:00:00Z"
model, err := brandDB.Sign(asserts.ModelType, headers, nil, "")
c.Assert(err, IsNil)
err = db.Check(model)
c.Assert(err, ErrorMatches, `model assertion timestamp "2011-01-01 14:00:00 \+0000 UTC" outside of signing key validity \(key valid since.*\)`)
}
func (mods *modelSuite) TestClassicDecodeOK(c *C) {
encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.AuthorityID(), Equals, "brand-id1")
c.Check(model.Timestamp(), Equals, mods.ts)
c.Check(model.Series(), Equals, "16")
c.Check(model.BrandID(), Equals, "brand-id1")
c.Check(model.Model(), Equals, "baz-3000")
c.Check(model.DisplayName(), Equals, "Baz 3000")
c.Check(model.Classic(), Equals, true)
c.Check(model.Distribution(), Equals, "")
c.Check(model.Architecture(), Equals, "amd64")
c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
Name: "brand-gadget",
SnapType: "gadget",
Modes: []string{"run"},
Presence: "required",
})
c.Check(model.Gadget(), Equals, "brand-gadget")
c.Check(model.KernelSnap(), IsNil)
c.Check(model.Kernel(), Equals, "")
c.Check(model.KernelTrack(), Equals, "")
c.Check(model.Base(), Equals, "")
c.Check(model.BaseSnap(), IsNil)
c.Check(model.Store(), Equals, "brand-store")
essentialSnaps := model.EssentialSnaps()
c.Check(essentialSnaps, DeepEquals, []*asserts.ModelSnap{
model.GadgetSnap(),
})
snaps := model.SnapsWithoutEssential()
c.Check(snaps, DeepEquals, []*asserts.ModelSnap{
{
Name: "foo",
Modes: []string{"run"},
Presence: "required",
},
{
Name: "bar",
Modes: []string{"run"},
Presence: "required",
},
})
// gadget included
reqSnaps := naming.NewSnapSet(model.RequiredWithEssentialSnaps())
for _, e := range essentialSnaps {
c.Check(reqSnaps.Contains(e), Equals, true)
}
for _, s := range snaps {
c.Check(reqSnaps.Contains(s), Equals, true)
}
c.Check(reqSnaps.Size(), Equals, len(essentialSnaps)+len(snaps))
// gadget excluded
noEssential := naming.NewSnapSet(model.RequiredNoEssentialSnaps())
for _, e := range essentialSnaps {
c.Check(noEssential.Contains(e), Equals, false)
}
for _, s := range snaps {
c.Check(noEssential.Contains(s), Equals, true)
}
c.Check(noEssential.Size(), Equals, len(snaps))
}
func (mods *modelSuite) TestClassicDecodeInvalid(c *C) {
encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
invalidTests := []struct{ original, invalid, expectedErr string }{
{"classic: true\n", "classic: foo\n", `"classic" header must be 'true' or 'false'`},
{"architecture: amd64\n", "architecture:\n - foo\n", `"architecture" header must be a string`},
{"gadget: brand-gadget\n", "gadget:\n - foo\n", `"gadget" header must be a string`},
{"gadget: brand-gadget\n", "kernel: brand-kernel\n", `cannot specify a kernel with a non-extended classic model`},
{"gadget: brand-gadget\n", "base: some-base\n", `cannot specify a base with a non-extended classic model`},
{"gadget: brand-gadget\n", "gadget:\n - xyz\n", `"gadget" header must be a string`},
}
for _, test := range invalidTests {
invalid := strings.Replace(encoded, test.original, test.invalid, 1)
_, err := asserts.Decode([]byte(invalid))
c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
}
}
func (mods *modelSuite) TestClassicDecodeGadgetAndArchOptional(c *C) {
encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "gadget: brand-gadget\n", "", 1)
encoded = strings.Replace(encoded, "architecture: amd64\n", "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.Classic(), Equals, true)
c.Check(model.Distribution(), Equals, "")
c.Check(model.Architecture(), Equals, "")
c.Check(model.GadgetSnap(), IsNil)
c.Check(model.Gadget(), Equals, "")
c.Check(model.GadgetTrack(), Equals, "")
}
func (mods *modelSuite) TestWithSnapsDecodeOK(c *C) {
tt := []struct {
modelRaw string
isClassic bool
}{
{modelRaw: core20ModelExample, isClassic: false},
{modelRaw: classicModelWithSnapsExample, isClassic: true},
}
for _, t := range tt {
mods.testWithSnapsDecodeOK(c, t.modelRaw, t.isClassic)
}
}
func (mods *modelSuite) testWithSnapsDecodeOK(c *C, modelRaw string, isClassic bool) {
encoded := strings.Replace(modelRaw, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.AuthorityID(), Equals, "brand-id1")
c.Check(model.Timestamp(), Equals, mods.ts)
c.Check(model.Series(), Equals, "16")
c.Check(model.BrandID(), Equals, "brand-id1")
c.Check(model.Model(), Equals, "baz-3000")
c.Check(model.DisplayName(), Equals, "Baz 3000")
c.Check(model.Architecture(), Equals, "amd64")
c.Check(model.Classic(), Equals, isClassic)
if isClassic {
c.Check(model.Distribution(), Equals, "ubuntu")
} else {
c.Check(model.Distribution(), Equals, "")
}
c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
Name: "brand-gadget",
SnapID: "brandgadgetdidididididididididid",
SnapType: "gadget",
Modes: []string{"run", "ephemeral"},
DefaultChannel: "latest/stable",
Presence: "required",
})
c.Check(model.Gadget(), Equals, "brand-gadget")
c.Check(model.GadgetTrack(), Equals, "")
c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{
Name: "baz-linux",
SnapID: "bazlinuxidididididididididididid",
SnapType: "kernel",
Modes: []string{"run", "ephemeral"},
DefaultChannel: "20",
Presence: "required",
})
c.Check(model.Kernel(), Equals, "baz-linux")
c.Check(model.KernelTrack(), Equals, "")
c.Check(model.Base(), Equals, "core20")
c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{
Name: "core20",
SnapID: naming.WellKnownSnapID("core20"),
SnapType: "base",
Modes: []string{"run", "ephemeral"},
DefaultChannel: "latest/stable",
Presence: "required",
})
c.Check(model.Store(), Equals, "brand-store")
c.Check(model.Grade(), Equals, asserts.ModelSecured)
c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyEncrypted)
essentialSnaps := model.EssentialSnaps()
c.Check(essentialSnaps, DeepEquals, []*asserts.ModelSnap{
model.KernelSnap(),
model.BaseSnap(),
model.GadgetSnap(),
})
snaps := model.SnapsWithoutEssential()
c.Check(snaps, DeepEquals, []*asserts.ModelSnap{
{
Name: "other-base",
SnapID: "otherbasedididididididididididid",
SnapType: "base",
Modes: []string{"run"},
DefaultChannel: "latest/stable",
Presence: "required",
},
{
Name: "nm",
SnapID: "nmididididididididididididididid",
SnapType: "app",
Modes: []string{"ephemeral", "run"},
DefaultChannel: "1.0",
Presence: "required",
},
{
Name: "myapp",
SnapID: "myappdididididididididididididid",
SnapType: "app",
Modes: []string{"run"},
DefaultChannel: "2.0",
Presence: "required",
},
{
Name: "myappopt",
SnapID: "myappoptidididididididididididid",
SnapType: "app",
Modes: []string{"run"},
DefaultChannel: "latest/stable",
Presence: "optional",
Classic: isClassic,
},
})
// essential snaps included
reqSnaps := naming.NewSnapSet(model.RequiredWithEssentialSnaps())
for _, e := range essentialSnaps {
c.Check(reqSnaps.Contains(e), Equals, true)
}
for _, s := range snaps {
c.Check(reqSnaps.Contains(s), Equals, s.Presence == "required")
}
c.Check(reqSnaps.Size(), Equals, len(essentialSnaps)+len(snaps)-1)
// essential snaps excluded
noEssential := naming.NewSnapSet(model.RequiredNoEssentialSnaps())
for _, e := range essentialSnaps {
c.Check(noEssential.Contains(e), Equals, false)
}
for _, s := range snaps {
c.Check(noEssential.Contains(s), Equals, s.Presence == "required")
}
c.Check(noEssential.Size(), Equals, len(snaps)-1)
c.Check(model.SystemUserAuthority(), HasLen, 0)
c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1"})
c.Check(model.PreseedAuthority(), DeepEquals, []string{"brand-id1"})
}
func (mods *modelSuite) TestCore20ExplictBootBase(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", ` -
name: core20
id: core20ididididididididididididid
type: base
default-channel: latest/candidate
`, 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{
Name: "core20",
SnapID: "core20ididididididididididididid",
SnapType: "base",
Modes: []string{"run", "ephemeral"},
DefaultChannel: "latest/candidate",
Presence: "required",
})
}
func (mods *modelSuite) TestCore20ExplictSnapd(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", ` -
name: snapd
id: snapdidididididididididididididd
type: snapd
default-channel: latest/edge
`, 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
snapdSnap := model.EssentialSnaps()[0]
c.Check(snapdSnap, DeepEquals, &asserts.ModelSnap{
Name: "snapd",
SnapID: "snapdidididididididididididididd",
SnapType: "snapd",
Modes: []string{"run", "ephemeral"},
DefaultChannel: "latest/edge",
Presence: "required",
})
}
func (mods *modelSuite) TestCore20GradeOptionalDefaultSigned(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", "", 1)
encoded = strings.Replace(encoded, "grade: secured\n", "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.Grade(), Equals, asserts.ModelSigned)
}
func (mods *modelSuite) TestCore20ValidGrades(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", "", 1)
for _, grade := range []string{"signed", "secured", "dangerous"} {
ex := strings.Replace(encoded, "grade: secured\n", fmt.Sprintf("grade: %s\n", grade), 1)
a, err := asserts.Decode([]byte(ex))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(string(model.Grade()), Equals, grade)
}
}
func (mods *modelSuite) TestModelGradeCode(c *C) {
for i, grade := range []asserts.ModelGrade{asserts.ModelGradeUnset, asserts.ModelDangerous, asserts.ModelSigned, asserts.ModelSecured} {
// unset is represented as zero
code := 0
if i > 0 {
// have some space between grades to add new ones
n := (i - 1) * 8
if n == 0 {
n = 1 // dangerous
}
// lower 16 bits are reserved
code = n << 16
}
c.Check(grade.Code(), Equals, uint32(code))
}
}
func (mods *modelSuite) TestCore20GradeDangerous(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", "", 1)
encoded = strings.Replace(encoded, "grade: secured\n", "grade: dangerous\n", 1)
// snap ids are optional with grade dangerous to allow working
// with local/not pushed yet to the store snaps
encoded = strings.Replace(encoded, " id: myappdididididididididididididid\n", "", 1)
encoded = strings.Replace(encoded, " id: brandgadgetdidididididididididid\n", "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.Grade(), Equals, asserts.ModelDangerous)
snaps := model.SnapsWithoutEssential()
c.Check(snaps[len(snaps)-2], DeepEquals, &asserts.ModelSnap{
Name: "myapp",
SnapType: "app",
Modes: []string{"run"},
DefaultChannel: "2.0",
Presence: "required",
})
}
func (mods *modelSuite) TestCore20ValidStorageSafety(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", "", 1)
encoded = strings.Replace(encoded, "grade: secured\n", "grade: signed\n", 1)
for _, tc := range []struct {
ss asserts.StorageSafety
sss string
}{
{asserts.StorageSafetyPreferEncrypted, "prefer-encrypted"},
{asserts.StorageSafetyPreferUnencrypted, "prefer-unencrypted"},
{asserts.StorageSafetyEncrypted, "encrypted"},
} {
ex := strings.Replace(encoded, "storage-safety: encrypted\n", fmt.Sprintf("storage-safety: %s\n", tc.sss), 1)
a, err := asserts.Decode([]byte(ex))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.StorageSafety(), Equals, tc.ss)
}
}
func (mods *modelSuite) TestCore20DefaultStorageSafetySecured(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", "", 1)
ex := strings.Replace(encoded, "storage-safety: encrypted\n", "", 1)
a, err := asserts.Decode([]byte(ex))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyEncrypted)
}
func (mods *modelSuite) TestCore20DefaultStorageSafetySignedDangerous(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", "", 1)
encoded = strings.Replace(encoded, "storage-safety: encrypted\n", "", 1)
for _, grade := range []string{"dangerous", "signed"} {
ex := strings.Replace(encoded, "grade: secured\n", fmt.Sprintf("grade: %s\n", grade), 1)
a, err := asserts.Decode([]byte(ex))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyPreferEncrypted)
}
}
func (mods *modelSuite) TestWithSnapsDecodeInvalid(c *C) {
tt := []struct {
modelRaw string
isClassic bool
}{
{modelRaw: core20ModelExample, isClassic: false},
{modelRaw: classicModelWithSnapsExample, isClassic: true},
}
for _, t := range tt {
mods.testWithSnapsDecodeInvalid(c, t.modelRaw, t.isClassic)
}
}
func (mods *modelSuite) testWithSnapsDecodeInvalid(c *C, modelRaw string, isClassic bool) {
encoded := strings.Replace(modelRaw, "TSLINE", mods.tsLine, 1)
snapsStanza := encoded[strings.Index(encoded, "snaps:"):strings.Index(encoded, "grade:")]
invalidTests := []struct{ original, invalid, expectedErr string }{
{"base: core20\n", "", `"base" header is mandatory`},
{"base: core20\n", "base: alt-base\n", `cannot specify not well-known base "alt-base" without a corresponding "snaps" header entry`},
{snapsStanza, "snaps: snap\n", `"snaps" header must be a list of maps`},
{snapsStanza, "snaps:\n - snap\n", `"snaps" header must be a list of maps`},
{"name: myapp\n", "other: 1\n", `"name" of snap is mandatory`},
{"name: myapp\n", "name: myapp_2\n", `invalid snap name "myapp_2"`},
{"id: myappdididididididididididididid\n", "id: 2\n", `"id" of snap "myapp" contains invalid characters: "2"`},
{" id: myappdididididididididididididid\n", "", `"id" of snap "myapp" is mandatory for secured grade model`},
{"type: gadget\n", "type:\n - g\n", `"type" of snap "brand-gadget" must be a string`},
{"type: app\n", "type: thing\n", `"type" of snap "myappopt" must be one of must be one of app|base|gadget|kernel|core|snapd`},
{"modes:\n - run\n", "modes: run\n", `"modes" of snap "other-base" must be a list of strings`},
{"default-channel: 20\n", "default-channel: edge\n", `default channel for snap "baz-linux" must specify a track`},
{"default-channel: 2.0\n", "default-channel:\n - x\n", `"default-channel" of snap "myapp" must be a string`},
{"default-channel: 2.0\n", "default-channel: 2.0/xyz/z\n", `invalid default channel for snap "myapp": invalid risk in channel name: 2.0/xyz/z`},
{"presence: optional\n", "presence:\n - opt\n", `"presence" of snap "myappopt" must be a string`},
{"presence: optional\n", "presence: no\n", `"presence" of snap "myappopt" must be one of must be one of required|optional`},
{"OTHER", " -\n name: myapp\n id: myappdididididididididididididid\n", `cannot list the same snap "myapp" multiple times`},
{"OTHER", " -\n name: myapp2\n id: myappdididididididididididididid\n", `cannot specify the same snap id "myappdididididididididididididid" multiple times, specified for snaps "myapp" and "myapp2"`},
{"OTHER", " -\n name: kernel2\n id: kernel2didididididididididididid\n type: kernel\n", `cannot specify multiple kernel snaps: "baz-linux" and "kernel2"`},
{"OTHER", " -\n name: gadget2\n id: gadget2didididididididididididid\n type: gadget\n", `cannot specify multiple gadget snaps: "brand-gadget" and "gadget2"`},
{"type: gadget\n", "type: gadget\n presence: required\n", `essential snaps are always available, cannot specify presence for snap "brand-gadget"`},
{"type: gadget\n", "type: gadget\n modes:\n - run\n", `essential snaps are always available, cannot specify modes of snap "brand-gadget"`},
{"type: kernel\n", "type: kernel\n presence: required\n", `essential snaps are always available, cannot specify presence for snap "baz-linux"`},
{"OTHER", " -\n name: core20\n id: core20ididididididididididididid\n type: base\n presence: optional\n", `essential snaps are always available, cannot specify presence for snap "core20"`},
{"OTHER", " -\n name: core20\n id: core20ididididididididididididid\n type: app\n", `boot base "core20" must specify type "base", not "app"`},
{"OTHER", "kernel: foo\n", `cannot specify separate "kernel" header once using the extended snaps header`},
{"OTHER", "gadget: foo\n", `cannot specify separate "gadget" header once using the extended snaps header`},
{"OTHER", "required-snaps:\n - foo\n", `cannot specify separate "required-snaps" header once using the extended snaps header`},
{"grade: secured\n", "grade: foo\n", `grade for model must be secured|signed|dangerous`},
{"storage-safety: encrypted\n", "storage-safety: foo\n", `storage-safety for model must be encrypted\|prefer-encrypted\|prefer-unencrypted, not "foo"`},
{"storage-safety: encrypted\n", "storage-safety: prefer-unencrypted\n", `secured grade model must not have storage-safety overridden, only "encrypted" is valid`},
}
if isClassic {
classicInvalid := []struct{ original, invalid, expectedErr string }{
{"distribution: ubuntu\n", "", `"distribution" header is mandatory, see distribution ID in os-release spec`},
{"distribution: ubuntu\n", "distribution: Ubuntu\n", `"distribution" header contains invalid characters: "Ubuntu", see distribution ID in os-release spec`},
{"distribution: ubuntu\n", "distribution: *buntu\n", `"distribution" header contains invalid characters: "\*buntu", see distribution ID in os-release spec`},
{"type: gadget\n", "type: app\n", `cannot specify a kernel in an extended classic model without a model gadget`},
{" classic: true\n", " classic: what", `"classic" of snap "myappopt" must be 'true' or 'false'`},
{"OTHER", ` modes:
- ephemeral
- run
`, `classic snap "myappopt" not allowed outside of run mode: \[ephemeral run\]`},
{"OTHER", ` modes:
- install
`, `classic snap "myappopt" not allowed outside of run mode: \[install\]`},
{` type: app
presence: optional
`, ` type: base
presence: optional
`, `snap "myappopt" cannot be classic with type "base" instead of app`},
{"\nclassic: true\ndistribution: ubuntu\n", "\nclassic: false\n", `snap "myappopt" cannot be classic in non-classic model`},
}
invalidTests = append(invalidTests, classicInvalid...)
} else {
coreInvalid := []struct{ original, invalid, expectedErr string }{
{"OTHER", "distribution: ubuntu\n", `cannot specify distribution for model unless it is classic and has an extended snaps header`},
{"type: gadget\n", "type: app\n", `one "snaps" header entry must specify the model gadget`},
{"type: kernel\n", "type: app\n", `one "snaps" header entry must specify the model kernel`},
}
invalidTests = append(invalidTests, coreInvalid...)
}
for _, test := range invalidTests {
invalid := strings.Replace(encoded, test.original, test.invalid, 1)
invalid = strings.Replace(invalid, "OTHER", "", 1)
_, err := asserts.Decode([]byte(invalid))
c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
}
}
func (mods *modelSuite) TestClassicWithSnapsMinimalDecodeOK(c *C) {
// XXX support also omitting the base?
encoded := strings.Replace(classicModelWithSnapsExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", "", 1)
tests := []struct {
originalFrag string
changedFrag string
hasGadget bool
}{
// no kernel and no gadget
{`
-
name: baz-linux
id: bazlinuxidididididididididididid
type: kernel
default-channel: 20
-
name: brand-gadget
id: brandgadgetdidididididididididid
type: gadget`, "", false},
// no kernel but a gadget
{`
-
name: baz-linux
id: bazlinuxidididididididididididid
type: kernel
default-channel: 20`, "", true},
}
for _, t := range tests {
minimal := strings.Replace(encoded, t.originalFrag, t.changedFrag, 1)
a, err := asserts.Decode([]byte(minimal))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.Architecture(), Equals, "amd64")
c.Check(model.Classic(), Equals, true)
c.Check(model.Distribution(), Equals, "ubuntu")
c.Check(model.Base(), Equals, "core20")
// no kernel
c.Check(model.KernelSnap(), IsNil)
c.Check(model.Kernel(), Equals, "")
c.Check(model.KernelTrack(), Equals, "")
c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{
Name: "core20",
SnapID: naming.WellKnownSnapID("core20"),
SnapType: "base",
Modes: []string{"run", "ephemeral"},
DefaultChannel: "latest/stable",
Presence: "required",
})
expectedEssSnap := []*asserts.ModelSnap{
model.BaseSnap(),
}
if t.hasGadget {
c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
Name: "brand-gadget",
SnapID: "brandgadgetdidididididididididid",
SnapType: "gadget",
Modes: []string{"run", "ephemeral"},
DefaultChannel: "latest/stable",
Presence: "required",
})
c.Check(model.Gadget(), Equals, "brand-gadget")
c.Check(model.GadgetTrack(), Equals, "")
expectedEssSnap = append(expectedEssSnap, model.GadgetSnap())
} else {
c.Check(model.GadgetSnap(), IsNil)
c.Check(model.Gadget(), Equals, "")
c.Check(model.GadgetTrack(), Equals, "")
}
c.Check(model.Grade(), Equals, asserts.ModelSecured)
c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyEncrypted)
essentialSnaps := model.EssentialSnaps()
c.Check(essentialSnaps, DeepEquals, expectedEssSnap)
snaps := model.SnapsWithoutEssential()
c.Check(snaps, DeepEquals, []*asserts.ModelSnap{
{
Name: "other-base",
SnapID: "otherbasedididididididididididid",
SnapType: "base",
Modes: []string{"run"},
DefaultChannel: "latest/stable",
Presence: "required",
},
{
Name: "nm",
SnapID: "nmididididididididididididididid",
SnapType: "app",
Modes: []string{"ephemeral", "run"},
DefaultChannel: "1.0",
Presence: "required",
},
{
Name: "myapp",
SnapID: "myappdididididididididididididid",
SnapType: "app",
Modes: []string{"run"},
DefaultChannel: "2.0",
Presence: "required",
},
{
Name: "myappopt",
SnapID: "myappoptidididididididididididid",
SnapType: "app",
Modes: []string{"run"},
DefaultChannel: "latest/stable",
Presence: "optional",
Classic: true,
},
})
// essential snaps included
reqSnaps := naming.NewSnapSet(model.RequiredWithEssentialSnaps())
for _, e := range essentialSnaps {
c.Check(reqSnaps.Contains(e), Equals, true)
}
for _, s := range snaps {
c.Check(reqSnaps.Contains(s), Equals, s.Presence == "required")
}
c.Check(reqSnaps.Size(), Equals, len(essentialSnaps)+len(snaps)-1)
// essential snaps excluded
noEssential := naming.NewSnapSet(model.RequiredNoEssentialSnaps())
for _, e := range essentialSnaps {
c.Check(noEssential.Contains(e), Equals, false)
}
for _, s := range snaps {
c.Check(noEssential.Contains(s), Equals, s.Presence == "required")
}
c.Check(noEssential.Size(), Equals, len(snaps)-1)
}
}
func (mods *modelSuite) TestModelValidationSetAtSequence(c *C) {
mvs := &asserts.ModelValidationSet{
AccountID: "test",
Name: "set",
Mode: asserts.ModelValidationSetModeEnforced,
}
c.Check(mvs.AtSequence(), DeepEquals, &asserts.AtSequence{
Type: asserts.ValidationSetType,
SequenceKey: []string{release.Series, "test", "set"},
Sequence: 0,
Pinned: false,
Revision: asserts.RevisionNotKnown,
})
}
func (mods *modelSuite) TestModelValidationSetAtSequenceNoSequence(c *C) {
mvs := &asserts.ModelValidationSet{
AccountID: "test",
Name: "set",
Sequence: 1,
Mode: asserts.ModelValidationSetModeEnforced,
}
c.Check(mvs.AtSequence(), DeepEquals, &asserts.AtSequence{
Type: asserts.ValidationSetType,
SequenceKey: []string{release.Series, "test", "set"},
Sequence: 1,
Pinned: true,
Revision: asserts.RevisionNotKnown,
})
}
func (mods *modelSuite) TestValidationSetsDecodeInvalid(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
tests := []struct {
frag string
expectedErr string
}{
// invalid format 1
{`validation-sets: 12395
`, "assertion model: \"validation-sets\" must be a list of validation sets"},
// invalid format 2
{`validation-sets:
- test
`, "assertion model: entry in \"validation-sets\" is not a valid validation-set"},
// missing name
{`validation-sets:
-
mode: prefer-enforce
`, "assertion model: \"name\" of validation-set is mandatory"},
// account-id not a valid string
{`validation-sets:
-
account-id:
- 1
name: my-set
mode: enforce
`, "assertion model: \"account-id\" of validation-set \"my-set\" must be a string"},
// missing mode
{`validation-sets:
-
name: my-set
`, "assertion model: \"mode\" of validation-set \"brand-id1/my-set\" is mandatory"},
// invalid value in mode
{`validation-sets:
-
account-id: developer1
name: my-set
sequence: 10
mode: hello
`, "assertion model: \"mode\" of validation-set \"brand-id1/my-set\" must be prefer-enforce|enforce, not \"hello\""},
// sequence number invalid (not an integer)
{`validation-sets:
-
account-id: developer1
sequence: foo
name: my-set
mode: enforce
`, "assertion model: \"sequence\" of validation-set \"developer1/my-set\" is not an integer: foo"},
// sequence number invalid (below)
{`validation-sets:
-
account-id: developer1
sequence: -1
name: my-set
mode: enforce
`, "assertion model: \"sequence\" of validation-set \"developer1/my-set\" must be larger than 0 or left unspecified \\(meaning tracking latest\\)"},
// sequence number invalid (0 is not allowed)
{`validation-sets:
-
account-id: developer1
sequence: 0
name: my-set
mode: enforce
`, "assertion model: \"sequence\" of validation-set \"developer1/my-set\" must be larger than 0 or left unspecified \\(meaning tracking latest\\)"},
// duplicate validation-set
{`validation-sets:
-
account-id: developer1
name: my-set
mode: prefer-enforce
-
account-id: developer1
name: my-set
mode: enforce
`, "assertion model: cannot add validation set \"developer1/my-set\" twice"},
}
for _, t := range tests {
data := strings.Replace(encoded, "OTHER", t.frag, 1)
_, err := asserts.Decode([]byte(data))
c.Check(err, ErrorMatches, t.expectedErr)
}
}
func (mods *modelSuite) TestValidationSetsDecodeOK(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
tests := []struct {
frag string
expected []*asserts.ModelValidationSet
}{
// brand validation-set, this should instead use the brand specified
// by the core20ModelExample, as account-id is not set
{`validation-sets:
-
name: my-set
mode: prefer-enforce
`,
[]*asserts.ModelValidationSet{
{
AccountID: "brand-id1",
Name: "my-set",
Mode: asserts.ModelValidationSetModePreferEnforced,
},
}},
// pinned set
{`validation-sets:
-
account-id: developer1
name: my-set
sequence: 10
mode: enforce
`,
[]*asserts.ModelValidationSet{
{
AccountID: "developer1",
Name: "my-set",
Sequence: 10,
Mode: asserts.ModelValidationSetModeEnforced,
},
}},
// unpinned set
{`validation-sets:
-
account-id: developer1
name: my-set
mode: prefer-enforce
`,
[]*asserts.ModelValidationSet{
{
AccountID: "developer1",
Name: "my-set",
Mode: asserts.ModelValidationSetModePreferEnforced,
},
}},
}
for _, t := range tests {
data := strings.Replace(encoded, "OTHER", t.frag, 1)
a, err := asserts.Decode([]byte(data))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.Architecture(), Equals, "amd64")
c.Check(model.Classic(), Equals, false)
c.Check(model.Base(), Equals, "core20")
c.Check(model.ValidationSets(), DeepEquals, t.expected)
}
}
func (mods *modelSuite) TestModelValidationSetSequenceKey(c *C) {
mvs := &asserts.ModelValidationSet{
AccountID: "test",
Name: "set",
Sequence: 1,
Mode: asserts.ModelValidationSetModeEnforced,
}
c.Check(mvs.SequenceKey(), Equals, "16/test/set")
}
func (mods *modelSuite) TestAllSnaps(c *C) {
encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
allSnaps := append([]*asserts.ModelSnap(nil), model.EssentialSnaps()...)
// make sure that we have essential snaps to compare to
c.Assert(len(allSnaps), testutil.IntGreaterThan, 0)
essentialLen := len(allSnaps)
allSnaps = append(allSnaps, model.SnapsWithoutEssential()...)
// same here, make sure that we have non-essential snaps to compare to
c.Assert(len(allSnaps), testutil.IntGreaterThan, essentialLen)
c.Check(model.AllSnaps(), DeepEquals, allSnaps)
}
func (mods *modelSuite) TestDecodeWithComponentsOK(c *C) {
encoded := strings.Replace(coreModelWithComponentsExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a.Type(), Equals, asserts.ModelType)
model := a.(*asserts.Model)
c.Check(model.AuthorityID(), Equals, "brand-id1")
c.Check(model.Timestamp(), Equals, mods.ts)
c.Check(model.Series(), Equals, "16")
c.Check(model.BrandID(), Equals, "brand-id1")
c.Check(model.Model(), Equals, "baz-3000")
c.Check(model.DisplayName(), Equals, "Baz 3000")
c.Check(model.Architecture(), Equals, "amd64")
c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
Name: "brand-gadget",
SnapID: "brandgadgetdidididididididididid",
SnapType: "gadget",
Modes: []string{"run", "ephemeral"},
Presence: "required",
DefaultChannel: "latest/stable",
})
c.Check(model.Gadget(), Equals, "brand-gadget")
c.Check(model.GadgetTrack(), Equals, "")
c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{
Name: "baz-linux",
SnapID: "bazlinuxidididididididididididid",
SnapType: "kernel",
Modes: []string{"run", "ephemeral"},
Presence: "required",
DefaultChannel: "20",
})
c.Check(model.Kernel(), Equals, "baz-linux")
c.Check(model.KernelTrack(), Equals, "")
c.Check(model.Base(), Equals, "core24")
c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{
Name: "core24",
SnapID: "dwTAh7MZZ01zyriOZErqd1JynQLiOGvM",
SnapType: "base",
Modes: []string{"run", "ephemeral"},
Presence: "required",
DefaultChannel: "latest/stable",
})
c.Check(model.Store(), Equals, "brand-store")
c.Check(model.Grade(), Equals, asserts.ModelSecured)
c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyEncrypted)
essentialSnaps := model.EssentialSnaps()
c.Check(essentialSnaps, DeepEquals, []*asserts.ModelSnap{
model.KernelSnap(),
model.BaseSnap(),
model.GadgetSnap(),
})
snaps := model.SnapsWithoutEssential()
c.Check(snaps, DeepEquals, []*asserts.ModelSnap{
{
Name: "other-base",
SnapID: "otherbasedididididididididididid",
SnapType: "base",
Modes: []string{"run"},
DefaultChannel: "latest/stable",
Presence: "required",
},
{
Name: "nm",
SnapID: "nmididididididididididididididid",
SnapType: "app",
Modes: []string{"ephemeral", "run"},
DefaultChannel: "1.0",
Presence: "required",
Components: map[string]asserts.ModelComponent{
"comp1": {
Presence: "optional",
Modes: []string{"ephemeral"},
},
"comp2": {
Presence: "required",
Modes: []string{"ephemeral", "run"},
},
},
},
{
Name: "myapp",
SnapID: "myappdididididididididididididid",
SnapType: "app",
Modes: []string{"ephemeral", "run"},
DefaultChannel: "2.0",
Presence: "optional",
Components: map[string]asserts.ModelComponent{
"comp1": {
Presence: "optional",
Modes: []string{"ephemeral", "run"},
},
"comp2": {
Presence: "required",
Modes: []string{"ephemeral", "run"},
},
},
},
{
Name: "myappopt",
SnapID: "myappoptidididididididididididid",
SnapType: "app",
DefaultChannel: "latest/stable",
Presence: "required",
Modes: []string{"run"},
Components: map[string]asserts.ModelComponent{
"comp1": {
Presence: "optional",
Modes: []string{"run"},
},
"comp2": {
Presence: "required",
Modes: []string{"run"},
},
},
},
})
c.Check(model.SystemUserAuthority(), HasLen, 0)
c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1"})
c.Check(model.PreseedAuthority(), DeepEquals, []string{"brand-id1"})
}
func (mods *modelSuite) TestDecodeWithComponentsBadPresence1(c *C) {
encoded := strings.Replace(coreModelWithComponentsExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", ` -
name: somesnap
id: somesnapidididididididididididid
type: app
presence: required
components:
comp1: badpresenceval
`, 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err.Error(), Equals, `assertion model: presence of component "comp1" of snap "somesnap" must be one of required|optional`)
c.Assert(a, IsNil)
}
func (mods *modelSuite) TestDecodeWithComponentsBadPresence2(c *C) {
encoded := strings.Replace(coreModelWithComponentsExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", ` -
name: somesnap
id: somesnapidididididididididididid
type: app
presence: required
components:
comp1:
presence: badpresenceval
`, 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err.Error(), Equals, `assertion model: presence of component "comp1" of snap "somesnap" must be one of required|optional`)
c.Assert(a, IsNil)
}
func (mods *modelSuite) TestDecodeWithComponentsBadMode(c *C) {
encoded := strings.Replace(coreModelWithComponentsExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", ` -
name: somesnap
id: somesnapidididididididididididid
type: app
presence: required
modes:
- run
components:
comp1:
presence: required
modes:
- ephemeral
`, 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err.Error(), Equals, `assertion model: mode "ephemeral" of component "comp1" of snap "somesnap" is incompatible with the snap modes`)
c.Assert(a, IsNil)
}
func (mods *modelSuite) TestDecodeWithComponentsBadContent(c *C) {
for i, tc := range []struct {
compsEntry string
errMsg string
}{
{` components:
- comp1
- comp2
`,
`assertion model: "components" of snap "somesnap" must be a map from strings to components`},
{` components:
comp_1: required
`,
`parsing assertion headers: invalid map entry key: "comp_1"`},
{` components:
comp1:
presence: required
other: something
`,
`assertion model: entry "other" of component "comp1" of snap "somesnap" is unknown`},
{` components:
comp1:
modes:
- run
`,
`assertion model: "presence" of component "comp1" of snap "somesnap" is mandatory`,
},
{` components:
comp1:
presence: required
modes:
- foomode
`,
`assertion model: mode "foomode" of component "comp1" of snap "somesnap" is incompatible with the snap modes`,
},
} {
c.Logf("test %d: %q", i, tc.compsEntry)
encoded := strings.Replace(coreModelWithComponentsExample, "TSLINE", mods.tsLine, 1)
encoded = strings.Replace(encoded, "OTHER", ` -
name: somesnap
id: somesnapidididididididididididid
type: app
presence: required
modes:
- run
`+tc.compsEntry, 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err.Error(), Equals, tc.errMsg)
c.Assert(a, IsNil)
}
}