Skip to content

Commit

Permalink
Handle validation-sets with store action.
Browse files Browse the repository at this point in the history
  • Loading branch information
stolowski committed Feb 3, 2021
1 parent 293b016 commit 40121d2
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 18 deletions.
90 changes: 74 additions & 16 deletions store/store_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type CurrentSnap struct {
}

type AssertionQuery interface {
ToResolve() (map[asserts.Grouping][]*asserts.AtRevision, error)
ToResolve() (map[asserts.Grouping][]*asserts.AtRevision, map[asserts.Grouping][]*asserts.AtSequence, error)

AddError(e error, ref *asserts.Ref) error
AddGroupingError(e error, grouping asserts.Grouping) error
Expand Down Expand Up @@ -123,7 +123,7 @@ type snapActionJSON struct {
Epoch interface{} `json:"epoch,omitempty"`
// For assertions
Key string `json:"key,omitempty"`
Assertions []assertAtJSON `json:"assertions,omitempty"`
Assertions []interface{} `json:"assertions,omitempty"`
}

type assertAtJSON struct {
Expand All @@ -132,6 +132,16 @@ type assertAtJSON struct {
IfNewerThan *int `json:"if-newer-than,omitempty"`
}

type assertSeqAtJSON struct {
Type string `json:"type"`
SequenceKey []string `json:"sequence-key"`
IfNewerThan *int `json:"if-newer-than,omitempty"`
IfSequenceNewerThan *int `json:"if-sequence-newer-than,omitempty"`
// if-sequence-equal-or-newer-than and sequence are mutually exclusive
IfSequenceEqualOrNewerThan *int `json:"if-sequence-equal-or-newer-than,omitempty"`
Sequence int `json:"sequence,omitempty"`
}

type snapRelease struct {
Architecture string `json:"architecture"`
Channel string `json:"channel"`
Expand Down Expand Up @@ -196,22 +206,23 @@ func (s *Store) SnapAction(ctx context.Context, currentSnaps []*CurrentSnap, act
}

var toResolve map[asserts.Grouping][]*asserts.AtRevision
var toResolveSeq map[asserts.Grouping][]*asserts.AtSequence
if assertQuery != nil {
var err error
toResolve, err = assertQuery.ToResolve()
toResolve, toResolveSeq, err = assertQuery.ToResolve()
if err != nil {
return nil, nil, err
}
}

if len(currentSnaps) == 0 && len(actions) == 0 && len(toResolve) == 0 {
if len(currentSnaps) == 0 && len(actions) == 0 && len(toResolve) == 0 && len(toResolveSeq) == 0 {
// nothing to do
return nil, nil, &SnapActionError{NoResults: true}
}

authRefreshes := 0
for {
sars, ars, err := s.snapAction(ctx, currentSnaps, actions, assertQuery, toResolve, user, opts)
sars, ars, err := s.snapAction(ctx, currentSnaps, actions, assertQuery, toResolve, toResolveSeq, user, opts)

if saErr, ok := err.(*SnapActionError); ok && authRefreshes < 2 && len(saErr.Other) > 0 {
// do we need to try to refresh auths?, 2 tries
Expand Down Expand Up @@ -278,7 +289,7 @@ type AssertionResult struct {
StreamURLs []string
}

func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, actions []*SnapAction, assertQuery AssertionQuery, toResolve map[asserts.Grouping][]*asserts.AtRevision, user *auth.UserState, opts *RefreshOptions) ([]SnapActionResult, []AssertionResult, error) {
func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, actions []*SnapAction, assertQuery AssertionQuery, toResolve map[asserts.Grouping][]*asserts.AtRevision, toResolveSeq map[asserts.Grouping][]*asserts.AtSequence, user *auth.UserState, opts *RefreshOptions) ([]SnapActionResult, []AssertionResult, error) {
requestSalt := ""
if opts != nil {
requestSalt = opts.PrivacyKey
Expand Down Expand Up @@ -317,15 +328,16 @@ func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, act
}
}

actionJSONs := make([]*snapActionJSON, len(actions)+len(toResolve))
actionJSONs := make([]*snapActionJSON, len(actions)+len(toResolve)+len(toResolveSeq))
var actionIndex int

// snaps
downloadNum := 0
installNum := 0
installs := make(map[string]*SnapAction, len(actions))
downloads := make(map[string]*SnapAction, len(actions))
refreshes := make(map[string]*SnapAction, len(actions))
for i, a := range actions {
for _, a := range actions {
if !isValidAction(a.Action) {
return nil, nil, fmt.Errorf("internal error: unsupported action %q", a.Action)
}
Expand Down Expand Up @@ -385,30 +397,76 @@ func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, act

aJSON.InstanceKey = instanceKey

actionJSONs[i] = aJSON
actionJSONs[actionIndex] = aJSON
actionIndex++
}

// assertions
var assertMaxFormats map[string]int
if len(toResolve) > 0 {
i := len(actionJSONs) - len(toResolve)
for grp, ats := range toResolve {
aJSON := &snapActionJSON{
Action: "fetch-assertions",
Key: string(grp),
}
aJSON.Assertions = make([]assertAtJSON, len(ats))
aJSON.Assertions = make([]interface{}, len(ats))
for j, at := range ats {
aJSON.Assertions[j].Type = at.Type.Name
aJSON.Assertions[j].PrimaryKey = at.PrimaryKey
aj := &assertAtJSON{
Type: at.Type.Name,
PrimaryKey: at.PrimaryKey,
}
rev := at.Revision
if rev != asserts.RevisionNotKnown {
aJSON.Assertions[j].IfNewerThan = &rev
aj.IfNewerThan = &rev
}
aJSON.Assertions[j] = aj
}
actionJSONs[i] = aJSON
i++
actionJSONs[actionIndex] = aJSON
actionIndex++
}
}

if len(toResolveSeq) > 0 {
for grp, ats := range toResolveSeq {
aJSON := &snapActionJSON{
Action: "fetch-assertions",
Key: string(grp),
}
aJSON.Assertions = make([]interface{}, len(ats))
for j, at := range ats {
aj := assertSeqAtJSON{
Type: at.Type.Name,
SequenceKey: at.SequenceKey,
}
// for pinned we request the assertion ​by the sequence point <sequence-number>​, i.e.
// {"type": "validation-set",
// "sequence-key": ["16", "account-id", "name"],
// "sequence": <sequence-number>}
if at.Pinned {
if at.Sequence <= 0 {
return nil, nil, fmt.Errorf("internal error: sequence not set for pinned sequence %s, %v", at.Type.Name, at.SequenceKey)
}
aj.Sequence = at.Sequence
} else {
// for not pinned, if sequence is specified, then
// use it for "if-sequence-equal-or-newer-than": <sequence-number>
if at.Sequence > 0 {
aj.IfSequenceEqualOrNewerThan = &at.Sequence
}
}
rev := at.Revision
// revision (if set) goes to "if-newer-than": <assert-revision>
if rev != asserts.RevisionNotKnown {
aj.IfNewerThan = &rev
}
aJSON.Assertions[j] = aj
}
actionJSONs[actionIndex] = aJSON
actionIndex++
}
}

if len(toResolve) > 0 || len(toResolveSeq) > 0 {
assertMaxFormats = asserts.MaxSupportedFormats(1)
}

Expand Down
155 changes: 153 additions & 2 deletions store/store_action_fetch_assertions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ var _ = Suite(&storeActionFetchAssertionsSuite{})

type testAssertQuery struct {
toResolve map[asserts.Grouping][]*asserts.AtRevision
toResolveSeq map[asserts.Grouping][]*asserts.AtSequence
errors map[string]error
}

func (q *testAssertQuery) ToResolve() (map[asserts.Grouping][]*asserts.AtRevision, error) {
return q.toResolve, nil
func (q *testAssertQuery) ToResolve() (map[asserts.Grouping][]*asserts.AtRevision, map[asserts.Grouping][]*asserts.AtSequence, error) {
return q.toResolve, q.toResolveSeq, nil
}

func (q *testAssertQuery) addError(e error, u string) {
Expand Down Expand Up @@ -440,3 +441,153 @@ func (s *storeActionFetchAssertionsSuite) TestReportFetchAssertionsError(c *C) {
}
}
}

func (s *storeActionFetchAssertionsSuite) TestUpdateSequenceForming(c *C) {
restore := release.MockOnClassic(false)
defer restore()

mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", snapActionPath)
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)

jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var req struct {
Context []map[string]interface{} `json:"context"`
Actions []map[string]interface{} `json:"actions"`

AssertionMaxFormats map[string]int `json:"assertion-max-formats"`
}

err = json.Unmarshal(jsonReq, &req)
c.Assert(err, IsNil)

c.Assert(req.Context, HasLen, 0)
c.Assert(req.Actions, HasLen, 2)
expectedAction1 := map[string]interface{}{
"action": "fetch-assertions",
"key": "g1",
"assertions": []interface{}{
map[string]interface{}{
"type": "validation-set",
"sequence-key": []interface{}{
"16",
"account-1/name-1",
},
"sequence": float64(3),
},
map[string]interface{}{
"type": "validation-set",
"sequence-key": []interface{}{
"16",
"account-1/name-2",
},
"if-sequence-equal-or-newer-than": float64(5),
"if-newer-than": float64(10),
},
},
}
expectedAction2 := map[string]interface{}{
"action": "fetch-assertions",
"key": "g2",
"assertions": []interface{}{
map[string]interface{}{
"type": "validation-set",
"sequence-key": []interface{}{
"16",
"account-2/name",
},
},
},
}
expectedActions := []map[string]interface{}{expectedAction1, expectedAction2}
if req.Actions[0]["key"] != "g1" {
expectedActions = []map[string]interface{}{expectedAction2, expectedAction1}
}
c.Assert(req.Actions, DeepEquals, expectedActions)

c.Assert(req.AssertionMaxFormats, DeepEquals, asserts.MaxSupportedFormats(1))

fmt.Fprintf(w, `{
"results": [{
"result": "fetch-assertions",
"key": "g1",
"assertion-stream-urls": [
"https://api.snapcraft.io/v2/assertions/snap-declaration/16/iEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5"
]
}, {
"result": "fetch-assertions",
"key": "g2",
"assertion-stream-urls": []
}
]
}`)
}))

c.Assert(mockServer, NotNil)
defer mockServer.Close()

mockServerURL, _ := url.Parse(mockServer.URL)
cfg := store.Config{
StoreBaseURL: mockServerURL,
}
dauthCtx := &testDauthContext{c: c, device: s.device}
sto := store.New(&cfg, dauthCtx)

assertq := &testAssertQuery{
toResolveSeq: map[asserts.Grouping][]*asserts.AtSequence{
asserts.Grouping("g1"): {
&asserts.AtSequence{
Type: asserts.ValidationSetType,
SequenceKey: []string{
"16",
"account-1/name-1",
},
Sequence: 3,
Pinned: true,
Revision: asserts.RevisionNotKnown,
},
&asserts.AtSequence{
Type: asserts.ValidationSetType,
SequenceKey: []string{
"16",
"account-1/name-2",
},
Sequence: 5,
Revision: 10,
},
},
asserts.Grouping("g2"): {
&asserts.AtSequence{
Type: asserts.ValidationSetType,
SequenceKey: []string{
"16",
"account-2/name",
},
Revision: asserts.RevisionNotKnown,
},
},
},
}

results, aresults, err := sto.SnapAction(s.ctx, nil,
nil, assertq, nil, nil)
c.Assert(err, IsNil)
c.Check(results, HasLen, 0)
c.Check(aresults, HasLen, 2)
seen := 0
for _, aresult := range aresults {
if aresult.Grouping == asserts.Grouping("g1") {
seen++
c.Check(aresult.StreamURLs, DeepEquals, []string{
"https://api.snapcraft.io/v2/assertions/snap-declaration/16/iEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5",
})
} else {
seen++
c.Check(aresult.Grouping, Equals, asserts.Grouping("g2"))
c.Check(aresult.StreamURLs, HasLen, 0)
}
}
c.Check(seen, Equals, 2)
}

0 comments on commit 40121d2

Please sign in to comment.