Skip to content

Commit

Permalink
asserts: define registry-control assertion
Browse files Browse the repository at this point in the history
  • Loading branch information
st3v3nmw committed Sep 16, 2024
1 parent f3d0682 commit 5149fd0
Show file tree
Hide file tree
Showing 9 changed files with 492 additions and 3 deletions.
2 changes: 2 additions & 0 deletions asserts/asserts.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ var (
DeviceSessionRequestType = &AssertionType{"device-session-request", []string{"brand-id", "model", "serial"}, nil, assembleDeviceSessionRequest, noAuthority}
SerialRequestType = &AssertionType{"serial-request", nil, nil, assembleSerialRequest, noAuthority}
AccountKeyRequestType = &AssertionType{"account-key-request", []string{"public-key-sha3-384"}, nil, assembleAccountKeyRequest, noAuthority}
RegistryControlType = &AssertionType{"registry-control", []string{"brand-id", "model", "serial", "operator-id"}, nil, assembleRegistryControl, noAuthority}
)

var typeRegistry = map[string]*AssertionType{
Expand All @@ -178,6 +179,7 @@ var typeRegistry = map[string]*AssertionType{
DeviceSessionRequestType.Name: DeviceSessionRequestType,
SerialRequestType.Name: SerialRequestType,
AccountKeyRequestType.Name: AccountKeyRequestType,
RegistryControlType.Name: RegistryControlType,
}

// Type returns the AssertionType with name or nil
Expand Down
6 changes: 5 additions & 1 deletion asserts/asserts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func (as *assertsSuite) TestTypeNames(c *C) {
"model",
"preseed",
"registry",
"registry-control",
"repair",
"serial",
"serial-request",
Expand Down Expand Up @@ -1207,7 +1208,10 @@ func (as *assertsSuite) TestWithAuthority(c *C) {
"validation-set",
"repair",
}
c.Check(withAuthority, HasLen, asserts.NumAssertionType-3) // excluding device-session-request, serial-request, account-key-request

// excluding device-session-request, serial-request, account-key-request, registry-control
c.Check(withAuthority, HasLen, asserts.NumAssertionType-4)

for _, name := range withAuthority {
typ := asserts.Type(name)
_, err := asserts.AssembleAndSignInTest(typ, nil, []byte("{}"), testPrivKey1)
Expand Down
17 changes: 17 additions & 0 deletions asserts/header_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,20 @@ func checkMapWhat(m map[string]interface{}, name, what string) (map[string]inter
}
return mv, nil
}

func checkList(headers map[string]interface{}, name string) ([]interface{}, error) {
return checkListWhat(headers, name, "header")
}

func checkListWhat(headers map[string]interface{}, name, what string) ([]interface{}, error) {
value, ok := headers[name]
if !ok {
return nil, nil
}

list, ok := value.([]interface{})
if !ok {
return nil, fmt.Errorf("%q %s must be a list", name, what)
}
return list, nil
}
76 changes: 76 additions & 0 deletions asserts/registry.go → asserts/registry_asserts.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,79 @@ func assembleRegistry(assert assertionBase) (Assertion, error) {
timestamp: timestamp,
}, nil
}

// RegistryControl holds a registry-control assertion, which holds a list of
// registry views delegated by the device to an operator.
type RegistryControl struct {
assertionBase

registryControl *registry.RegistryControl
}

// BrandID returns the brand identifier of the device.
func (rgCtrl *RegistryControl) BrandID() string {
return rgCtrl.HeaderString("brand-id")
}

// Model returns the model name identifier of the device.
func (rgCtrl *RegistryControl) Model() string {
return rgCtrl.HeaderString("model")
}

// Serial returns the serial identifier of the device, together with
// brand id and model they form the unique identifier of the device.
func (rgCtrl *RegistryControl) Serial() string {
return rgCtrl.HeaderString("serial")
}

// OperatorID returns the identifier of the account the device
// has delegated registry control to.
func (rgCtrl *RegistryControl) OperatorID() string {
return rgCtrl.registryControl.OperatorID
}

// RegistryControl...
func (rgCtrl *RegistryControl) RegistryControl() *registry.RegistryControl {
return rgCtrl.registryControl
}

// TODO: Confirm that the brand-id, model, & serial match the device's serial assertion
func assembleRegistryControl(assert assertionBase) (Assertion, error) {
_, err := checkNotEmptyString(assert.headers, "brand-id")
if err != nil {
return nil, err
}

_, err = checkModel(assert.headers)
if err != nil {
return nil, err
}

_, err = checkNotEmptyString(assert.headers, "serial")
if err != nil {
return nil, err
}

operatorID, err := checkNotEmptyString(assert.headers, "operator-id")
if err != nil {
return nil, err
}

views, err := checkList(assert.headers, "views")
if err != nil {
return nil, err
}
if views == nil {
return nil, fmt.Errorf(`"views" stanza is mandatory`)
}

rgCtrl, err := registry.NewRegistryControl(operatorID, views)
if err != nil {
return nil, err
}

return &RegistryControl{
assertionBase: assert,
registryControl: rgCtrl,
}, nil
}
89 changes: 88 additions & 1 deletion asserts/registry_test.go → asserts/registry_asserts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ type registrySuite struct {
tsLine string
}

var _ = Suite(&registrySuite{})
var (
_ = Suite(&registrySuite{})
_ = Suite(&registryControlSuite{})
)

func (s *registrySuite) SetUpSuite(c *C) {
s.ts = time.Now().Truncate(time.Second).UTC()
Expand Down Expand Up @@ -199,3 +202,87 @@ func (s *registrySuite) TestAssembleAndSignChecksSchemaFormatFail(c *C) {
_, err := asserts.AssembleAndSignInTest(asserts.RegistryType, headers, []byte(schema), testPrivKey0)
c.Assert(err, ErrorMatches, `assertion registry: JSON in body must be indented with 2 spaces and sort object entries by key`)
}

type registryControlSuite struct{}

const (
registryControlExample = `type: registry-control
brand-id: generic
model: generic-classic
serial: 03961d5d-26e5-443f-838d-6db046126bea
operator-id: f22PSauKuNkwQTM9Wz67ZCjNACuSjjhN
views:
-
name: canonical/network/control-device
-
name: canonical/network/observe-device
-
name: canonical/network/control-interfaces
-
name: canonical/network/observe-interfaces
sign-key-sha3-384: t9yuKGLyiezBq_PXMJZsGdkTukmL7MgrgqXAlxxiZF4TYryOjZcy48nnjDmEHQDp
AXNpZw==`
)

func (s *registryControlSuite) TestDecodeOK(c *C) {
encoded := registryControlExample

a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
c.Check(a, NotNil)
c.Check(a.Type(), Equals, asserts.RegistryControlType)

rgCtrlA := a.(*asserts.RegistryControl)
c.Check(rgCtrlA.AuthorityID(), Equals, "")
c.Check(rgCtrlA.BrandID(), Equals, "generic")
c.Check(rgCtrlA.Model(), Equals, "generic-classic")
c.Check(rgCtrlA.Serial(), Equals, "03961d5d-26e5-443f-838d-6db046126bea")
c.Check(rgCtrlA.OperatorID(), Equals, "f22PSauKuNkwQTM9Wz67ZCjNACuSjjhN")

rgCtrl := rgCtrlA.RegistryControl()
c.Assert(rgCtrl, NotNil)
networkRegistry := rgCtrl.Registries["canonical/network"]
c.Assert(networkRegistry, NotNil)

c.Check(len(networkRegistry.Views), Equals, 4)
c.Check(rgCtrl.IsDelegated("canonical", "network", "control-device"), Equals, true)
c.Check(rgCtrl.IsDelegated("canonical", "network", "control-interfaces"), Equals, true)
c.Check(rgCtrl.IsDelegated("canonical", "network", "observe-device"), Equals, true)
c.Check(rgCtrl.IsDelegated("canonical", "network", "observe-interfaces"), Equals, true)

c.Check(rgCtrl.IsDelegated("canonical", "network", "control-vpn"), Equals, false)

rgCtrl.Delegate("canonical", "network", "control-vpn")
c.Check(rgCtrl.IsDelegated("canonical", "network", "control-vpn"), Equals, true)

rgCtrl.Revoke("canonical", "network", "control-vpn")
c.Check(rgCtrl.IsDelegated("canonical", "network", "control-vpn"), Equals, false)
}

func (s *registryControlSuite) TestDecodeInvalid(c *C) {
encoded := registryControlExample
const validationSetErrPrefix = "assertion registry-control: "

viewsStanza := encoded[strings.Index(encoded, "views:") : strings.Index(encoded, "sign-key-sha3-384:")-1]

invalidTests := []struct{ original, invalid, expectedErr string }{
{"brand-id: generic\n", "", `"brand-id" header is mandatory`},
{"model: generic-classic\n", "", `"model" header is mandatory`},
{"serial: 03961d5d-26e5-443f-838d-6db046126bea\n", "", `"serial" header is mandatory`},
{"operator-id: f22PSauKuNkwQTM9Wz67ZCjNACuSjjhN\n", "", `"operator-id" header is mandatory`},
{viewsStanza, "views: abcd", `"views" header must be a list`},
{viewsStanza, "foo: bar", `"views" stanza is mandatory`},
{
"canonical/network/control-interfaces",
"canonical",
`view at position 3: "name" must be in the format account/registry/view`,
},
}

for i, test := range invalidTests {
invalid := strings.Replace(encoded, test.original, test.invalid, 1)
_, err := asserts.Decode([]byte(invalid))
c.Check(err, ErrorMatches, validationSetErrPrefix+test.expectedErr, Commentf("test %d/%d failed", i+1, len(invalidTests)))
}
}
7 changes: 6 additions & 1 deletion features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ const (
RefreshAppAwarenessUX
// Registries enables experimental configuration based on registries and views.
Registries
// RegistryControl enables experimental remote management of registries
RegistryControl
// AppArmorPrompting enables AppArmor to prompt the user for permission when apps perform certain operations.
AppArmorPrompting

Expand Down Expand Up @@ -119,7 +121,9 @@ var featureNames = map[SnapdFeature]string{
QuotaGroups: "quota-groups",

RefreshAppAwarenessUX: "refresh-app-awareness-ux",
Registries: "registries",

Registries: "registries",
RegistryControl: "registry-control",

AppArmorPrompting: "apparmor-prompting",
}
Expand All @@ -146,6 +150,7 @@ var featuresExported = map[SnapdFeature]bool{

RefreshAppAwarenessUX: true,
Registries: true,
RegistryControl: true,
AppArmorPrompting: true,
}

Expand Down
3 changes: 3 additions & 0 deletions features/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func (*featureSuite) TestName(c *C) {
check(features.QuotaGroups, "quota-groups")
check(features.RefreshAppAwarenessUX, "refresh-app-awareness-ux")
check(features.Registries, "registries")
check(features.RegistryControl, "registry-control")
check(features.AppArmorPrompting, "apparmor-prompting")

c.Check(tested, Equals, features.NumberOfFeatures())
Expand Down Expand Up @@ -106,6 +107,7 @@ func (*featureSuite) TestIsExported(c *C) {
check(features.QuotaGroups, false)
check(features.RefreshAppAwarenessUX, true)
check(features.Registries, true)
check(features.RegistryControl, true)
check(features.AppArmorPrompting, true)

c.Check(tested, Equals, features.NumberOfFeatures())
Expand Down Expand Up @@ -235,6 +237,7 @@ func (*featureSuite) TestIsEnabledWhenUnset(c *C) {
check(features.QuotaGroups, false)
check(features.RefreshAppAwarenessUX, false)
check(features.Registries, false)
check(features.RegistryControl, false)
check(features.AppArmorPrompting, false)

c.Check(tested, Equals, features.NumberOfFeatures())
Expand Down
Loading

0 comments on commit 5149fd0

Please sign in to comment.