Files
snapd/daemon/api_validate_test.go
Alfonso Sánchez-Beato 768021bf03 many: add components side info to snapstate
Add components information to the snap state. This is included in the
Sequence variable, so we need to handle in a special way the
marshaling to keep compatibility with state files created by older
snapd versions.

The change to the Sequence field implies multiple changes in tests
that use it. Ancillary methods to handle this have been created.
2023-12-02 09:40:31 +00:00

965 lines
31 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2020 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 daemon_test
import (
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"gopkg.in/check.v1"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/assertstest"
"github.com/snapcore/snapd/asserts/snapasserts"
"github.com/snapcore/snapd/daemon"
"github.com/snapcore/snapd/overlord/assertstate"
"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/naming"
"github.com/snapcore/snapd/testutil"
)
var _ = check.Suite(&apiValidationSetsSuite{})
type apiValidationSetsSuite struct {
apiBaseSuite
storeSigning *assertstest.StoreStack
dev1Signing *assertstest.SigningDB
dev1acct *asserts.Account
acct1Key *asserts.AccountKey
mockSeqFormingAssertionFn func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error)
}
type byName []*snapasserts.InstalledSnap
func (b byName) Len() int { return len(b) }
func (b byName) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func (b byName) Less(i, j int) bool {
return b[i].SnapName() < b[j].SnapName()
}
func (s *apiValidationSetsSuite) SetUpTest(c *check.C) {
s.apiBaseSuite.SetUpTest(c)
d := s.daemon(c)
s.expectAuthenticatedAccess()
restore := asserts.MockMaxSupportedFormat(asserts.ValidationSetType, 1)
s.AddCleanup(restore)
s.mockSeqFormingAssertionFn = nil
s.storeSigning = assertstest.NewStoreStack("can0nical", nil)
st := d.Overlord().State()
st.Lock()
snapstate.ReplaceStore(st, s)
assertstatetest.AddMany(st, s.storeSigning.StoreAccountKey(""))
st.Unlock()
s.dev1acct = assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
c.Assert(s.storeSigning.Add(s.dev1acct), check.IsNil)
// developer signing
dev1PrivKey, _ := assertstest.GenerateKey(752)
s.acct1Key = assertstest.NewAccountKey(s.storeSigning, s.dev1acct, nil, dev1PrivKey.PublicKey(), "")
s.dev1Signing = assertstest.NewSigningDB(s.dev1acct.AccountID(), dev1PrivKey)
c.Assert(s.storeSigning.Add(s.acct1Key), check.IsNil)
d.Overlord().Loop()
s.AddCleanup(func() { d.Overlord().Stop() })
}
func (s *apiValidationSetsSuite) mockValidationSetsTracking(st *state.State) {
st.Set("validation-sets", map[string]interface{}{
fmt.Sprintf("%s/foo", s.dev1acct.AccountID()): map[string]interface{}{
"account-id": s.dev1acct.AccountID(),
"name": "foo",
"mode": assertstate.Enforce,
"pinned-at": 9,
// Current should equal pinned-at if pinned-at != 0 but let's check api_validate is robust
"current": 99,
},
fmt.Sprintf("%s/baz", s.dev1acct.AccountID()): map[string]interface{}{
"account-id": s.dev1acct.AccountID(),
"name": "baz",
"mode": assertstate.Monitor,
"pinned-at": 0,
"current": 2,
},
})
}
func (s *apiValidationSetsSuite) mockAssert(c *check.C, name, sequence string) asserts.Assertion {
snaps := []interface{}{map[string]interface{}{
"id": "yOqKhntON3vR7kwEbVPsILm7bUViPDzz",
"name": "snap-b",
"presence": "required",
"revision": "1",
}}
headers := map[string]interface{}{
"authority-id": s.dev1acct.AccountID(),
"account-id": s.dev1acct.AccountID(),
"name": name,
"series": "16",
"sequence": sequence,
"revision": "5",
"timestamp": "2030-11-06T09:16:26Z",
"snaps": snaps,
}
vs, err := s.dev1Signing.Sign(asserts.ValidationSetType, headers, nil, "")
c.Assert(err, check.IsNil)
return vs
}
func (s *apiValidationSetsSuite) SeqFormingAssertion(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
s.pokeStateLock()
return s.mockSeqFormingAssertionFn(assertType, sequenceKey, sequence, user)
}
func (s *apiValidationSetsSuite) TestQueryValidationSetsErrors(c *check.C) {
s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
return nil, &asserts.NotFoundError{
Type: assertType,
}
}
st := s.d.Overlord().State()
st.Lock()
s.mockValidationSetsTracking(st)
st.Unlock()
for i, tc := range []struct {
validationSet string
// sequence is normally an int, use string for passing invalid ones.
sequence string
message string
status int
}{
{
validationSet: "abc/Xfoo",
message: `invalid name "Xfoo"`,
status: 400,
},
{
validationSet: "Xfoo/bar",
message: `invalid account ID "Xfoo"`,
status: 400,
},
{
validationSet: "foo/foo",
message: "validation set not found",
status: 404,
},
{
validationSet: "foo/bar",
sequence: "1999",
message: "validation set not found",
status: 404,
},
{
validationSet: "foo/bar",
sequence: "x",
message: "invalid sequence argument",
status: 400,
},
{
validationSet: "foo/bar",
sequence: "-2",
message: "invalid sequence argument: -2",
status: 400,
},
} {
q := url.Values{}
if tc.sequence != "" {
q.Set("sequence", tc.sequence)
}
req, err := http.NewRequest("GET", fmt.Sprintf("/v2/validation-sets/%s?%s", tc.validationSet, q.Encode()), nil)
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Check(rspe.Status, check.Equals, tc.status, check.Commentf("case #%d", i))
c.Check(rspe.Message, check.Matches, tc.message)
}
}
func (s *apiValidationSetsSuite) TestGetValidationSetsNone(c *check.C) {
req, err := http.NewRequest("GET", "/v2/validation-sets", nil)
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.([]daemon.ValidationSetResult)
c.Check(res, check.HasLen, 0)
}
func (s *apiValidationSetsSuite) TestListValidationSets(c *check.C) {
st := s.d.Overlord().State()
st.Lock()
s.mockValidationSetsTracking(st)
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
as := s.mockAssert(c, "foo", "9")
err := assertstate.Add(st, as)
c.Check(err, check.IsNil)
as = s.mockAssert(c, "baz", "2")
err = assertstate.Add(st, as)
st.Unlock()
c.Assert(err, check.IsNil)
req, err := http.NewRequest("GET", "/v2/validation-sets", nil)
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.([]daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, []daemon.ValidationSetResult{
{
AccountID: s.dev1acct.AccountID(),
Name: "baz",
Mode: "monitor",
Sequence: 2,
Valid: false,
},
{
AccountID: s.dev1acct.AccountID(),
Name: "foo",
PinnedAt: 9,
Mode: "enforce",
Sequence: 9,
Valid: false,
},
})
}
func (s *apiValidationSetsSuite) TestGetValidationSetOne(c *check.C) {
s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
return nil, &asserts.NotFoundError{
Type: assertType,
}
}
st := s.d.Overlord().State()
st.Lock()
as := s.mockAssert(c, "baz", "2")
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key, as)
s.mockValidationSetsTracking(st)
st.Unlock()
req, err := http.NewRequest("GET", fmt.Sprintf("/v2/validation-sets/%s/baz", s.dev1acct.AccountID()), nil)
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: s.dev1acct.AccountID(),
Name: "baz",
Mode: "monitor",
Sequence: 2,
Valid: false,
})
}
func (s *apiValidationSetsSuite) TestGetValidationSetPinned(c *check.C) {
q := url.Values{}
q.Set("sequence", "9")
req, err := http.NewRequest("GET", fmt.Sprintf("/v2/validation-sets/%s/foo?%s", s.dev1acct.AccountID(), q.Encode()), nil)
c.Assert(err, check.IsNil)
st := s.d.Overlord().State()
st.Lock()
as := s.mockAssert(c, "foo", "9")
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key, as)
s.mockValidationSetsTracking(st)
st.Unlock()
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: s.dev1acct.AccountID(),
Name: "foo",
PinnedAt: 9,
Mode: "enforce",
Sequence: 9,
Valid: false,
})
}
func (s *apiValidationSetsSuite) TestGetValidationSetNotFound(c *check.C) {
s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
return nil, &asserts.NotFoundError{
Type: assertType,
}
}
req, err := http.NewRequest("GET", "/v2/validation-sets/foo/other", nil)
c.Assert(err, check.IsNil)
st := s.d.Overlord().State()
st.Lock()
s.mockValidationSetsTracking(st)
st.Unlock()
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 404)
c.Check(string(rspe.Kind), check.Equals, "validation-set-not-found")
c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{
"account-id": "foo",
"name": "other",
})
}
var validationSetAssertion = []byte("type: validation-set\n" +
"format: 1\n" +
"authority-id: foo\n" +
"account-id: foo\n" +
"name: other\n" +
"sequence: 2\n" +
"revision: 5\n" +
"series: 16\n" +
"snaps:\n" +
" -\n" +
" id: yOqKhntON3vR7kwEbVPsILm7bUViPDzz\n" +
" name: snap-b\n" +
" presence: required\n" +
" revision: 1\n" +
"timestamp: 2020-11-06T09:16:26Z\n" +
"sign-key-sha3-384: 7bbncP0c4RcufwReeiylCe0S7IMCn-tHLNSCgeOVmV3K-7_MzpAHgJDYeOjldefE\n\n" +
"AXNpZw==")
func (s *apiValidationSetsSuite) TestGetValidationSetLatestFromRemote(c *check.C) {
s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
c.Assert(assertType, check.NotNil)
c.Assert(assertType.Name, check.Equals, "validation-set")
// no sequence number element, querying the latest
c.Assert(sequenceKey, check.DeepEquals, []string{"16", "foo", "other"})
c.Assert(sequence, check.Equals, 0)
as, err := asserts.Decode(validationSetAssertion)
c.Assert(err, check.IsNil)
// validity
c.Assert(as.Type().Name, check.Equals, "validation-set")
return as, nil
}
restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
c.Assert(vsets, check.NotNil)
sort.Sort(byName(snaps))
c.Assert(snaps, check.DeepEquals, []*snapasserts.InstalledSnap{
{
SnapRef: naming.NewSnapRef("snap-a", "snapaid"),
Revision: snap.R(2),
},
{
SnapRef: naming.NewSnapRef("snap-b", "snapbid"),
Revision: snap.R(4),
},
})
c.Assert(ignoreValidation, check.IsNil)
// nil indicates successful validation
return nil
})
defer restore()
req, err := http.NewRequest("GET", "/v2/validation-sets/foo/other", nil)
c.Assert(err, check.IsNil)
st := s.d.Overlord().State()
st.Lock()
s.mockValidationSetsTracking(st)
snapstate.Set(st, "snap-a", &snapstate.SnapState{
Active: true,
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-a", Revision: snap.R(2), SnapID: "snapaid"}}),
Current: snap.R(2),
})
snapstate.Set(st, "snap-b", &snapstate.SnapState{
Active: true,
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(4), SnapID: "snapbid"}}),
Current: snap.R(4),
})
st.Unlock()
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: "foo",
Name: "other",
Sequence: 2,
Valid: true,
})
}
func (s *apiValidationSetsSuite) TestGetValidationSetLatestFromRemoteValidationFails(c *check.C) {
s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
as, err := asserts.Decode(validationSetAssertion)
c.Assert(err, check.IsNil)
return as, nil
}
restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
return &snapasserts.ValidationSetsValidationError{}
})
defer restore()
req, err := http.NewRequest("GET", "/v2/validation-sets/foo/other", nil)
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: "foo",
Name: "other",
Sequence: 2,
Valid: false,
})
}
func (s *apiValidationSetsSuite) TestGetValidationSetLatestFromRemoteRealValidation(c *check.C) {
s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
as, err := asserts.Decode(validationSetAssertion)
c.Assert(err, check.IsNil)
return as, nil
}
st := s.d.Overlord().State()
for _, tc := range []struct {
revision snap.Revision
expectedValidationStatus bool
}{
// required at revision 1 per validationSetAssertion, so it's valid
{snap.R(1), true},
// but revision 2 is not valid
{snap.R(2), false},
} {
st.Lock()
snapstate.Set(st, "snap-b", &snapstate.SnapState{
Active: true,
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-b", Revision: tc.revision, SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}),
Current: tc.revision,
})
st.Unlock()
req, err := http.NewRequest("GET", "/v2/validation-sets/foo/other", nil)
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: "foo",
Name: "other",
Sequence: 2,
Valid: tc.expectedValidationStatus,
})
}
}
func (s *apiValidationSetsSuite) TestGetValidationSetSpecificSequenceFromRemote(c *check.C) {
s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
c.Assert(assertType, check.NotNil)
c.Assert(assertType.Name, check.Equals, "validation-set")
c.Assert(sequenceKey, check.DeepEquals, []string{"16", "foo", "other"})
c.Assert(sequence, check.Equals, 2)
as, err := asserts.Decode(validationSetAssertion)
c.Assert(err, check.IsNil)
return as, nil
}
restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
c.Assert(vsets, check.NotNil)
sort.Sort(byName(snaps))
c.Assert(snaps, check.DeepEquals, []*snapasserts.InstalledSnap{
{
SnapRef: naming.NewSnapRef("snap-a", "snapaid"),
Revision: snap.R(33),
},
})
c.Assert(ignoreValidation, check.IsNil)
// nil indicates successful validation
return nil
})
defer restore()
q := url.Values{}
q.Set("sequence", "2")
req, err := http.NewRequest("GET", "/v2/validation-sets/foo/other?"+q.Encode(), nil)
c.Assert(err, check.IsNil)
st := s.d.Overlord().State()
st.Lock()
s.mockValidationSetsTracking(st)
snapstate.Set(st, "snap-a", &snapstate.SnapState{
Active: true,
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-a", Revision: snap.R(33), SnapID: "snapaid"}}),
Current: snap.R(33),
})
st.Unlock()
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: "foo",
Name: "other",
Sequence: 2,
Valid: true,
})
}
func (s *apiValidationSetsSuite) TestGetValidationSetFromRemoteFallbackToLocalAssertion(c *check.C) {
s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
// not found in the store
return nil, &asserts.NotFoundError{
Type: assertType,
}
}
restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
// nil indicates successful validation
return nil
})
defer restore()
st := s.d.Overlord().State()
st.Lock()
// assertion available in the local db (from snap ack)
vs := s.mockAssert(c, "bar", "2")
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key, vs)
st.Unlock()
q := url.Values{}
q.Set("sequence", "2")
req, err := http.NewRequest("GET", fmt.Sprintf("/v2/validation-sets/%s/bar?%s", s.dev1acct.AccountID(), q.Encode()), nil)
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: s.dev1acct.AccountID(),
Name: "bar",
Sequence: 2,
Valid: true,
})
}
func (s *apiValidationSetsSuite) TestGetValidationSetPinnedNotFound(c *check.C) {
s.mockSeqFormingAssertionFn = func(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
return nil, &asserts.NotFoundError{
Type: assertType,
}
}
q := url.Values{}
q.Set("sequence", "333")
req, err := http.NewRequest("GET", "/v2/validation-sets/foo/bar?"+q.Encode(), nil)
c.Assert(err, check.IsNil)
st := s.d.Overlord().State()
st.Lock()
s.mockValidationSetsTracking(st)
st.Unlock()
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 404)
c.Check(string(rspe.Kind), check.Equals, "validation-set-not-found")
c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{
"account-id": "foo",
"name": "bar",
"sequence": 333,
})
}
func (s *apiValidationSetsSuite) TestApplyValidationSetMonitorModePinnedLocalOnly(c *check.C) {
st := s.d.Overlord().State()
st.Lock()
s.mockValidationSetsTracking(st)
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
as := s.mockAssert(c, "bar", "99")
err := assertstate.Add(st, as)
st.Unlock()
c.Assert(err, check.IsNil)
var called int
restore := daemon.MockAssertstateMonitorValidationSet(func(st *state.State, accountID, name string, sequence, userID int) (*assertstate.ValidationSetTracking, error) {
c.Assert(accountID, check.Equals, s.dev1acct.AccountID())
c.Assert(name, check.Equals, "bar")
c.Assert(sequence, check.Equals, 99)
called++
// Current should be the same as PinnedAt when PinnedAt != 0 but let's check api_validate is robust
return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, PinnedAt: 99, Current: 99999}, nil
})
defer restore()
restore = daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
// nil indicates successful validation
return nil
})
defer restore()
body := `{"action":"apply","mode":"monitor", "sequence":99}`
req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: s.dev1acct.AccountID(),
Name: "bar",
Mode: "monitor",
PinnedAt: 99,
Sequence: 99,
Valid: true,
})
c.Check(called, check.Equals, 1)
}
func (s *apiValidationSetsSuite) TestApplyValidationSetMonitorModeError(c *check.C) {
restore := daemon.MockAssertstateMonitorValidationSet(func(st *state.State, accountID, name string, sequence, userID int) (*assertstate.ValidationSetTracking, error) {
return nil, fmt.Errorf("boom")
})
defer restore()
body := `{"action":"apply","mode":"monitor"}`
req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 400)
c.Check(rspe.Message, check.Equals, fmt.Sprintf(`cannot monitor validation set %s/bar: boom`, s.dev1acct.AccountID()))
}
func (s *apiValidationSetsSuite) TestForgetValidationSet(c *check.C) {
st := s.d.Overlord().State()
for i, sequence := range []int{0, 9} {
st.Lock()
s.mockValidationSetsTracking(st)
st.Unlock()
var body string
if sequence != 0 {
body = fmt.Sprintf(`{"action":"forget", "sequence":%d}`, sequence)
} else {
body = `{"action":"forget"}`
}
var tr assertstate.ValidationSetTracking
st.Lock()
// validity, it exists before removing
err := assertstate.GetValidationSet(st, s.dev1acct.AccountID(), "foo", &tr)
st.Unlock()
c.Assert(err, check.IsNil)
c.Check(tr.AccountID, check.Equals, s.dev1acct.AccountID())
c.Check(tr.Name, check.Equals, "foo")
req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/foo", s.dev1acct.AccountID()), strings.NewReader(body))
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200, check.Commentf("case #%d", i))
// after forget it's removed
st.Lock()
err = assertstate.GetValidationSet(st, s.dev1acct.AccountID(), "foo", &tr)
st.Unlock()
c.Assert(err, testutil.ErrorIs, state.ErrNoState)
// and forget again fails
req, err = http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/foo", s.dev1acct.AccountID()), strings.NewReader(body))
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 404, check.Commentf("case #%d", i))
}
}
func (s *apiValidationSetsSuite) TestApplyValidationSetsErrors(c *check.C) {
st := s.d.Overlord().State()
st.Lock()
s.mockValidationSetsTracking(st)
st.Unlock()
for i, tc := range []struct {
validationSet string
mode string
// sequence is normally an int, use string for passing invalid ones.
sequence string
message string
status int
}{
{
validationSet: "0/zzz",
mode: "monitor",
message: `invalid account ID "0"`,
status: 400,
},
{
validationSet: "Xfoo/bar",
mode: "monitor",
message: `invalid account ID "Xfoo"`,
status: 400,
},
{
validationSet: "foo/Xabc",
mode: "monitor",
message: `invalid name "Xabc"`,
status: 400,
},
{
validationSet: "foo/bar",
sequence: "x",
message: "cannot decode request body into validation set action: invalid character 'x' looking for beginning of value",
status: 400,
},
{
validationSet: "foo/bar",
mode: "bad",
message: `invalid mode "bad"`,
status: 400,
},
{
validationSet: "foo/bar",
sequence: "-1",
mode: "monitor",
message: `invalid sequence argument: -1`,
status: 400,
},
} {
var body string
if tc.sequence != "" {
body = fmt.Sprintf(`{"action":"apply","mode":"%s", "sequence":%s}`, tc.mode, tc.sequence)
} else {
body = fmt.Sprintf(`{"action":"apply","mode":"%s"}`, tc.mode)
}
req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s", tc.validationSet), strings.NewReader(body))
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Check(rspe.Status, check.Equals, tc.status, check.Commentf("case #%d", i))
c.Check(rspe.Message, check.Matches, tc.message)
}
}
func (s *apiValidationSetsSuite) TestApplyValidationSetUnsupportedAction(c *check.C) {
body := `{"action":"baz","mode":"monitor"}`
req, err := http.NewRequest("POST", "/v2/validation-sets/foo/bar", strings.NewReader(body))
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Check(rspe.Status, check.Equals, 400)
c.Check(rspe.Message, check.Matches, `unsupported action "baz"`)
}
func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceMode(c *check.C) {
st := s.d.Overlord().State()
st.Lock()
defer st.Unlock()
s.mockValidationSetsTracking(st)
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
as := s.mockAssert(c, "bar", "99")
err := assertstate.Add(st, as)
c.Assert(err, check.IsNil)
var called int
restore := daemon.MockAssertstateFetchEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) {
c.Check(ignoreValidation, check.HasLen, 0)
c.Assert(accountID, check.Equals, s.dev1acct.AccountID())
c.Assert(name, check.Equals, "bar")
c.Assert(sequence, check.Equals, 0)
c.Check(userID, check.Equals, 0)
called++
return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, Mode: assertstate.Enforce, Current: 99}, nil
})
defer restore()
snapstate.Set(st, "snap-b", &snapstate.SnapState{
Active: true,
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}),
Current: snap.R(1),
})
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
st.Unlock()
defer st.Lock()
body := `{"action":"apply","mode":"enforce"}`
req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: s.dev1acct.AccountID(),
Name: "bar",
Mode: "enforce",
Sequence: 99,
Valid: true,
})
c.Check(called, check.Equals, 1)
}
func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeIgnoreValidationOK(c *check.C) {
st := s.d.Overlord().State()
st.Lock()
defer st.Unlock()
s.mockValidationSetsTracking(st)
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
as := s.mockAssert(c, "bar", "99")
err := assertstate.Add(st, as)
c.Assert(err, check.IsNil)
var called int
restore := daemon.MockAssertstateFetchEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) {
c.Check(ignoreValidation, check.DeepEquals, map[string]bool{"snap-b": true})
c.Check(snaps, testutil.DeepUnsortedMatches, []*snapasserts.InstalledSnap{
snapasserts.NewInstalledSnap("snap-b", "yOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.R("1"))})
c.Assert(accountID, check.Equals, s.dev1acct.AccountID())
c.Assert(name, check.Equals, "bar")
c.Assert(sequence, check.Equals, 0)
c.Check(userID, check.Equals, 0)
called++
return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, Mode: assertstate.Enforce, Current: 99}, nil
})
defer restore()
snapstate.Set(st, "snap-b", &snapstate.SnapState{
Active: true,
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}),
Current: snap.R(1),
Flags: snapstate.Flags{IgnoreValidation: true},
})
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
st.Unlock()
defer st.Lock()
body := `{"action":"apply","mode":"enforce"}`
req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: s.dev1acct.AccountID(),
Name: "bar",
Mode: "enforce",
Sequence: 99,
Valid: true,
})
c.Check(called, check.Equals, 1)
}
func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeSpecificSequence(c *check.C) {
st := s.d.Overlord().State()
st.Lock()
defer st.Unlock()
s.mockValidationSetsTracking(st)
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
as := s.mockAssert(c, "bar", "5")
err := assertstate.Add(st, as)
c.Assert(err, check.IsNil)
var called int
restore := daemon.MockAssertstateFetchEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) {
c.Assert(accountID, check.Equals, s.dev1acct.AccountID())
c.Assert(name, check.Equals, "bar")
c.Assert(sequence, check.Equals, 5)
c.Check(userID, check.Equals, 0)
called++
return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, Mode: assertstate.Enforce, PinnedAt: 5, Current: 5}, nil
})
defer restore()
snapstate.Set(st, "snap-b", &snapstate.SnapState{
Active: true,
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}),
Current: snap.R(1),
})
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
st.Unlock()
defer st.Lock()
body := `{"action":"apply","mode":"enforce","sequence":5}`
req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
res := rsp.Result.(daemon.ValidationSetResult)
c.Check(res, check.DeepEquals, daemon.ValidationSetResult{
AccountID: s.dev1acct.AccountID(),
Name: "bar",
Mode: "enforce",
PinnedAt: 5,
Sequence: 5,
Valid: true,
})
c.Check(called, check.Equals, 1)
}
func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeError(c *check.C) {
restore := daemon.MockAssertstateFetchEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) {
return nil, fmt.Errorf("boom")
})
defer restore()
st := s.d.Overlord().State()
st.Lock()
defer st.Unlock()
snapstate.Set(st, "snap-b", &snapstate.SnapState{
Active: true,
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}),
Current: snap.R(1),
})
assertstatetest.AddMany(st, s.dev1acct, s.acct1Key)
st.Unlock()
defer st.Lock()
body := `{"action":"apply","mode":"enforce"}`
req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body))
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 400)
c.Check(string(rspe.Message), check.Equals, "cannot enforce validation set: boom")
}