mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
Rename validationSetForTracked to validationSetsForTracking. Signed-off-by: Miguel Pires <miguel.pires@canonical.com>
418 lines
12 KiB
Go
418 lines
12 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
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"github.com/snapcore/snapd/asserts"
|
|
"github.com/snapcore/snapd/asserts/snapasserts"
|
|
"github.com/snapcore/snapd/client"
|
|
"github.com/snapcore/snapd/overlord/assertstate"
|
|
"github.com/snapcore/snapd/overlord/auth"
|
|
"github.com/snapcore/snapd/overlord/snapstate"
|
|
"github.com/snapcore/snapd/overlord/state"
|
|
"github.com/snapcore/snapd/release"
|
|
)
|
|
|
|
var (
|
|
validationSetsListCmd = &Command{
|
|
Path: "/v2/validation-sets",
|
|
GET: listValidationSets,
|
|
ReadAccess: authenticatedAccess{},
|
|
}
|
|
|
|
validationSetsCmd = &Command{
|
|
Path: "/v2/validation-sets/{account}/{name}",
|
|
GET: getValidationSet,
|
|
POST: applyValidationSet,
|
|
ReadAccess: authenticatedAccess{},
|
|
WriteAccess: authenticatedAccess{},
|
|
}
|
|
)
|
|
|
|
type validationSetResult struct {
|
|
AccountID string `json:"account-id"`
|
|
Name string `json:"name"`
|
|
PinnedAt int `json:"pinned-at,omitempty"`
|
|
Mode string `json:"mode,omitempty"`
|
|
Sequence int `json:"sequence,omitempty"`
|
|
Valid bool `json:"valid"`
|
|
// TODO: attributes for Notes column
|
|
}
|
|
|
|
func modeString(mode assertstate.ValidationSetMode) (string, error) {
|
|
switch mode {
|
|
case assertstate.Monitor:
|
|
return "monitor", nil
|
|
case assertstate.Enforce:
|
|
return "enforce", nil
|
|
}
|
|
return "", fmt.Errorf("internal error: unhandled mode %d", mode)
|
|
}
|
|
|
|
func validationSetNotFound(accountID, name string, sequence int) Response {
|
|
v := map[string]interface{}{
|
|
"account-id": accountID,
|
|
"name": name,
|
|
}
|
|
if sequence != 0 {
|
|
v["sequence"] = sequence
|
|
}
|
|
return &apiError{
|
|
Status: 404,
|
|
Message: "validation set not found",
|
|
Kind: client.ErrorKindValidationSetNotFound,
|
|
Value: v,
|
|
}
|
|
}
|
|
|
|
func listValidationSets(c *Command, r *http.Request, _ *auth.UserState) Response {
|
|
st := c.d.overlord.State()
|
|
st.Lock()
|
|
defer st.Unlock()
|
|
|
|
validationSets, err := assertstate.ValidationSets(st)
|
|
if err != nil {
|
|
return InternalError("accessing validation sets failed: %v", err)
|
|
}
|
|
|
|
names := make([]string, 0, len(validationSets))
|
|
for k := range validationSets {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
snaps, _, err := snapstate.InstalledSnaps(st)
|
|
if err != nil {
|
|
return InternalError(err.Error())
|
|
}
|
|
|
|
results := make([]validationSetResult, len(names))
|
|
for i, vs := range names {
|
|
tr := validationSets[vs]
|
|
sets, err := validationSetsForTracking(st, tr)
|
|
if err != nil {
|
|
return InternalError("cannot get assertion for validation set tracking %s/%s/%d: %v", tr.AccountID, tr.Name, tr.Sequence(), err)
|
|
}
|
|
// do not pass ignore validation map, we don't want to ignore validation and show invalid ones.
|
|
validErr := checkInstalledSnaps(sets, snaps, nil)
|
|
modeStr, err := modeString(tr.Mode)
|
|
if err != nil {
|
|
return InternalError(err.Error())
|
|
}
|
|
results[i] = validationSetResult{
|
|
AccountID: tr.AccountID,
|
|
Name: tr.Name,
|
|
PinnedAt: tr.PinnedAt,
|
|
Mode: modeStr,
|
|
Sequence: tr.Sequence(),
|
|
Valid: validErr == nil,
|
|
}
|
|
}
|
|
|
|
return SyncResponse(results)
|
|
}
|
|
|
|
var checkInstalledSnaps = func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error {
|
|
return vsets.CheckInstalledSnaps(snaps, ignoreValidation)
|
|
}
|
|
|
|
func validationSetResultFromTracking(st *state.State, tr *assertstate.ValidationSetTracking) (*validationSetResult, error) {
|
|
modeStr, err := modeString(tr.Mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sets, err := validationSetsForTracking(st, tr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snaps, _, err := snapstate.InstalledSnaps(st)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
validErr := checkInstalledSnaps(sets, snaps, nil)
|
|
return &validationSetResult{
|
|
AccountID: tr.AccountID,
|
|
Name: tr.Name,
|
|
PinnedAt: tr.PinnedAt,
|
|
Mode: modeStr,
|
|
Sequence: tr.Sequence(),
|
|
Valid: validErr == nil,
|
|
}, nil
|
|
}
|
|
|
|
func getValidationSet(c *Command, r *http.Request, user *auth.UserState) Response {
|
|
vars := muxVars(r)
|
|
accountID := vars["account"]
|
|
name := vars["name"]
|
|
|
|
if !asserts.IsValidAccountID(accountID) {
|
|
return BadRequest("invalid account ID %q", accountID)
|
|
}
|
|
if !asserts.IsValidValidationSetName(name) {
|
|
return BadRequest("invalid name %q", name)
|
|
}
|
|
|
|
query := r.URL.Query()
|
|
|
|
// sequence is optional
|
|
sequenceStr := query.Get("sequence")
|
|
var sequence int
|
|
if sequenceStr != "" {
|
|
var err error
|
|
sequence, err = strconv.Atoi(sequenceStr)
|
|
if err != nil {
|
|
return BadRequest("invalid sequence argument")
|
|
}
|
|
if sequence < 0 {
|
|
return BadRequest("invalid sequence argument: %d", sequence)
|
|
}
|
|
}
|
|
|
|
st := c.d.overlord.State()
|
|
st.Lock()
|
|
defer st.Unlock()
|
|
|
|
var tr assertstate.ValidationSetTracking
|
|
err := assertstate.GetValidationSet(st, accountID, name, &tr)
|
|
if errors.Is(err, state.ErrNoState) || (err == nil && sequence != 0 && sequence != tr.PinnedAt) {
|
|
// not available locally, try to find in the store.
|
|
return validateAgainstStore(st, accountID, name, sequence, user)
|
|
}
|
|
if err != nil {
|
|
return InternalError("accessing validation sets failed: %v", err)
|
|
}
|
|
|
|
// evaluate against installed snaps
|
|
res, err := validationSetResultFromTracking(st, &tr)
|
|
if err != nil {
|
|
return InternalError(err.Error())
|
|
}
|
|
return SyncResponse(*res)
|
|
}
|
|
|
|
type validationSetApplyRequest struct {
|
|
Action string `json:"action"`
|
|
Mode string `json:"mode"`
|
|
Sequence int `json:"sequence,omitempty"`
|
|
}
|
|
|
|
func applyValidationSet(c *Command, r *http.Request, user *auth.UserState) Response {
|
|
vars := muxVars(r)
|
|
accountID := vars["account"]
|
|
name := vars["name"]
|
|
|
|
if !asserts.IsValidAccountID(accountID) {
|
|
return BadRequest("invalid account ID %q", accountID)
|
|
}
|
|
if !asserts.IsValidValidationSetName(name) {
|
|
return BadRequest("invalid name %q", name)
|
|
}
|
|
|
|
var req validationSetApplyRequest
|
|
decoder := json.NewDecoder(r.Body)
|
|
if err := decoder.Decode(&req); err != nil {
|
|
return BadRequest("cannot decode request body into validation set action: %v", err)
|
|
}
|
|
if decoder.More() {
|
|
return BadRequest("extra content found in request body")
|
|
}
|
|
if req.Sequence < 0 {
|
|
return BadRequest("invalid sequence argument: %d", req.Sequence)
|
|
}
|
|
|
|
st := c.d.overlord.State()
|
|
st.Lock()
|
|
defer st.Unlock()
|
|
|
|
switch req.Action {
|
|
case "forget":
|
|
return forgetValidationSet(st, accountID, name, req.Sequence)
|
|
case "apply":
|
|
return updateValidationSet(st, accountID, name, req.Mode, req.Sequence, user)
|
|
default:
|
|
return BadRequest("unsupported action %q", req.Action)
|
|
}
|
|
}
|
|
|
|
var assertstateMonitorValidationSet = assertstate.MonitorValidationSet
|
|
var assertstateFetchAndApplyEnforcedValidationSet = assertstate.FetchAndApplyEnforcedValidationSet
|
|
var assertstateTryEnforcedValidationSets = assertstate.TryEnforcedValidationSets
|
|
|
|
// updateValidationSet handles snap validate --monitor and --enforce accountId/name[=sequence].
|
|
func updateValidationSet(st *state.State, accountID, name string, reqMode string, sequence int, user *auth.UserState) Response {
|
|
var mode assertstate.ValidationSetMode
|
|
switch reqMode {
|
|
case "monitor":
|
|
mode = assertstate.Monitor
|
|
case "enforce":
|
|
mode = assertstate.Enforce
|
|
default:
|
|
return BadRequest("invalid mode %q", reqMode)
|
|
}
|
|
|
|
userID := 0
|
|
if user != nil {
|
|
userID = user.ID
|
|
}
|
|
|
|
if mode == assertstate.Enforce {
|
|
return enforceValidationSet(st, accountID, name, sequence, userID)
|
|
}
|
|
|
|
tr, err := assertstateMonitorValidationSet(st, accountID, name, sequence, userID)
|
|
if err != nil {
|
|
return BadRequest("cannot get validation set assertion for %v: %v", assertstate.ValidationSetKey(accountID, name), err)
|
|
}
|
|
|
|
res, err := validationSetResultFromTracking(st, tr)
|
|
if err != nil {
|
|
return InternalError(err.Error())
|
|
}
|
|
return SyncResponse(*res)
|
|
}
|
|
|
|
// forgetValidationSet forgets the validation set.
|
|
// The state needs to be locked by the caller.
|
|
func forgetValidationSet(st *state.State, accountID, name string, sequence int) Response {
|
|
// check if it exists first
|
|
var tr assertstate.ValidationSetTracking
|
|
err := assertstate.GetValidationSet(st, accountID, name, &tr)
|
|
if errors.Is(err, state.ErrNoState) || (err == nil && sequence != 0 && sequence != tr.PinnedAt) {
|
|
return validationSetNotFound(accountID, name, sequence)
|
|
}
|
|
if err != nil {
|
|
return InternalError("accessing validation sets failed: %v", err)
|
|
}
|
|
if err := assertstate.ForgetValidationSet(st, accountID, name); err != nil {
|
|
return BadRequest("cannot forget validation set for %v: %v", assertstate.ValidationSetKey(accountID, name), err)
|
|
}
|
|
return SyncResponse(nil)
|
|
}
|
|
|
|
func validationSetsForTracking(st *state.State, tr *assertstate.ValidationSetTracking) (*snapasserts.ValidationSets, error) {
|
|
as, err := validationSetAssertFromDb(st, tr.AccountID, tr.Name, tr.Sequence())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sets := snapasserts.NewValidationSets()
|
|
if err := sets.Add(as); err != nil {
|
|
return nil, err
|
|
}
|
|
return sets, nil
|
|
}
|
|
|
|
func validationSetAssertFromDb(st *state.State, accountID, name string, sequence int) (*asserts.ValidationSet, error) {
|
|
headers := map[string]string{
|
|
"series": release.Series,
|
|
"account-id": accountID,
|
|
"name": name,
|
|
"sequence": fmt.Sprintf("%d", sequence),
|
|
}
|
|
db := assertstate.DB(st)
|
|
as, err := db.Find(asserts.ValidationSetType, headers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vset := as.(*asserts.ValidationSet)
|
|
return vset, nil
|
|
}
|
|
|
|
func validateAgainstStore(st *state.State, accountID, name string, sequence int, user *auth.UserState) Response {
|
|
// get from the store
|
|
as, err := getSingleSeqFormingAssertion(st, accountID, name, sequence, user)
|
|
if _, ok := err.(*asserts.NotFoundError); ok {
|
|
// not in the store - try to find in the database
|
|
as, err = validationSetAssertFromDb(st, accountID, name, sequence)
|
|
if _, ok := err.(*asserts.NotFoundError); ok {
|
|
return validationSetNotFound(accountID, name, sequence)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return InternalError(err.Error())
|
|
}
|
|
sets := snapasserts.NewValidationSets()
|
|
vset := as.(*asserts.ValidationSet)
|
|
if err := sets.Add(vset); err != nil {
|
|
return InternalError(err.Error())
|
|
}
|
|
snaps, _, err := snapstate.InstalledSnaps(st)
|
|
if err != nil {
|
|
return InternalError(err.Error())
|
|
}
|
|
|
|
validErr := checkInstalledSnaps(sets, snaps, nil)
|
|
res := validationSetResult{
|
|
AccountID: vset.AccountID(),
|
|
Name: vset.Name(),
|
|
Sequence: vset.Sequence(),
|
|
// TODO: pass actual err details and implement "verbose" mode
|
|
// for the client?
|
|
Valid: validErr == nil,
|
|
}
|
|
return SyncResponse(res)
|
|
}
|
|
|
|
func getSingleSeqFormingAssertion(st *state.State, accountID, name string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
|
|
sto := snapstate.Store(st, nil)
|
|
at := asserts.Type("validation-set")
|
|
if at == nil {
|
|
panic("validation-set assert type not found")
|
|
}
|
|
|
|
sequenceKey := []string{release.Series, accountID, name}
|
|
st.Unlock()
|
|
as, err := sto.SeqFormingAssertion(at, sequenceKey, sequence, user)
|
|
st.Lock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return as, nil
|
|
}
|
|
|
|
func enforceValidationSet(st *state.State, accountID, name string, sequence, userID int) Response {
|
|
snaps, ignoreValidation, err := snapstate.InstalledSnaps(st)
|
|
if err != nil {
|
|
return InternalError(err.Error())
|
|
}
|
|
tr, err := assertstateFetchAndApplyEnforcedValidationSet(st, accountID, name, sequence, userID, snaps, ignoreValidation)
|
|
if err != nil {
|
|
// XXX: provide more specific error kinds? This would probably require
|
|
// assertstate.ValidationSetAssertionForEnforce tuning too.
|
|
return BadRequest("cannot enforce validation set: %v", err)
|
|
}
|
|
|
|
res, err := validationSetResultFromTracking(st, tr)
|
|
if err != nil {
|
|
return InternalError(err.Error())
|
|
}
|
|
return SyncResponse(*res)
|
|
}
|