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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 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_test
    21  
    22  import (
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"strings"
    28  	"time"
    29  
    30  	. "gopkg.in/check.v1"
    31  
    32  	"github.com/snapcore/snapd/asserts"
    33  )
    34  
    35  var (
    36  	_ = Suite(&repairSuite{})
    37  )
    38  
    39  type repairSuite struct {
    40  	modelsLine string
    41  	ts         time.Time
    42  	tsLine     string
    43  	basesLine  string
    44  	modesLine  string
    45  
    46  	repairStr string
    47  }
    48  
    49  const script = `#!/bin/sh
    50  set -e
    51  echo "Unpack embedded payload"
    52  match=$(grep --text --line-number '^PAYLOAD:$' $0 | cut -d ':' -f 1)
    53  payload_start=$((match + 1))
    54  # Using "base64" as its part of coreutils which should be available
    55  # everywhere
    56  tail -n +$payload_start $0 | base64 --decode - | tar -xzf -
    57  # run embedded content
    58  ./hello
    59  exit 0
    60  # payload generated with, may contain binary data
    61  #   printf '#!/bin/sh\necho hello from the inside\n' > hello
    62  #   chmod +x hello
    63  #   tar czf - hello | base64 -
    64  PAYLOAD:
    65  H4sIAJJt+FgAA+3STQrCMBDF8ax7ihEP0CkxyXn8iCZQE2jr/W11Iwi6KiL8f5u3mLd4i0mx76tZ
    66  l86Cc0t2welrPu2c6awGr95bG4x26rw1oivveriN034QMfFSy6fet/uf2m7aQy7tmJp4TFXS8g5y
    67  HupVphQllzGfYvPrkQAAAAAAAAAAAAAAAACAN3dTp9TNACgAAA==
    68  `
    69  
    70  var repairExample = fmt.Sprintf("type: repair\n"+
    71  	"authority-id: acme\n"+
    72  	"brand-id: acme\n"+
    73  	"summary: example repair\n"+
    74  	"architectures:\n"+
    75  	"  - amd64\n"+
    76  	"  - arm64\n"+
    77  	"repair-id: 42\n"+
    78  	"series:\n"+
    79  	"  - 16\n"+
    80  	"MODELSLINE"+
    81  	"TSLINE"+
    82  	"BASESLINE"+
    83  	"MODESLINE"+
    84  	"body-length: %v\n"+
    85  	"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij"+
    86  	"\n\n"+
    87  	script+"\n\n"+
    88  	"AXNpZw==", len(script))
    89  
    90  func (s *repairSuite) SetUpTest(c *C) {
    91  	s.modelsLine = "models:\n  - acme/frobinator\n"
    92  	s.ts = time.Now().Truncate(time.Second).UTC()
    93  	s.tsLine = "timestamp: " + s.ts.Format(time.RFC3339) + "\n"
    94  	s.basesLine = "bases:\n  - core20\n"
    95  	s.modesLine = "modes:\n  - run\n"
    96  
    97  	s.repairStr = strings.Replace(repairExample, "MODELSLINE", s.modelsLine, 1)
    98  	s.repairStr = strings.Replace(s.repairStr, "TSLINE", s.tsLine, 1)
    99  	s.repairStr = strings.Replace(s.repairStr, "BASESLINE", s.basesLine, 1)
   100  	s.repairStr = strings.Replace(s.repairStr, "MODESLINE", s.modesLine, 1)
   101  }
   102  
   103  func (s *repairSuite) TestDecodeOK(c *C) {
   104  	a, err := asserts.Decode([]byte(s.repairStr))
   105  	c.Assert(err, IsNil)
   106  	c.Check(a.Type(), Equals, asserts.RepairType)
   107  	_, ok := a.(asserts.SequenceMember)
   108  	c.Assert(ok, Equals, true)
   109  	repair := a.(*asserts.Repair)
   110  	c.Check(repair.Timestamp(), Equals, s.ts)
   111  	c.Check(repair.BrandID(), Equals, "acme")
   112  	c.Check(repair.RepairID(), Equals, 42)
   113  	c.Check(repair.Sequence(), Equals, 42)
   114  	c.Check(repair.Summary(), Equals, "example repair")
   115  	c.Check(repair.Series(), DeepEquals, []string{"16"})
   116  	c.Check(repair.Bases(), DeepEquals, []string{"core20"})
   117  	c.Check(repair.Modes(), DeepEquals, []string{"run"})
   118  	c.Check(repair.Architectures(), DeepEquals, []string{"amd64", "arm64"})
   119  	c.Check(repair.Models(), DeepEquals, []string{"acme/frobinator"})
   120  	c.Check(string(repair.Body()), Equals, script)
   121  }
   122  
   123  const (
   124  	repairErrPrefix = "assertion repair: "
   125  )
   126  
   127  func (s *repairSuite) TestDisabled(c *C) {
   128  	disabledTests := []struct {
   129  		disabled, expectedErr string
   130  		dis                   bool
   131  	}{
   132  		{"true", "", true},
   133  		{"false", "", false},
   134  		{"foo", `"disabled" header must be 'true' or 'false'`, false},
   135  	}
   136  
   137  	for _, test := range disabledTests {
   138  		repairStr := strings.Replace(repairExample, "MODELSLINE", fmt.Sprintf("disabled: %s\n", test.disabled), 1)
   139  		repairStr = strings.Replace(repairStr, "TSLINE", s.tsLine, 1)
   140  		repairStr = strings.Replace(repairStr, "BASESLINE", "", 1)
   141  		repairStr = strings.Replace(repairStr, "MODESLINE", "", 1)
   142  
   143  		a, err := asserts.Decode([]byte(repairStr))
   144  		if test.expectedErr != "" {
   145  			c.Check(err, ErrorMatches, repairErrPrefix+test.expectedErr)
   146  		} else {
   147  			c.Assert(err, IsNil)
   148  			repair := a.(*asserts.Repair)
   149  			c.Check(repair.Disabled(), Equals, test.dis)
   150  		}
   151  	}
   152  }
   153  
   154  func (s *repairSuite) TestDecodeModesAndBases(c *C) {
   155  	tt := []struct {
   156  		comment  string
   157  		bases    []string
   158  		modes    []string
   159  		expbases []string
   160  		expmodes []string
   161  		err      string
   162  	}{
   163  		// happy uc20+ cases
   164  		{
   165  			comment:  "core20 base with run mode",
   166  			bases:    []string{"core20"},
   167  			modes:    []string{"run"},
   168  			expbases: []string{"core20"},
   169  			expmodes: []string{"run"},
   170  		},
   171  		{
   172  			comment:  "core20 base with recover mode",
   173  			bases:    []string{"core20"},
   174  			modes:    []string{"recover"},
   175  			expbases: []string{"core20"},
   176  			expmodes: []string{"recover"},
   177  		},
   178  		{
   179  			comment:  "core20 base with recover and run modes",
   180  			bases:    []string{"core20"},
   181  			modes:    []string{"recover", "run"},
   182  			expbases: []string{"core20"},
   183  			expmodes: []string{"recover", "run"},
   184  		},
   185  		{
   186  			comment:  "core22 base with run mode",
   187  			bases:    []string{"core22"},
   188  			modes:    []string{"run"},
   189  			expbases: []string{"core22"},
   190  			expmodes: []string{"run"},
   191  		},
   192  		{
   193  			comment:  "core20 and core22 bases with run mode",
   194  			bases:    []string{"core20", "core22"},
   195  			modes:    []string{"run"},
   196  			expbases: []string{"core20", "core22"},
   197  			expmodes: []string{"run"},
   198  		},
   199  		{
   200  			comment:  "core20 and core22 bases with run and recover modes",
   201  			bases:    []string{"core20", "core22"},
   202  			modes:    []string{"run", "recover"},
   203  			expbases: []string{"core20", "core22"},
   204  			expmodes: []string{"run", "recover"},
   205  		},
   206  		{
   207  			comment:  "all bases with run mode (effectively all uc20 bases)",
   208  			modes:    []string{"run"},
   209  			expmodes: []string{"run"},
   210  		},
   211  		{
   212  			comment:  "all bases with recover mode (effectively all uc20 bases)",
   213  			modes:    []string{"recover"},
   214  			expmodes: []string{"recover"},
   215  		},
   216  		{
   217  			comment:  "core20 base with empty modes",
   218  			bases:    []string{"core20"},
   219  			expbases: []string{"core20"},
   220  		},
   221  		{
   222  			comment:  "core22 base with empty modes",
   223  			bases:    []string{"core22"},
   224  			expbases: []string{"core22"},
   225  		},
   226  
   227  		// unhappy uc20 cases
   228  		{
   229  			comment: "core20 base with single invalid mode",
   230  			bases:   []string{"core20"},
   231  			modes:   []string{"not-a-real-uc20-mode"},
   232  			err:     `assertion repair: header \"modes\" contains an invalid element: \"not-a-real-uc20-mode\" \(valid values are run and recover\)`,
   233  		},
   234  		{
   235  			comment: "core20 base with invalid modes",
   236  			bases:   []string{"core20"},
   237  			modes:   []string{"run", "not-a-real-uc20-mode"},
   238  			err:     `assertion repair: header \"modes\" contains an invalid element: \"not-a-real-uc20-mode\" \(valid values are run and recover\)`,
   239  		},
   240  		{
   241  			comment: "core20 base with install mode",
   242  			bases:   []string{"core20"},
   243  			modes:   []string{"install"},
   244  			err:     `assertion repair: header \"modes\" contains an invalid element: \"install\" \(valid values are run and recover\)`,
   245  		},
   246  
   247  		// happy uc18/uc16 cases
   248  		{
   249  			comment:  "core18 base with empty modes",
   250  			bases:    []string{"core18"},
   251  			expbases: []string{"core18"},
   252  		},
   253  		{
   254  			comment:  "core base with empty modes",
   255  			bases:    []string{"core"},
   256  			expbases: []string{"core"},
   257  		},
   258  
   259  		// unhappy uc18/uc16 cases
   260  		{
   261  			comment: "core18 base with non-empty modes",
   262  			bases:   []string{"core18"},
   263  			modes:   []string{"run"},
   264  			err:     "assertion repair: in the presence of a non-empty \"modes\" header, \"bases\" must only contain base snaps supporting recovery modes",
   265  		},
   266  		{
   267  			comment: "core base with non-empty modes",
   268  			bases:   []string{"core"},
   269  			modes:   []string{"run"},
   270  			err:     "assertion repair: in the presence of a non-empty \"modes\" header, \"bases\" must only contain base snaps supporting recovery modes",
   271  		},
   272  		{
   273  			comment: "core16 base with non-empty modes",
   274  			bases:   []string{"core16"},
   275  			modes:   []string{"run"},
   276  			err:     "assertion repair: in the presence of a non-empty \"modes\" header, \"bases\" must only contain base snaps supporting recovery modes",
   277  		},
   278  
   279  		// unhappy non-core specific cases
   280  		{
   281  			comment: "invalid snap name as base",
   282  			bases:   []string{"foo....bar"},
   283  			err:     "assertion repair: invalid snap name \"foo....bar\" in \"bases\"",
   284  		},
   285  	}
   286  
   287  	for _, t := range tt {
   288  		comment := Commentf(t.comment)
   289  		repairStr := strings.Replace(repairExample, "MODELSLINE", s.modelsLine, 1)
   290  		repairStr = strings.Replace(repairStr, "TSLINE", s.tsLine, 1)
   291  
   292  		var basesStr, modesStr string
   293  		if len(t.bases) != 0 {
   294  			basesStr = "bases:\n"
   295  			for _, b := range t.bases {
   296  				basesStr += "  - " + b + "\n"
   297  			}
   298  		}
   299  		if len(t.modes) != 0 {
   300  			modesStr = "modes:\n"
   301  			for _, m := range t.modes {
   302  				modesStr += "  - " + m + "\n"
   303  			}
   304  		}
   305  
   306  		repairStr = strings.Replace(repairStr, "BASESLINE", basesStr, 1)
   307  		repairStr = strings.Replace(repairStr, "MODESLINE", modesStr, 1)
   308  
   309  		assert, err := asserts.Decode([]byte(repairStr))
   310  		if t.err != "" {
   311  			c.Assert(err, ErrorMatches, t.err, comment)
   312  		} else {
   313  			c.Assert(err, IsNil, comment)
   314  			repair, ok := assert.(*asserts.Repair)
   315  			c.Assert(ok, Equals, true, comment)
   316  
   317  			c.Assert(repair.Bases(), DeepEquals, t.expbases, comment)
   318  			c.Assert(repair.Modes(), DeepEquals, t.expmodes, comment)
   319  		}
   320  	}
   321  }
   322  
   323  func (s *repairSuite) TestDecodeInvalid(c *C) {
   324  	invalidTests := []struct{ original, invalid, expectedErr string }{
   325  		{"series:\n  - 16\n", "series: \n", `"series" header must be a list of strings`},
   326  		{"series:\n  - 16\n", "series: something\n", `"series" header must be a list of strings`},
   327  		{"architectures:\n  - amd64\n  - arm64\n", "architectures: foo\n", `"architectures" header must be a list of strings`},
   328  		{"models:\n  - acme/frobinator\n", "models: \n", `"models" header must be a list of strings`},
   329  		{"models:\n  - acme/frobinator\n", "models: something\n", `"models" header must be a list of strings`},
   330  		{"repair-id: 42\n", "repair-id: no-number\n", `"repair-id" header is not an integer: no-number`},
   331  		{"repair-id: 42\n", "repair-id: 0\n", `"repair-id" must be >=1: 0`},
   332  		{"repair-id: 42\n", "repair-id: 01\n", `"repair-id" header has invalid prefix zeros: 01`},
   333  		{"repair-id: 42\n", "repair-id: 99999999999999999999\n", `"repair-id" header is out of range: 99999999999999999999`},
   334  		{"brand-id: acme\n", "brand-id: brand-id-not-eq-authority-id\n", `authority-id and brand-id must match, repair assertions are expected to be signed by the brand: "acme" != "brand-id-not-eq-authority-id"`},
   335  		{"summary: example repair\n", "", `"summary" header is mandatory`},
   336  		{"summary: example repair\n", "summary: \n", `"summary" header should not be empty`},
   337  		{"summary: example repair\n", "summary:\n    multi\n    line\n", `"summary" header cannot have newlines`},
   338  		{s.tsLine, "", `"timestamp" header is mandatory`},
   339  		{s.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
   340  		{s.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
   341  	}
   342  
   343  	for _, test := range invalidTests {
   344  		invalid := strings.Replace(s.repairStr, test.original, test.invalid, 1)
   345  		_, err := asserts.Decode([]byte(invalid))
   346  		c.Check(err, ErrorMatches, repairErrPrefix+test.expectedErr)
   347  	}
   348  }
   349  
   350  // FIXME: move to a different layer later
   351  func (s *repairSuite) TestRepairCanEmbedScripts(c *C) {
   352  	a, err := asserts.Decode([]byte(s.repairStr))
   353  	c.Assert(err, IsNil)
   354  	c.Check(a.Type(), Equals, asserts.RepairType)
   355  	repair := a.(*asserts.Repair)
   356  
   357  	tmpdir := c.MkDir()
   358  	repairScript := filepath.Join(tmpdir, "repair")
   359  	err = ioutil.WriteFile(repairScript, []byte(repair.Body()), 0755)
   360  	c.Assert(err, IsNil)
   361  	cmd := exec.Command(repairScript)
   362  	cmd.Dir = tmpdir
   363  	output, err := cmd.CombinedOutput()
   364  	c.Check(err, IsNil)
   365  	c.Check(string(output), Equals, `Unpack embedded payload
   366  hello from the inside
   367  `)
   368  }