github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/asserts/validation_set.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package asserts
    21  
    22  import (
    23  	"fmt"
    24  	"regexp"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/snapcore/snapd/snap/naming"
    29  	"github.com/snapcore/snapd/strutil"
    30  )
    31  
    32  // Presence represents a presence constraint.
    33  type Presence string
    34  
    35  const (
    36  	PresenceRequired Presence = "required"
    37  	PresenceOptional Presence = "optional"
    38  	PresenceInvalid  Presence = "invalid"
    39  )
    40  
    41  func presencesAsStrings(presences ...Presence) []string {
    42  	strs := make([]string, len(presences))
    43  	for i, pres := range presences {
    44  		strs[i] = string(pres)
    45  	}
    46  	return strs
    47  }
    48  
    49  var validValidationSetSnapPresences = presencesAsStrings(PresenceRequired, PresenceOptional, PresenceInvalid)
    50  
    51  func checkPresence(snap map[string]interface{}, which string, valid []string) (Presence, error) {
    52  	presence, err := checkOptionalStringWhat(snap, "presence", which)
    53  	if err != nil {
    54  		return Presence(""), err
    55  	}
    56  	if presence != "" && !strutil.ListContains(valid, presence) {
    57  		return Presence(""), fmt.Errorf("presence %s must be one of %s", which, strings.Join(valid, "|"))
    58  	}
    59  	return Presence(presence), nil
    60  }
    61  
    62  // ValidationSetSnap holds the details about a snap constrained by a validation-set assertion.
    63  type ValidationSetSnap struct {
    64  	Name   string
    65  	SnapID string
    66  
    67  	Presence Presence
    68  
    69  	Revision int
    70  }
    71  
    72  // SnapName implements naming.SnapRef.
    73  func (s *ValidationSetSnap) SnapName() string {
    74  	return s.Name
    75  }
    76  
    77  // ID implements naming.SnapRef.
    78  func (s *ValidationSetSnap) ID() string {
    79  	return s.SnapID
    80  }
    81  
    82  func checkValidationSetSnap(snap map[string]interface{}) (*ValidationSetSnap, error) {
    83  	name, err := checkNotEmptyStringWhat(snap, "name", "of snap")
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	if err := naming.ValidateSnap(name); err != nil {
    88  		return nil, fmt.Errorf("invalid snap name %q", name)
    89  	}
    90  
    91  	what := fmt.Sprintf("of snap %q", name)
    92  
    93  	snapID, err := checkStringMatchesWhat(snap, "id", what, naming.ValidSnapID)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  
    98  	presence, err := checkPresence(snap, what, validValidationSetSnapPresences)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	var snapRevision int
   104  	if _, ok := snap["revision"]; ok {
   105  		var err error
   106  		snapRevision, err = checkSnapRevisionWhat(snap, "revision", what)
   107  		if err != nil {
   108  			return nil, err
   109  		}
   110  	}
   111  	if snapRevision != 0 && presence == PresenceInvalid {
   112  		return nil, fmt.Errorf(`cannot specify revision %s at the same time as stating its presence is invalid`, what)
   113  	}
   114  
   115  	return &ValidationSetSnap{
   116  		Name:     name,
   117  		SnapID:   snapID,
   118  		Presence: presence,
   119  		Revision: snapRevision,
   120  	}, nil
   121  }
   122  
   123  func checkValidationSetSnaps(snapList interface{}) ([]*ValidationSetSnap, error) {
   124  	const wrongHeaderType = `"snaps" header must be a list of maps`
   125  
   126  	entries, ok := snapList.([]interface{})
   127  	if !ok {
   128  		return nil, fmt.Errorf(wrongHeaderType)
   129  	}
   130  
   131  	seen := make(map[string]bool, len(entries))
   132  	seenIDs := make(map[string]string, len(entries))
   133  	snaps := make([]*ValidationSetSnap, 0, len(entries))
   134  	for _, entry := range entries {
   135  		snap, ok := entry.(map[string]interface{})
   136  		if !ok {
   137  			return nil, fmt.Errorf(wrongHeaderType)
   138  		}
   139  		valSetSnap, err := checkValidationSetSnap(snap)
   140  		if err != nil {
   141  			return nil, err
   142  		}
   143  
   144  		if seen[valSetSnap.Name] {
   145  			return nil, fmt.Errorf("cannot list the same snap %q multiple times", valSetSnap.Name)
   146  		}
   147  		seen[valSetSnap.Name] = true
   148  		snapID := valSetSnap.SnapID
   149  		if underName := seenIDs[snapID]; underName != "" {
   150  			return nil, fmt.Errorf("cannot specify the same snap id %q multiple times, specified for snaps %q and %q", snapID, underName, valSetSnap.Name)
   151  		}
   152  		seenIDs[snapID] = valSetSnap.Name
   153  
   154  		if valSetSnap.Presence == "" {
   155  			valSetSnap.Presence = PresenceRequired
   156  		}
   157  
   158  		snaps = append(snaps, valSetSnap)
   159  	}
   160  
   161  	return snaps, nil
   162  }
   163  
   164  // ValidationSet holds a validation-set assertion, which is a
   165  // statement by an account about a set snaps and possibly revisions
   166  // for which an extrinsic/implied property is valid (e.g. they work
   167  // well together). validation-sets are organized in sequences under a
   168  // name.
   169  type ValidationSet struct {
   170  	assertionBase
   171  
   172  	seq int
   173  
   174  	snaps []*ValidationSetSnap
   175  
   176  	timestamp time.Time
   177  }
   178  
   179  // Series returns the series for which the snap in the set are declared.
   180  func (vs *ValidationSet) Series() string {
   181  	return vs.HeaderString("series")
   182  }
   183  
   184  // AccountID returns the identifier of the account that signed this assertion.
   185  func (vs *ValidationSet) AccountID() string {
   186  	return vs.HeaderString("account-id")
   187  }
   188  
   189  // Name returns the name under which the validation-set is organized.
   190  func (vs *ValidationSet) Name() string {
   191  	return vs.HeaderString("name")
   192  }
   193  
   194  // Sequence returns the sequential number of the validation-set in its
   195  // named sequence.
   196  func (vs *ValidationSet) Sequence() int {
   197  	return vs.seq
   198  }
   199  
   200  // Snaps returns the constrained snaps by the validation-set.
   201  func (vs *ValidationSet) Snaps() []*ValidationSetSnap {
   202  	return vs.snaps
   203  }
   204  
   205  // Timestamp returns the time when the validation-set was issued.
   206  func (vs *ValidationSet) Timestamp() time.Time {
   207  	return vs.timestamp
   208  }
   209  
   210  func checkSequence(headers map[string]interface{}, name string) (int, error) {
   211  	seqnum, err := checkInt(headers, name)
   212  	if err != nil {
   213  		return -1, err
   214  	}
   215  	if seqnum < 1 {
   216  		return -1, fmt.Errorf("%q must be >=1: %v", name, seqnum)
   217  	}
   218  	return seqnum, nil
   219  }
   220  
   221  var (
   222  	validValidationSetName = regexp.MustCompile("^[a-z0-9](?:-?[a-z0-9])*$")
   223  )
   224  
   225  func assembleValidationSet(assert assertionBase) (Assertion, error) {
   226  	authorityID := assert.AuthorityID()
   227  	accountID := assert.HeaderString("account-id")
   228  	if accountID != authorityID {
   229  		return nil, fmt.Errorf("authority-id and account-id must match, validation-set assertions are expected to be signed by the issuer account: %q != %q", authorityID, accountID)
   230  	}
   231  
   232  	_, err := checkStringMatches(assert.headers, "name", validValidationSetName)
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	seq, err := checkSequence(assert.headers, "sequence")
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	snapList, ok := assert.headers["snaps"]
   243  	if !ok {
   244  		return nil, fmt.Errorf(`"snaps" header is mandatory`)
   245  	}
   246  	snaps, err := checkValidationSetSnaps(snapList)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  
   251  	timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	return &ValidationSet{
   257  		assertionBase: assert,
   258  		seq:           seq,
   259  		snaps:         snaps,
   260  		timestamp:     timestamp,
   261  	}, nil
   262  }
   263  
   264  func IsValidValidationSetName(name string) bool {
   265  	return validValidationSetName.MatchString(name)
   266  }