Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

asserts,confdb: have operators as a list in confdb-control assertion #15013

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 110 additions & 21 deletions asserts/confdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"

"github.com/snapcore/snapd/confdb"
Expand Down Expand Up @@ -151,6 +153,22 @@ func (cc *ConfdbControl) Prerequisites() []*Ref {
}
}

// NewConfdbControl returns an empty confdb-control assertion.
func NewConfdbControl(brand, model, serial string) *ConfdbControl {
return &ConfdbControl{
assertionBase: assertionBase{
headers: map[string]interface{}{
"type": "confdb-control",
"brand-id": brand,
"model": model,
"serial": serial,
"groups": []interface{}{},
},
},
operators: map[string]*confdb.Operator{},
}
}

// BrandID returns the brand identifier of the device.
func (cc *ConfdbControl) BrandID() string {
return cc.HeaderString("brand-id")
Expand All @@ -167,6 +185,72 @@ func (cc *ConfdbControl) Serial() string {
return cc.HeaderString("serial")
}

// ConfdbControlGroup holds a single group in a confdb-control assertion.
type ConfdbControlGroup struct {
Operators []string
Authentications []string
Views []string
}

// Groups returns the groups in the raw assertion's format.
func (cc *ConfdbControl) Groups() []*ConfdbControlGroup {
// Map auth to view->operators mapping
authMap := map[confdb.Authentication]map[confdb.ViewRef][]string{}
var auths []confdb.Authentication

// Group operators by auth and view
for _, operator := range cc.operators {
for view, auth := range operator.Delegations {
_, exists := authMap[auth]
if !exists {
authMap[auth] = map[confdb.ViewRef][]string{}
auths = append(auths, auth)
}

authMap[auth][view] = append(authMap[auth][view], operator.ID)
}
}

// Sort auths for consistent output
sort.Slice(auths, func(i, j int) bool {
return auths[i] < auths[j]
})

// Create groups
var groups []*ConfdbControlGroup
for _, auth := range auths {
viewMap := authMap[auth]
authStrs := confdb.ConvertAuthenticationToStrings(auth)

// Group by unique operator sets
operatorSetMap := map[string]*ConfdbControlGroup{}

for view, operators := range viewMap {
sort.Strings(operators)
key := strings.Join(operators, ",")

if group, exists := operatorSetMap[key]; exists {
group.Views = append(group.Views, view.String())
} else {
group := &ConfdbControlGroup{
Operators: operators,
Authentications: authStrs,
Views: []string{view.String()},
}
operatorSetMap[key] = group
groups = append(groups, group)
}
}
}

// Sort views in each group
for _, group := range groups {
sort.Strings(group.Views)
}

return groups
}

// assembleConfdbControl creates a new confdb-control assertion after validating
// all required fields and constraints.
func assembleConfdbControl(assert assertionBase) (Assertion, error) {
Expand Down Expand Up @@ -209,28 +293,12 @@ func parseConfdbControlGroups(rawGroups []interface{}) (map[string]*confdb.Opera
return nil, fmt.Errorf("%s: must be a map", errPrefix)
}

operatorID, err := checkNotEmptyStringWhat(group, "operator-id", "field")
auth, err := checkStringListInMap(group, "authentications", "field", nil)
if err != nil {
return nil, fmt.Errorf("%s: %w", errPrefix, err)
}

// Currently, operatorIDs must be snap store account IDs
if !IsValidAccountID(operatorID) {
return nil, fmt.Errorf(`%s: invalid "operator-id" %s`, errPrefix, operatorID)
}

operator, ok := operators[operatorID]
if !ok {
operator = &confdb.Operator{ID: operatorID}
operators[operatorID] = operator
}

auth, err := checkStringListInMap(group, "authentication", "field", nil)
if err != nil {
return nil, fmt.Errorf(`%s: "authentication" %w`, errPrefix, err)
return nil, fmt.Errorf(`%s: "authentications" %w`, errPrefix, err)
}
if auth == nil {
return nil, fmt.Errorf(`%s: "authentication" must be provided`, errPrefix)
return nil, fmt.Errorf(`%s: "authentications" must be provided`, errPrefix)
}

views, err := checkStringListInMap(group, "views", "field", nil)
Expand All @@ -241,8 +309,29 @@ func parseConfdbControlGroups(rawGroups []interface{}) (map[string]*confdb.Opera
return nil, fmt.Errorf(`%s: "views" must be provided`, errPrefix)
}

if err := operator.AddControlGroup(views, auth); err != nil {
return nil, fmt.Errorf(`%s: %w`, errPrefix, err)
operatorIDs, err := checkStringListInMap(group, "operators", "field", nil)
if err != nil {
return nil, fmt.Errorf("%s: %w", errPrefix, err)
}
if operatorIDs == nil {
return nil, fmt.Errorf(`%s: "operators" must be provided`, errPrefix)
}

for _, operatorID := range operatorIDs {
// Currently, operatorIDs must be snap store account IDs
if !IsValidAccountID(operatorID) {
return nil, fmt.Errorf(`%s: invalid "operator-id" %s`, errPrefix, operatorID)
}

operator, ok := operators[operatorID]
if !ok {
operator = &confdb.Operator{ID: operatorID}
operators[operatorID] = operator
}

if err := operator.Delegate(views, auth); err != nil {
return nil, fmt.Errorf(`%s: %w`, errPrefix, err)
}
}
}

Expand Down
142 changes: 99 additions & 43 deletions asserts/confdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/confdb"
"github.com/snapcore/snapd/testutil"
)

type confdbSuite struct {
Expand Down Expand Up @@ -215,25 +216,38 @@ model: generic-classic
serial: 03961d5d-26e5-443f-838d-6db046126bea
groups:
-
operator-id: john
authentication:
operators:
- john
authentications:
- operator-key
views:
- canonical/network/control-device
- canonical/network/observe-device
-
operator-id: john
authentication:
operators:
- john
authentications:
- store
views:
- canonical/network/control-interfaces
-
operator-id: jane
authentication:
operators:
- jane
authentications:
- store
- operator-key
views:
- canonical/network/observe-interfaces
-
operators:
- alice
- bob
authentications:
- store
- operator-key
views:
- canonical/network/observe-device
- canonical/network/control-interfaces
sign-key-sha3-384: t9yuKGLyiezBq_PXMJZsGdkTukmL7MgrgqXAlxxiZF4TYryOjZcy48nnjDmEHQDp
AXNpZw==`
Expand Down Expand Up @@ -294,34 +308,20 @@ func (s *confdbCtrlSuite) TestDecodeOK(c *C) {
john, ok := operators["john"]
c.Assert(ok, Equals, true)
c.Assert(john.ID, Equals, "john")
c.Assert(len(john.Groups), Equals, 2)

g := john.Groups[0]
c.Assert(g.Authentication, DeepEquals, []confdb.AuthenticationMethod{"operator-key"})
expectedViews := []*confdb.ViewRef{
{Account: "canonical", Confdb: "network", View: "control-device"},
{Account: "canonical", Confdb: "network", View: "observe-device"},
}
c.Assert(g.Views, DeepEquals, expectedViews)

g = john.Groups[1]
c.Assert(g.Authentication, DeepEquals, []confdb.AuthenticationMethod{"store"})
expectedViews = []*confdb.ViewRef{
{Account: "canonical", Confdb: "network", View: "control-interfaces"},
}
c.Assert(g.Views, DeepEquals, expectedViews)
delegated, _ := john.IsDelegated("canonical/network/control-device", []string{"operator-key"})
c.Check(delegated, Equals, true)
delegated, _ = john.IsDelegated("canonical/network/observe-device", []string{"operator-key"})
c.Check(delegated, Equals, true)
delegated, _ = john.IsDelegated("canonical/network/control-interfaces", []string{"store"})
c.Check(delegated, Equals, true)

jane, ok := operators["jane"]
c.Assert(ok, Equals, true)
c.Assert(jane.ID, Equals, "jane")
c.Assert(len(jane.Groups), Equals, 1)

g = jane.Groups[0]
c.Assert(g.Authentication, DeepEquals, []confdb.AuthenticationMethod{"operator-key", "store"})
expectedViews = []*confdb.ViewRef{
{Account: "canonical", Confdb: "network", View: "observe-interfaces"},
}
c.Assert(g.Views, DeepEquals, expectedViews)
delegated, _ = jane.IsDelegated("canonical/network/observe-interfaces", []string{"store", "operator-key"})
c.Check(delegated, Equals, true)
}

func (s *confdbCtrlSuite) TestDecodeInvalid(c *C) {
Expand All @@ -340,26 +340,21 @@ func (s *confdbCtrlSuite) TestDecodeInvalid(c *C) {
{"groups:", "groups: foo\nviews:", `"groups" header must be a list`},
{"groups:", "views:", `"groups" stanza is mandatory`},
{"groups:", "groups:\n - bar", `cannot parse group at position 1: must be a map`},
{" operator-id: jane\n", "", `cannot parse group at position 3: "operator-id" field is mandatory`},
{" operators:\n - jane\n", "", `cannot parse group at position 3: "operators" must be provided`},
{
"operator-id: jane\n",
"operator-id: \n",
`cannot parse group at position 3: "operator-id" field should not be empty`,
},
{
"operator-id: jane\n",
"operator-id: @op\n",
" - jane",
" - @op",
`cannot parse group at position 3: invalid "operator-id" @op`,
},
{
" authentication:\n - store",
" authentication: abcd",
`cannot parse group at position 2: "authentication" field must be a list of strings`,
" authentications:\n - store",
" authentications: abcd",
`cannot parse group at position 2: "authentications" field must be a list of strings`,
},
{
" authentication:\n - store",
" authentications:\n - store",
" foo: bar",
`cannot parse group at position 2: "authentication" must be provided`,
`cannot parse group at position 2: "authentications" must be provided`,
},
{
" views:\n - canonical/network/control-interfaces",
Expand All @@ -374,12 +369,12 @@ func (s *confdbCtrlSuite) TestDecodeInvalid(c *C) {
{
" - operator-key",
" - foo-bar",
"cannot parse group at position 1: cannot add group: invalid authentication method: foo-bar",
"cannot parse group at position 1: cannot delegate: invalid authentication method: foo-bar",
},
{
"canonical/network/control-interfaces",
"canonical",
`cannot parse group at position 2: view "canonical" must be in the format account/confdb/view`,
`cannot parse group at position 2: cannot delegate: view "canonical" must be in the format account/confdb/view`,
},
}

Expand Down Expand Up @@ -446,3 +441,64 @@ func (s *confdbCtrlSuite) TestAckAssertionOK(c *C) {
err = s.db.Add(a)
c.Assert(err, IsNil)
}

func (s *confdbCtrlSuite) TestGroups(c *C) {
confdbCtrl := asserts.NewConfdbControl("canonical", "pc", "42")

aa := &confdb.Operator{ID: "aa"}
aa.Delegate([]string{"dd/ee/ff", "gg/hh/ii", "jj/kk/ll"}, []string{"store", "operator-key"})
aa.Delegate([]string{"pp/qq/rr"}, []string{"operator-key"})
aa.Delegate([]string{"mm/nn/oo"}, []string{"store"})
aa.Delegate([]string{"ss/tt/vv"}, []string{"store"})
aa.Delegate([]string{"ss/tt/vv"}, []string{"operator-key"})
confdbCtrl.AddOperator(aa)

bb := &confdb.Operator{ID: "bb"}
bb.Delegate([]string{"dd/ee/ff", "gg/hh/ii", "jj/kk/ll", "xx/yy/zz"}, []string{"operator-key", "store"})
bb.Delegate([]string{"mm/nn/oo"}, []string{"store"})
bb.Delegate([]string{"aa/bb/cc"}, []string{"operator-key"})
confdbCtrl.AddOperator(bb)

cc := &confdb.Operator{ID: "cc"}
cc.Delegate([]string{"dd/ee/ff", "gg/hh/ii", "jj/kk/ll", "xx/yy/zz"}, []string{"store", "operator-key"})
cc.Delegate([]string{"pp/qq/rr"}, []string{"operator-key"})
confdbCtrl.AddOperator(cc)

groups := confdbCtrl.Groups()
c.Assert(groups, HasLen, 6)
expectedGroups := []*asserts.ConfdbControlGroup{
{
Operators: []string{"aa", "cc"},
Authentications: []string{"operator-key"},
Views: []string{"pp/qq/rr"},
},
{
Operators: []string{"bb"},
Authentications: []string{"operator-key"},
Views: []string{"aa/bb/cc"},
},
{
Operators: []string{"aa", "bb"},
Authentications: []string{"store"},
Views: []string{"mm/nn/oo"},
},
{
Operators: []string{"bb", "cc"},
Authentications: []string{"operator-key", "store"},
Views: []string{"xx/yy/zz"},
},
{
Operators: []string{"aa", "bb", "cc"},
Authentications: []string{"operator-key", "store"},
Views: []string{"dd/ee/ff", "gg/hh/ii", "jj/kk/ll"},
},
{
Operators: []string{"aa"},
Authentications: []string{"operator-key", "store"},
Views: []string{"ss/tt/vv"},
},
}
for _, expected := range expectedGroups {
c.Assert(groups, testutil.DeepContains, expected)
}
}
5 changes: 5 additions & 0 deletions asserts/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,8 @@ func MockAssertionPrereqs(f func(a Assertion) []*Ref) func() {
func (cc *ConfdbControl) Operators() map[string]*confdb.Operator {
return cc.operators
}

// helper function to add operators
func (cc *ConfdbControl) AddOperator(operator *confdb.Operator) {
cc.operators[operator.ID] = operator
}
Loading
Loading