diff options
| author | Michael Vogt <mvo@ubuntu.com> | 2015-11-18 08:40:01 +0100 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2015-11-18 08:40:01 +0100 |
| commit | 86cba9607712a5e626da0f7958fc24fdd27d218d (patch) | |
| tree | ae6832a3a7bd00c820152596796ccf2b7cc515b8 | |
| parent | 1b532e0faf30426e06bfa4269c92c27dc7b26329 (diff) | |
| parent | 09157d04e0997225d64f7ebbe831e9c82cd2615d (diff) | |
Merge remote-tracking branch 'upstream/master' into feature/bootloader-set-bootvar2feature/bootloader-set-bootvar2
62 files changed, 3344 insertions, 1305 deletions
diff --git a/arch/arch.go b/arch/arch.go new file mode 100644 index 0000000000..206ad0db33 --- /dev/null +++ b/arch/arch.go @@ -0,0 +1,82 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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 arch + +import ( + "log" + "runtime" +) + +// ArchitectureType is the type for a supported snappy architecture +type ArchitectureType string + +// arch is global to allow tools like ubuntu-device-flash to +// change the architecture. This is important to e.g. install +// armhf snaps onto a armhf image that is generated on an amd64 +// machine +var arch = ArchitectureType(ubuntuArchFromGoArch(runtime.GOARCH)) + +// SetArchitecture allows overriding the auto detected Architecture +func SetArchitecture(newArch ArchitectureType) { + arch = newArch +} + +// UbuntuArchitecture returns the debian equivalent architecture for the +// currently running architecture. +// +// If the architecture does not map any debian architecture, the +// GOARCH is returned. +func UbuntuArchitecture() string { + return string(arch) +} + +// ubuntuArchFromGoArch maps a go architecture string to the coresponding +// Ubuntu architecture string. +// +// E.g. the go "386" architecture string maps to the ubuntu "i386" +// architecture. +func ubuntuArchFromGoArch(goarch string) string { + goArchMapping := map[string]string{ + "386": "i386", + "amd64": "amd64", + "arm": "armhf", + "arm64": "arm64", + "ppc64": "ppc64el", + } + + ubuntuArch := goArchMapping[goarch] + if ubuntuArch == "" { + log.Panicf("unknown goarch %v", goarch) + } + + return ubuntuArch +} + +// IsSupportedArchitecture returns true if the system architecture is in the +// list of architectures. +func IsSupportedArchitecture(architectures []string) bool { + for _, a := range architectures { + if a == "all" || a == string(arch) { + return true + } + } + + return false +} diff --git a/arch/arch_test.go b/arch/arch_test.go new file mode 100644 index 0000000000..973bbbe79f --- /dev/null +++ b/arch/arch_test.go @@ -0,0 +1,59 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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 arch + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +var _ = Suite(&ArchTestSuite{}) + +type ArchTestSuite struct { +} + +func (ts *ArchTestSuite) TestUbuntuArchitecture(c *C) { + c.Check(ubuntuArchFromGoArch("386"), Equals, "i386") + c.Check(ubuntuArchFromGoArch("amd64"), Equals, "amd64") + c.Check(ubuntuArchFromGoArch("arm"), Equals, "armhf") + c.Check(ubuntuArchFromGoArch("arm64"), Equals, "arm64") + c.Check(ubuntuArchFromGoArch("ppc64"), Equals, "ppc64el") +} + +func (ts *ArchTestSuite) TestSetArchitecture(c *C) { + SetArchitecture("armhf") + c.Assert(UbuntuArchitecture(), Equals, "armhf") +} + +func (ts *ArchTestSuite) TestSupportedArchitectures(c *C) { + arch = "armhf" + c.Check(IsSupportedArchitecture([]string{"all"}), Equals, true) + c.Check(IsSupportedArchitecture([]string{"amd64", "armhf", "powerpc"}), Equals, true) + c.Check(IsSupportedArchitecture([]string{"armhf"}), Equals, true) + c.Check(IsSupportedArchitecture([]string{"amd64", "powerpc"}), Equals, false) + + arch = "amd64" + c.Check(IsSupportedArchitecture([]string{"amd64", "armhf", "powerpc"}), Equals, true) + c.Check(IsSupportedArchitecture([]string{"powerpc"}), Equals, false) +} diff --git a/asserts/asserts.go b/asserts/asserts.go index 66b1a57b40..82ac28bdd6 100644 --- a/asserts/asserts.go +++ b/asserts/asserts.go @@ -40,17 +40,17 @@ const ( // Assertion represents an assertion through its general elements. type Assertion interface { - // the type of this assertion + // Type returns the type of this assertion Type() AssertionType - // the revision of this assertion + // Revision returns the revision of this assertion Revision() int - // the authority that signed this assertion + // AuthorityID returns the authority that signed this assertion AuthorityID() string // Header retrieves the header with name Header(name string) string - // the body of this assertion + // Body returns the body of this assertion Body() []byte // Signature returns the signed content and its unprocessed signature @@ -260,3 +260,14 @@ type assertionTypeRegistration struct { } var typeRegistry = make(map[AssertionType]*assertionTypeRegistration) + +// Encode serializes an assertion. +func Encode(assert Assertion) []byte { + content, signature := assert.Signature() + needed := len(content) + 2 + len(signature) + buf := bytes.NewBuffer(make([]byte, 0, needed)) + buf.Write(content) + buf.WriteString("\n\n") + buf.Write(signature) + return buf.Bytes() +} diff --git a/asserts/asserts_test.go b/asserts/asserts_test.go index 5dddf6b133..c4835530ea 100644 --- a/asserts/asserts_test.go +++ b/asserts/asserts_test.go @@ -107,10 +107,7 @@ func (as *AssertsSuite) TestDecodeGetSignatureBits(c *C) { } func (as *AssertsSuite) TestDecodeNoSignatureSplit(c *C) { - for _, encoded := range []string{ - "", - "foo", - } { + for _, encoded := range []string{"", "foo"} { _, err := asserts.Decode([]byte(encoded)) c.Check(err, ErrorMatches, "assertion content/signature separator not found") } @@ -158,3 +155,21 @@ func (as *AssertsSuite) TestDecodeInvalid(c *C) { } } + +func (as *AssertsSuite) TestEncode(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key1: key1\n" + + "primary-key2: key2\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n\n" + + "THE-BODY" + + "\n\n" + + "openpgp c2ln") + a, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + encodeRes := asserts.Encode(a) + c.Check(encodeRes, DeepEquals, encoded) +} diff --git a/caps/capabilities.go b/caps/capabilities.go index 00a1822053..e42228f83c 100644 --- a/caps/capabilities.go +++ b/caps/capabilities.go @@ -23,6 +23,7 @@ import ( "fmt" "regexp" "sort" + "sync" ) // Type is the name of a capability type. @@ -47,13 +48,19 @@ type Capability struct { // Repository stores all known snappy capabilities and types type Repository struct { + m sync.Mutex // protects the internals from concurrent access. If contention gets high, switch to a RWMutex // Map of capabilities, indexed by Capability.Name caps map[string]*Capability } +// NotFoundError means that a capability was not found +type NotFoundError struct { + what, name string +} + const ( // FileType is a basic capability vaguely expressing access to a specific - // file. This single capability type is here just to help boostrap + // file. This single capability type is here just to help bootstrap // the capability concept before we get to load capability interfaces // from YAML. FileType Type = "file" @@ -73,13 +80,19 @@ func ValidateName(name string) error { // NewRepository creates an empty capability repository func NewRepository() *Repository { - return &Repository{make(map[string]*Capability)} + return &Repository{ + caps: make(map[string]*Capability), + } } // Add a capability to the repository. -// Capability names must be unique within the repository. -// An error is returned if this constraint is violated. +// Capability names must be valid snap names, as defined by ValidateName, and +// must be unique within the repository. An error is returned if this +// constraint is violated. func (r *Repository) Add(cap *Capability) error { + r.m.Lock() + defer r.m.Unlock() + if err := ValidateName(cap.Name); err != nil { return err } @@ -92,12 +105,23 @@ func (r *Repository) Add(cap *Capability) error { // Remove removes the capability with the provided name. // Removing a capability that doesn't exist silently does nothing -func (r *Repository) Remove(name string) { - delete(r.caps, name) +func (r *Repository) Remove(name string) error { + r.m.Lock() + defer r.m.Unlock() + + _, ok := r.caps[name] + if ok { + delete(r.caps, name) + return nil + } + return &NotFoundError{"remove", name} } // Names returns all capability names in the repository in lexicographical order. func (r *Repository) Names() []string { + r.m.Lock() + defer r.m.Unlock() + keys := make([]string, len(r.caps)) i := 0 for key := range r.caps { @@ -107,3 +131,38 @@ func (r *Repository) Names() []string { sort.Strings(keys) return keys } + +// String representation of a capability. +func (c Capability) String() string { + return c.Name +} + +type byName []Capability + +func (c byName) Len() int { return len(c) } +func (c byName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c byName) Less(i, j int) bool { return c[i].Name < c[j].Name } + +// All returns all capabilities ordered by name. +func (r *Repository) All() []Capability { + r.m.Lock() + defer r.m.Unlock() + + caps := make([]Capability, len(r.caps)) + i := 0 + for _, capability := range r.caps { + caps[i] = *capability + i++ + } + sort.Sort(byName(caps)) + return caps +} + +func (e *NotFoundError) Error() string { + switch e.what { + case "remove": + return fmt.Sprintf("can't remove capability %q, no such capability", e.name) + default: + panic(fmt.Sprintf("unexpected what: %q", e.what)) + } +} diff --git a/caps/capabilities_test.go b/caps/capabilities_test.go index 2d774d6620..e2ac87fcd6 100644 --- a/caps/capabilities_test.go +++ b/caps/capabilities_test.go @@ -76,20 +76,52 @@ func (s *CapabilitySuite) TestAddInvalidName(c *C) { c.Assert(repo.Names(), Not(testutil.Contains), cap1.Name) } -func (s *CapabilitySuite) TestRemove(c *C) { +func (s *CapabilitySuite) TestRemoveGood(c *C) { repo := NewRepository() cap := &Capability{"name", "label", FileType} - repo.Remove(cap.Name) // This does nothing, silently - repo.Add(cap) // This is tested elsewhere - repo.Remove(cap.Name) + err := repo.Add(cap) + c.Assert(err, IsNil) + err = repo.Remove(cap.Name) + c.Assert(err, IsNil) c.Assert(repo.Names(), HasLen, 0) c.Assert(repo.Names(), Not(testutil.Contains), cap.Name) } +func (s *CapabilitySuite) TestRemoveNoSuchCapability(c *C) { + repo := NewRepository() + err := repo.Remove("name") + c.Assert(err, ErrorMatches, `can't remove capability "name", no such capability`) +} + func (s *CapabilitySuite) TestNames(c *C) { repo := NewRepository() - repo.Add(&Capability{"a", "label-a", FileType}) - repo.Add(&Capability{"b", "label-b", FileType}) - repo.Add(&Capability{"c", "label-c", FileType}) + // Note added in non-sorted order + err := repo.Add(&Capability{"a", "label-a", FileType}) + c.Assert(err, IsNil) + err = repo.Add(&Capability{"c", "label-c", FileType}) + c.Assert(err, IsNil) + err = repo.Add(&Capability{"b", "label-b", FileType}) + c.Assert(err, IsNil) c.Assert(repo.Names(), DeepEquals, []string{"a", "b", "c"}) } + +func (s *CapabilitySuite) TestString(c *C) { + cap := &Capability{"name", "label", FileType} + c.Assert(cap.String(), Equals, "name") +} + +func (s *CapabilitySuite) TestAll(c *C) { + repo := NewRepository() + // Note added in non-sorted order + err := repo.Add(&Capability{"a", "label-a", FileType}) + c.Assert(err, IsNil) + err = repo.Add(&Capability{"c", "label-c", FileType}) + c.Assert(err, IsNil) + err = repo.Add(&Capability{"b", "label-b", FileType}) + c.Assert(err, IsNil) + c.Assert(repo.All(), DeepEquals, []Capability{ + Capability{"a", "label-a", FileType}, + Capability{"b", "label-b", FileType}, + Capability{"c", "label-c", FileType}, + }) +} diff --git a/cmd/snappy/cmd_booted.go b/cmd/snappy/cmd_booted.go index db7ee6f1f8..de3c1938e9 100644 --- a/cmd/snappy/cmd_booted.go +++ b/cmd/snappy/cmd_booted.go @@ -21,8 +21,7 @@ package main import ( "github.com/ubuntu-core/snappy/logger" - "github.com/ubuntu-core/snappy/pkg" - "github.com/ubuntu-core/snappy/snappy" + "github.com/ubuntu-core/snappy/partition" ) type cmdBooted struct { @@ -43,10 +42,6 @@ func (x *cmdBooted) Execute(args []string) error { } func (x *cmdBooted) doBooted() error { - parts, err := snappy.ActiveSnapsByType(pkg.TypeCore) - if err != nil { - return err - } - - return parts[0].(*snappy.SystemImagePart).MarkBootSuccessful() + p := partition.New() + return p.MarkBootSuccessful() } diff --git a/cmd/snappy/cmd_build.go b/cmd/snappy/cmd_build.go index e14d469c23..2a7cdd9333 100644 --- a/cmd/snappy/cmd_build.go +++ b/cmd/snappy/cmd_build.go @@ -32,8 +32,8 @@ import ( const clickReview = "click-review" type cmdBuild struct { - Output string `long:"output" short:"o"` - BuildSnapfs bool `long:"snapfs"` + Output string `long:"output" short:"o"` + BuildSquashfs bool `long:"squashfs"` } var longBuildHelp = i18n.G("Creates a snap package and if available, runs the review scripts.") @@ -57,8 +57,8 @@ func (x *cmdBuild) Execute(args []string) (err error) { } var snapPackage string - if x.BuildSnapfs { - snapPackage, err = snappy.BuildSnapfsSnap(args[0], x.Output) + if x.BuildSquashfs { + snapPackage, err = snappy.BuildSquashfsSnap(args[0], x.Output) } else { snapPackage, err = snappy.BuildLegacySnap(args[0], x.Output) } diff --git a/cmd/snappy/cmd_info.go b/cmd/snappy/cmd_info.go index 7c06933810..c35ddc1d69 100644 --- a/cmd/snappy/cmd_info.go +++ b/cmd/snappy/cmd_info.go @@ -23,6 +23,7 @@ import ( "fmt" "strings" + "github.com/ubuntu-core/snappy/arch" "github.com/ubuntu-core/snappy/i18n" "github.com/ubuntu-core/snappy/logger" "github.com/ubuntu-core/snappy/pkg" @@ -119,7 +120,7 @@ func info() error { // TRANSLATORS: the %s release string fmt.Printf(i18n.G("release: %s\n"), release) // TRANSLATORS: the %s an architecture string - fmt.Printf(i18n.G("architecture: %s\n"), snappy.Architecture()) + fmt.Printf(i18n.G("architecture: %s\n"), arch.UbuntuArchitecture()) // TRANSLATORS: the %s is a comma separated list of framework names fmt.Printf(i18n.G("frameworks: %s\n"), strings.Join(frameworks, ", ")) //TRANSLATORS: the %s represents a list of installed appnames diff --git a/cmd/snappy/cmd_list.go b/cmd/snappy/cmd_list.go index 8904e4f0dd..08e13f6570 100644 --- a/cmd/snappy/cmd_list.go +++ b/cmd/snappy/cmd_list.go @@ -28,7 +28,6 @@ import ( "github.com/ubuntu-core/snappy/i18n" "github.com/ubuntu-core/snappy/logger" - "github.com/ubuntu-core/snappy/pkg" "github.com/ubuntu-core/snappy/snappy" ) @@ -127,43 +126,14 @@ func showVerboseList(installed []snappy.Part, o io.Writer) { } func showRebootMessage(installed []snappy.Part, o io.Writer) { - // Initialise to handle systems without a provisioned "other" - otherVersion := "0" - currentVersion := "0" - otherName := "" - needsReboot := false - + // display all parts that require a reboot for _, part := range installed { - // FIXME: extend this later to look at more than just - // core - once we do that the logic here needs - // to be modified as the current code assumes - // there are only two version instaleld and - // there is only a single part that may requires - // a reboot - if part.Type() != pkg.TypeCore { + if !part.NeedsReboot() { continue } - if part.NeedsReboot() { - needsReboot = true - } - - if part.IsActive() { - currentVersion = part.Version() - } else { - otherVersion = part.Version() - otherName = part.Name() - } - } - - if needsReboot { - if snappy.VersionCompare(otherVersion, currentVersion) > 0 { - // TRANSLATORS: the %s is a pkgname - fmt.Fprintln(o, fmt.Sprintf(i18n.G("Reboot to use the new %s."), otherName)) - } else { - // TRANSLATORS: the first %s is a pkgname the second a version - fmt.Fprintln(o, fmt.Sprintf(i18n.G("Reboot to use %s version %s."), otherName, otherVersion)) - } + // TRANSLATORS: the first %s is a pkgname the second a version + fmt.Fprintln(o, fmt.Sprintf(i18n.G("Reboot to use %s version %s."), part.Version(), part.Name())) } } diff --git a/cmd/snappy/cmd_policygen.go b/cmd/snappy/cmd_policygen.go new file mode 100644 index 0000000000..712bbb515a --- /dev/null +++ b/cmd/snappy/cmd_policygen.go @@ -0,0 +1,74 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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 main + +import ( + "fmt" + "os" + + "github.com/ubuntu-core/snappy/i18n" + "github.com/ubuntu-core/snappy/logger" + "github.com/ubuntu-core/snappy/snappy" +) + +type cmdPolicygen struct { + RegenerateAll bool `long:"regenerate-all"` + Compare bool `long:"compare"` + Force bool `short:"f" long:"force"` + Positional struct { + PackageYaml string `positional-arg-name:"package.yaml path"` + } `positional-args:"yes"` +} + +func init() { + arg, err := parser.AddCommand("policygen", + i18n.G("Generate the apparmor policy"), + i18n.G("Generate the apparmor policy"), + &cmdPolicygen{}) + if err != nil { + logger.Panicf("Unable to install: %v", err) + } + addOptionDescription(arg, "force", i18n.G("Force policy generation.")) + addOptionDescription(arg, "package.yaml path", i18n.G("The path to the package.yaml used to generate the apparmor policy.")) +} + +func (x *cmdPolicygen) Execute(args []string) error { + return withMutexAndRetry(x.doPolicygen) +} + +func (x *cmdPolicygen) doPolicygen() error { + if x.RegenerateAll { + return snappy.RegenerateAllPolicy(x.Force) + } + + fn := x.Positional.PackageYaml + if fn == "" { + return fmt.Errorf(i18n.G("must supply path to package.yaml")) + } + if _, err := os.Stat(fn); err != nil { + return fmt.Errorf("policygen: no such file: %s", fn) + } + + if x.Compare { + return snappy.CompareGeneratePolicyFromFile(fn) + } + + return snappy.GeneratePolicyFromFile(fn, x.Force) +} diff --git a/daemon/api.go b/daemon/api.go index d93fc99db3..3a17c314f4 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -585,21 +585,51 @@ func deleteOp(c *Command, r *http.Request) Response { } } +// licenseData holds details about the snap license, and may be +// marshaled back as an error when the license agreement is pending, +// and is expected as input to accept (or not) that license +// agreement. As such, its field names are part of the API. +type licenseData struct { + Intro string + License string + Agreed bool +} + +func (*licenseData) Error() string { + return "license agreement required" +} + type packageInstruction struct { - Action string `json:"action"` - LeaveOld bool `json:"leave_old"` + progress.NullProgress + Action string `json:"action"` + LeaveOld bool `json:"leave_old"` + License *licenseData `json:"license"` pkg string - prog progress.Meter } +// Agreed is part of the progress.Meter interface (q.v.) +// ask the user whether they agree to the given license's text +func (inst *packageInstruction) Agreed(intro, license string) bool { + if inst.License == nil || !inst.License.Agreed || inst.License.Intro != intro || inst.License.License != license { + inst.License = &licenseData{Intro: intro, License: license, Agreed: false} + return false + } + + return true +} + +var snappyInstall = snappy.Install + func (inst *packageInstruction) install() interface{} { flags := snappy.DoInstallGC if inst.LeaveOld { flags = 0 } - _, err := snappy.Install(inst.pkg, flags, inst.prog) - + _, err := snappyInstall(inst.pkg, flags, inst) if err != nil { + if inst.License != nil && snappy.IsLicenseNotAccepted(err) { + return error(inst.License) + } return err } @@ -614,7 +644,7 @@ func (inst *packageInstruction) update() interface{} { flags = 0 } - _, err := snappy.Update(inst.pkg, flags, inst.prog) + _, err := snappy.Update(inst.pkg, flags, inst) return err } @@ -624,24 +654,24 @@ func (inst *packageInstruction) remove() interface{} { flags = 0 } - return snappy.Remove(inst.pkg, flags, inst.prog) + return snappy.Remove(inst.pkg, flags, inst) } func (inst *packageInstruction) purge() interface{} { - return snappy.Purge(inst.pkg, 0, inst.prog) + return snappy.Purge(inst.pkg, 0, inst) } func (inst *packageInstruction) rollback() interface{} { - _, err := snappy.Rollback(inst.pkg, "", inst.prog) + _, err := snappy.Rollback(inst.pkg, "", inst) return err } func (inst *packageInstruction) activate() interface{} { - return snappy.SetActive(inst.pkg, true, inst.prog) + return snappy.SetActive(inst.pkg, true, inst) } func (inst *packageInstruction) deactivate() interface{} { - return snappy.SetActive(inst.pkg, false, inst.prog) + return snappy.SetActive(inst.pkg, false, inst) } func (inst *packageInstruction) dispatch() func() interface{} { @@ -686,7 +716,6 @@ func postPackage(c *Command, r *http.Request) Response { vars := muxVars(r) inst.pkg = vars["name"] + "." + vars["origin"] - inst.prog = &progress.NullProgress{} f := pkgActionDispatch(&inst) if f == nil { diff --git a/daemon/api_test.go b/daemon/api_test.go index 6e1ba02160..654f4047b7 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -45,6 +45,7 @@ import ( "github.com/ubuntu-core/snappy/release" "github.com/ubuntu-core/snappy/snappy" "github.com/ubuntu-core/snappy/systemd" + "github.com/ubuntu-core/snappy/timeout" ) type apiSuite struct { @@ -280,6 +281,8 @@ func (s *apiSuite) TestListIncludesAll(c *check.C) { "newSystemRepo", "newSnap", "pkgActionDispatch", + // packageInstruction vars: + "snappyInstall", } c.Check(found, check.Equals, len(api)+len(exceptions), check.Commentf(`At a glance it looks like you've not added all the Commands defined in api to the api list. If that is not the case, please add the exception to the "exceptions" list in this test.`)) @@ -847,7 +850,7 @@ func (s *apiSuite) TestPackageServiceGet(c *check.C) { m := rsp.Result.(map[string]*svcDesc) c.Assert(m["svc"], check.FitsTypeOf, new(svcDesc)) c.Check(m["svc"].Op, check.Equals, "status") - c.Check(m["svc"].Spec, check.DeepEquals, &snappy.ServiceYaml{Name: "svc", StopTimeout: snappy.DefaultTimeout}) + c.Check(m["svc"].Spec, check.DeepEquals, &snappy.ServiceYaml{Name: "svc", StopTimeout: timeout.DefaultTimeout}) c.Check(m["svc"].Status, check.DeepEquals, &snappy.PackageServiceStatus{ServiceName: "svc"}) } @@ -1036,3 +1039,115 @@ func (s *apiSuite) TestAppIconGetNoApp(c *check.C) { appIconCmd.GET(appIconCmd, req).ServeHTTP(rec, req) c.Check(rec.Code, check.Equals, 404) } + +func (s *apiSuite) TestPkgInstructionAgreedOK(c *check.C) { + lic := &licenseData{ + Intro: "hi", + License: "Void where empty", + Agreed: true, + } + + inst := &packageInstruction{License: lic} + + c.Check(inst.Agreed(lic.Intro, lic.License), check.Equals, true) +} + +func (s *apiSuite) TestPkgInstructionAgreedNOK(c *check.C) { + lic := &licenseData{ + Intro: "hi", + License: "Void where empty", + Agreed: false, + } + + inst := &packageInstruction{License: lic} + + c.Check(inst.Agreed(lic.Intro, lic.License), check.Equals, false) +} + +func (s *apiSuite) TestPkgInstructionMismatch(c *check.C) { + lic := &licenseData{ + Intro: "hi", + License: "Void where empty", + Agreed: true, + } + + inst := &packageInstruction{License: lic} + + c.Check(inst.Agreed("blah", "yak yak"), check.Equals, false) +} + +func (s *apiSuite) TestInstall(c *check.C) { + orig := snappyInstall + defer func() { snappyInstall = orig }() + + calledFlags := snappy.InstallFlags(42) + + snappyInstall = func(name string, flags snappy.InstallFlags, meter progress.Meter) (string, error) { + calledFlags = flags + + return "", nil + } + + inst := &packageInstruction{ + Action: "install", + } + + err := inst.dispatch()() + + c.Check(calledFlags, check.Equals, snappy.DoInstallGC) + c.Check(err, check.IsNil) +} + +func (s *apiSuite) TestInstallLeaveOld(c *check.C) { + orig := snappyInstall + defer func() { snappyInstall = orig }() + + calledFlags := snappy.InstallFlags(42) + + snappyInstall = func(name string, flags snappy.InstallFlags, meter progress.Meter) (string, error) { + calledFlags = flags + + return "", nil + } + + inst := &packageInstruction{ + Action: "install", + LeaveOld: true, + } + + err := inst.dispatch()() + + c.Check(calledFlags, check.Equals, snappy.InstallFlags(0)) + c.Check(err, check.IsNil) +} + +func (s *apiSuite) TestInstallLicensed(c *check.C) { + orig := snappyInstall + defer func() { snappyInstall = orig }() + + snappyInstall = func(name string, flags snappy.InstallFlags, meter progress.Meter) (string, error) { + if meter.Agreed("hi", "yak yak") { + return "", nil + } + + return "", snappy.ErrLicenseNotAccepted + } + + inst := &packageInstruction{ + Action: "install", + } + + lic, ok := inst.dispatch()().(*licenseData) + c.Assert(ok, check.Equals, true) + c.Check(lic, check.ErrorMatches, "license agreement required") + c.Check(lic.Intro, check.Equals, "hi") + c.Check(lic.License, check.Equals, "yak yak") + c.Check(lic.Agreed, check.Equals, false) + + // now, pass it in + inst.License = lic + inst.License.Agreed = true + + err := inst.dispatch()() + c.Check(err, check.IsNil) +} diff --git a/debian/ubuntu-snappy.dirs b/debian/ubuntu-snappy.dirs index ae102b1f4d..4f0e45f870 100644 --- a/debian/ubuntu-snappy.dirs +++ b/debian/ubuntu-snappy.dirs @@ -1,2 +1,3 @@ /apps /oem +/var/lib/snappy/apparmor/additional \ No newline at end of file diff --git a/debian/ubuntu-snappy.run-hooks.service b/debian/ubuntu-snappy.run-hooks.service index a6503e5502..bc13c14b31 100644 --- a/debian/ubuntu-snappy.run-hooks.service +++ b/debian/ubuntu-snappy.run-hooks.service @@ -1,12 +1,12 @@ [Unit] -Description=Run snappy compatibility hooks -After=local-fs.target +Description=Regenerate snappy security policies +After=local-fs.target apparmor.service Before=ubuntu-snappy.firstboot.service DefaultDependencies=false [Service] Type=oneshot -ExecStart=/usr/bin/snappy internal-run-hooks +ExecStart=/bin/sh -c "set -ex; if ! cmp /usr/share/snappy/security-policy-version /var/lib/snappy/security-policy-version; then /usr/bin/snappy policygen --regenerate-all; cp /usr/share/snappy/security-policy-version /var/lib/snappy/; fi" [Install] WantedBy=multi-user.target diff --git a/dirs/dirs.go b/dirs/dirs.go index a1ee94a701..d63c088824 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -25,17 +25,18 @@ import "path/filepath" var ( GlobalRootDir string - SnapAppsDir string - SnapOemDir string - SnapDataDir string - SnapDataHomeGlob string - SnapAppArmorDir string - SnapSeccompDir string - SnapUdevRulesDir string - LocaleDir string - SnapIconsDir string - SnapMetaDir string - SnapLockFile string + SnapAppsDir string + SnapOemDir string + SnapDataDir string + SnapDataHomeGlob string + SnapAppArmorDir string + SnapAppArmorAdditionalDir string + SnapSeccompDir string + SnapUdevRulesDir string + LocaleDir string + SnapIconsDir string + SnapMetaDir string + SnapLockFile string SnapBinariesDir string SnapServicesDir string @@ -56,7 +57,8 @@ func SetRootDir(rootdir string) { SnapOemDir = filepath.Join(rootdir, "/oem") SnapDataDir = filepath.Join(rootdir, "/var/lib/apps") SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/apps/") - SnapAppArmorDir = filepath.Join(rootdir, "/var/lib/apparmor/clicks") + SnapAppArmorDir = filepath.Join(rootdir, SnappyDir, "apparmor", "profiles") + SnapAppArmorAdditionalDir = filepath.Join(rootdir, SnappyDir, "apparmor", "additional") SnapSeccompDir = filepath.Join(rootdir, SnappyDir, "seccomp", "profiles") SnapIconsDir = filepath.Join(rootdir, SnappyDir, "icons") SnapMetaDir = filepath.Join(rootdir, SnappyDir, "meta") diff --git a/docs/rest.md b/docs/rest.md new file mode 100644 index 0000000000..cbdd9f09d3 --- /dev/null +++ b/docs/rest.md @@ -0,0 +1,528 @@ +# Snappy Ubuntu Core REST API + +Version: 1.0.2 DRAFT + +## Versioning + +As the API evolves, some changes are deemed backwards-compatible (such +as adding methods or verbs, or adding members to the returned JSON +objects) and don't warrant an endpoint change; some changes won't be +backwards compatible, and will be exposed under a new endpoint. + +## Connecting + +While it is expected to allow clients to connect using HTTPS over a +TCP socket, at this point only a unix socket is supported. The socket +is `/run/snapd.socket`. + +## Authentication + +Authentication over the unix socket is delegated to UNIX ACLs. At this +point only root can connect to it; regular user access is not +implemented yet, but should be doable once `SO_PEERCRED` is supported +to determine privilege levels. + +## Responses + +All responses are `application/json` unless noted otherwise. There are +three standard return types: + +* Standard return value +* Background operation +* Error + +Status codes follow that of HTTP. + +### Standard return value + +For a standard synchronous operation, the following JSON object is +returned: + +```javascript +{ + "result": {}, // Extra resource/action specific data + "status": "OK", + "status_code": 200, + "type": "sync" +} +``` + +The HTTP code will be 200 (`OK`), or 201 (`Created`, in which case the +`Location` HTTP header will be set), as appropriate. + +### Background operation + +When a request results in a background operation, the HTTP code is set +to 202 (`Accepted`) and the `Location` HTTP header is set to the +operation's URL. + +The body is a json object with the following structure: + +```javascript +{ + "result": { + "resource": "/1.0/operations/[uuid]", // see below + "status": "running", + "created_at": "..." // and other operation fields + }, + "status": "Accepted", + "status_code": 202, + "type": "async" +} +``` + +The response body is mostly provided as a user friendly way of seeing +what's going on without having to pull the target operation; all +information in the body can also be retrieved from the background +operation URL. + +### Error + +There are various situations in which something may immediately go +wrong, in those cases, the following return value is used: + +```javascript +{ + "result": {}, // may contain more details, for debugging + "status": "Bad Request", // text description of status_code + "status_code": 400, // or 401, etc. (same as HTTP code) + "type": "error" +} +``` + +HTTP code must be one of of 400, 401, 403, 404, 409, 412 or 500. + +### Timestamps + +Timestamps are presented in µs since the epoch UTC, formatted as a decimal +string. For example, `"1234567891234567"` represents +`2009-02-13T23:31:31.234567`. + +## /1.0 +### GET + +* Description: Server configuration and environment information +* Authorization: guest +* Operation: sync +* Return: Dict with the operating system's key values. + +#### Sample result: + +```javascript +{ + "default_channel": "edge", + "flavor": "core", + "api_compat": "0", // increased on minor API changes + "release": "15.04", + "store": "store-id" // only if not default +} +``` + +## /1.0/packages +### GET + +* Description: List of packages +* Authorization: trusted +* Operation: sync +* Return: list of URLs for packages this Ubuntu Core system can handle. + +The result is a JSON object with a packages key; its value is itself a +JSON object whose keys are qualified package names (e.g., +hello-world.canonical), and whose values describe that package. + +Sample result: + +```javascript +{ + "packages": { + "hello-world.canonical": { + "description": "hello-world", + "download_size": "22212", + "icon": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", + "installed_size": "-1", // always -1 if not installed + "name": "hello-world", + "origin": "canonical", + "resource": "/1.0/packages/hello-world.canonical", + "status": "not installed", + "type": "app", + "version": "1.0.18" + }, + "http.chipaca": { + "description": "HTTPie in a snap\nno description", + "download_size": "1578272", + "icon": "/1.0/icons/http.chipaca_3.1.png", + "installed_size": "1821897", + "name": "http", + "origin": "chipaca", + "resource": "/1.0/packages/http.chipaca", + "status": "active", + "type": "app", + "version": "3.1" + }, + "ubuntu-core.ubuntu": { + "description": "A secure, minimal transactional OS for devices and containers.", + "download_size": "19845748", + "icon": "", // core might not have an icon + "installed_size": "-1", // core doesn't have installed_size (yet) + "name": "ubuntu-core", + "origin": "ubuntu", + "resource": "/1.0/packages/ubuntu-core.ubuntu", + "status": "active", + "type": "core", + "update_available": "247", + "version": "241" + } + } +} +``` + +#### Fields +* `packages` + * `status`: can be either `not installed`, `installed`, `active` (i.e. is + current), `removed` (but data present); there is no `purged` state, as a + purged package is undistinguishable from a non-installed package. + * `name`: the package name. + * `version`: a string representing the version. + * `icon`: a url to the package icon, possibly relative to this server. + * `type`: the type of snappy package; one of `app`, `framework`, `kernel`, + `gadget`, or `os`. + * `description`: package description + * `installed_size`: for installed packages, how much space the package + itself (not its data) uses, formatted as a decimal string. + * `download_size`: for not-installed packages, how big the download will + be, formatted as a decimal string. + * `operation`: if the state signals that an operation is underway + (e.g. installing), the operation field describes that operation + * `rollback_available`: if present and not empty, it means the package can + be rolled back to the version specified as a value to this entry. + * `update_available`: if present and not empty, it means the package can be + updated to the version specified as a value to this entry. + +### POST + +* Description: Sideload a package to the system. +* Authorization: trusted +* Operation: async +* Return: background operation or standard error + +#### Input + +The package to sideload should be provided as part of the body of a +`mutlipart/form-data` request. The form should have only one file. If it also +has an `allow-unsigned` field (with any value), the package may be unsigned; +otherwise attempting to sideload an unsigned package will result in a failed +background operation. + +It's also possible to provide the package as the entire body of a `POST` (not a +multipart request). In this case the header `X-Allow-Unsigned` may be used to +allow sideloading unsigned packages. + +### PUT + +* Description: change configuration for active packages. It is an error to + attempt to change configuration for non-active packages; if a configuration + change is requested for a package that is not active in the system the whole + command is aborted even if other packages that are active are specified in + the same command. +* Authorization: trusted +* Operation: sync +* Return: configuration for all listed packages + +The request body is expected to be a JSON object with keys being the qualified +package name(s) to configure, and the values will be passed to the configure +hooks of the packages. The background operation information will similarly +list individual statuses of the configuration changes. + +#### Sample input: + +```javascript +{ + "dd.canonical": "some-option: 42", + "hello-world.canonical": "greeting: Hi" +} +``` + +#### Sample result: + +```javascript +{ + "dd.canonical": "some-option: 42\nsome-other-option: true", + "hello-world.canonical": "greeting: Hi\nheader: false" +} +``` + +## /1.0/packages/[name] +### GET + +* Description: Details for a package +* Authorization: trusted +* Operation: sync +* Return: package details (as in `/1.0/packages/`) + + +### POST + +* Description: Install, update, remove, purge, activate, deactivate, or + rollback the package +* Authorization: trusted +* Operation: async +* Return: background operation or standard error + +#### Sample input + +```javascript +{ + "action": "install" +} +``` + +#### Fields in the input object + +field | ignored except in action | description +-----------|-------------------|------------ +`action` | | Required; a string, one of `install`, `update`, `remove`, `purge`, `activate`, `deactivate`, or `rollback`. +`leave_old`| `install` `update` `remove` | A boolean, default is false (do not leave old packages around). Equivalent to commandline's `--no-gc`. +`config` | `install` | An object, passed to config after installation. See `.../config`. If missing, config isn't called. + +#### A note on licenses + +At this time the daemon does not support installing or updating packages that +require an explicit license agreement. This will be done in a +backwards-compatible way soon. + +## /1.0/packages/[name]/services + +Query an active package for information about its services, and alter the +state of those services. Commands under `.../services` will return an error if +the package is not active. + +### GET + +* Description: Services for a package +* Authorization: trusted +* Operation: sync +* Return: service configuration + +Returns a JSON object with a result key, its value is a list of JSON objects +where the package name is the item key. The value is another JSON object that +has three keys [`op`, `spec`, `status`], spec and status are JSON objects that +provide description about the service as well as its systemd unit. + +#### Sample result: + +```javascript +{ + "result": { + "xkcd-webserver": { + "op": "status", + "spec": { + "name": "xkcd-webserver", + "description": "A fun webserver", + "start": "bin/xkcd-webserver", + "stop-timeout": "30s", + "caps": [ + "networking", + "network-service" + ] + }, + "status": { + "service_file_name": "xkcd-webserver_xkcd-webserver_0.5.service", + "load_state": "loaded", + "active_state": "inactive", + "sub_state": "dead", + "unit_file_state": "enabled", + "package_name": "xkcd-webserver", + "service_name": "xkcd-webserver" + } + } + }, + "status": "OK", + "status_code": 200, + "type": "sync" +} +``` + +### PUT + +* Description: Put all services of a package into a specific state +* Authorization: trusted +* Operation: async + +#### Sample input: + +```javascript +{ +"action": "start|stop|restart|enable|disable" +} +``` + +## /1.0/packages/[name]/services/[name] + +### GET + +* Description: Service for a package +* Authorization: trusted +* Operation: sync +* Return: service configuration + +The result is a JSON object with a `result` key where the value is a JSON object +that includes a single object from the list of the upper level endpoint +(`/1.0/packages/[name]/services`). + +#### Sample result: + +```javascript +{ + "result": { + "op": "status", + "spec": { + "name": "xkcd-webserver", + "description": "A fun webserver", + "start": "bin/xkcd-webserver", + "stop-timeout": "30s", + "caps": [ + "networking", + "network-service" + ] + }, + "status": { + "service_file_name": "xkcd-webserver_xkcd-webserver_0.5.service", + "load_state": "loaded", + "active_state": "inactive", + "sub_state": "dead", + "unit_file_state": "enabled", + "package_name": "xkcd-webserver", + "service_name": "xkcd-webserver" + } + }, + "status": "OK", + "status_code": 200, + "type": "sync" +} +``` + +### PUT + +* Description: Put the service into a specific state +* Authorization: trusted +* Operation: async + +#### Sample input: + +```javascript +{ +"action": "start|stop|restart|enable|disable" +} +``` + +## /1.0/packages/[name]/services/[name]/logs + +### GET + +* Description: Logs for the service from a package +* Authorization: trusted +* Operation: sync +* Return: service logs + +#### Sample result: + +```javascript +[ + { + "timestamp": "1440679470679901", + "message": "something happened", + "raw": {} + }, + { + "timestamp": "1440679470680968", + "message": "bla bla", + "raw": {} + } +] +``` + +## /1.0/packages/[name]/config + +Query an active package for information about its configuration, and alter +that configuration. Will return an error if the package is not active. + +### GET + +* Description: Configuration for a package +* Authorization: trusted +* Operation: sync +* Return: package configuration + +#### Sample result: + +```javascript +"config:\n ubuntu-core:\n autopilot: false\n timezone: Europe/Berlin\n hostname: localhost.localdomain\n" +``` + +Notes: user facing implementations in text form must show this data using yaml. + +### PUT + +* Description: Set configuration for a package +* Authorization: trusted +* Operation: sync +* Return: package configuration + +#### Sample input: + +```javascript + config:\n ubuntu-core:\n autopilot: true\n +``` + +#### Sample result: + +```javascript +"config:\n ubuntu-core:\n autopilot: true\n timezone: Europe/Berlin\n hostname: localhost.localdomain\n" +``` + +## /1.0/operations/<uuid> + +### GET + +* Description: background operation +* Authorization: trusted +* Operation: sync +* Return: dict representing a background operation + +#### Sample result: + +```javascript +{ + "created_at": "1415639996123456", // Creation timestamp + "output": {}, + "resource": "/1.0/packages/camlistore.sergiusens", + "status": "running", // or “succeeded” or “failed” + "updated_at": "1415639996451214" // Last update timestamp +} +``` + +### DELETE + +* Description: If the operation has completed, `DELETE` will remove the + entry. Otherwise it is an error. +* Authorization: trusted +* Operation: sync +* Return: standard return value or standard error + +## /1.0/icons/[icon] + +### GET + +Gets a locally-installed snap's icon. The response will be the raw contents of +the icon file; the content-type will be set accordingly. + +This fetches the icon that was downloaded from the store at install time. + +## /1.0/icons/[name]/icon + +### GET + +Get an icon from a snap installed on the system. The response will be the raw +contents of the icon file; the content-type will be set accordingly. + +This fetches the icon from the package itself. diff --git a/docs/security.md b/docs/security.md index ad748ab402..a9746dc88e 100644 --- a/docs/security.md +++ b/docs/security.md @@ -80,7 +80,7 @@ options are available to modify the confinement: AppArmor and seccomp policy. Note: these are separate from `capabilities(7)`. Specify `caps: []` to indicate no additional `caps`. When `caps` and `security-template` are not specified, `caps` defaults to client networking. - Not compatible with `security-override` or `security-policy`. + Not compatible with `security-policy`. * AppArmor access is deny by default and apps are restricted to their app-specific directories, libraries, etc (enforcing ro, rw, etc). Additional access beyond what is allowed by the @@ -91,14 +91,14 @@ options are available to modify the confinement: `security-template` is declared via this option * `security-template`: (optional) alternate security template to use instead of `default`. When specified without `caps`, `caps` defaults to being empty. Not - compatible with `security-override` or `security-policy`. -* `security-override`: (optional) high level overrides to use when - `security-template` and `caps` are not sufficient - see - [advanced usage](https://wiki.ubuntu.com/SecurityTeam/Specifications/SnappyConfinement) - for details. Not compatible with `caps`, `security-template` or - `security-policy` - * `apparmor: path/to/security.override` - * `seccomp: path/to/filter.override` + compatible with `security-policy`. +* `security-override`: (optional) overrides to use when `security-template` and + `caps` are not sufficient. Not compatible with `security-policy`. The + following keys are supported: + * `read-paths`: a list of additional paths that the app can read + * `write-paths`: a list of additional paths that the app can write + * `abstractions`: a list of additional AppArmor abstractions for the app + * `syscalls`: a list of additional syscalls that the app can use * `security-policy`: (optional) hand-crafted low-level raw security policy to use instead of using default template-based security policy. Not compatible with `caps`, `security-template` or `security-override`. diff --git a/helpers/helpers.go b/helpers/helpers.go index 3de371fd21..3efc550aaa 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -32,7 +32,6 @@ import ( "os/user" "path/filepath" "reflect" - "runtime" "strings" "syscall" "text/template" @@ -41,8 +40,6 @@ import ( "github.com/ubuntu-core/snappy/logger" ) -var goarch = runtime.GOARCH - func init() { // golang does not init Seed() itself rand.Seed(time.Now().UTC().UnixNano()) @@ -187,36 +184,6 @@ func (e ErrUnsupportedFileType) Error() string { return fmt.Sprintf("%s: unsupported filetype %s", e.Name, e.Mode) } -// UbuntuArchitecture returns the debian equivalent architecture for the -// currently running architecture. -// -// If the architecture does not map any debian architecture, the -// GOARCH is returned. -func UbuntuArchitecture() string { - switch goarch { - case "386": - return "i386" - case "arm": - return "armhf" - default: - return goarch - } -} - -// IsSupportedArchitecture returns true if the system architecture is in the -// list of architectures. -func IsSupportedArchitecture(architectures []string) bool { - systemArch := UbuntuArchitecture() - - for _, arch := range architectures { - if arch == "all" || arch == systemArch { - return true - } - } - - return false -} - // Sha512sum returns the sha512 of the given file as a hexdigest func Sha512sum(infile string) (hexdigest string, err error) { r, err := os.Open(infile) diff --git a/helpers/helpers_test.go b/helpers/helpers_test.go index 76ba37af55..bd2c20b508 100644 --- a/helpers/helpers_test.go +++ b/helpers/helpers_test.go @@ -88,29 +88,6 @@ func (ts *HTestSuite) TestUnpack(c *C) { c.Check(fn, Equals, "/etc/fstab") } -func (ts *HTestSuite) TestUbuntuArchitecture(c *C) { - goarch = "arm" - c.Check(UbuntuArchitecture(), Equals, "armhf") - - goarch = "amd64" - c.Check(UbuntuArchitecture(), Equals, "amd64") - - goarch = "386" - c.Check(UbuntuArchitecture(), Equals, "i386") -} - -func (ts *HTestSuite) TestSupportedArchitectures(c *C) { - goarch = "arm" - c.Check(IsSupportedArchitecture([]string{"all"}), Equals, true) - c.Check(IsSupportedArchitecture([]string{"amd64", "armhf", "powerpc"}), Equals, true) - c.Check(IsSupportedArchitecture([]string{"armhf"}), Equals, true) - c.Check(IsSupportedArchitecture([]string{"amd64", "powerpc"}), Equals, false) - - goarch = "amd64" - c.Check(IsSupportedArchitecture([]string{"amd64", "armhf", "powerpc"}), Equals, true) - c.Check(IsSupportedArchitecture([]string{"powerpc"}), Equals, false) -} - func (ts *HTestSuite) TestChdir(c *C) { tmpdir := c.MkDir() diff --git a/integration-tests/run-in-image/debian/tests/control b/integration-tests/run-in-image/debian/tests/control deleted file mode 100644 index 7bf7aebab9..0000000000 --- a/integration-tests/run-in-image/debian/tests/control +++ /dev/null @@ -1,2 +0,0 @@ -Test-Command: ./snappy-selftest --yes-really -Depends: diff --git a/integration-tests/run-in-image/tests/04_test_install_hello b/integration-tests/run-in-image/tests/04_test_install_hello deleted file mode 100644 index 1693bcd353..0000000000 --- a/integration-tests/run-in-image/tests/04_test_install_hello +++ /dev/null @@ -1,7 +0,0 @@ -test() { - T="hello-world[[:space:]]+[0-9]{4}-[0-9]{2}-[0-9]{2}[[:space:]]+[0-9]+(\.[0-9]+)+[[:space:]]+canonical" - test_regexp "$T" sudo $SNAPPY install hello-world - - T="Hello World!" - test_equal "$T" hello-world.echo -} diff --git a/integration-tests/selftest b/integration-tests/selftest deleted file mode 100755 index ac3d746db5..0000000000 --- a/integration-tests/selftest +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/sh - -set -ex - -HERE="$(dirname $0)" - -# build the updated deb packages for the image -rm -f "$HERE/debs/*" -# the "-us -uc " flags tell it not to sign it -bzr-buildpackage --result-dir="$HERE/debs" "$HERE/.." -- -us -uc - -# build a base image -IMAGE="$HERE/image/snappy.img" -rm -f "$IMAGE/*" -# FIXME: hrm, hrm, needs sudo to work on testbed -# -# FIXME2: use wget here instead and just get the latest image from -# cdimage -sudo ubuntu-device-flash core -o "$IMAGE" rolling --channel edge --enable-ssh --developer-mode - -# base cmd -QEMU_CMD="qemu-system-$(uname -m) -enable-kvm -m 768 -localtime -nographic -net user -net nic,model=virtio" - -# fire it up -port=11022 -SERIAL=stdio -$QEMU_CMD -drive file="$IMAGE",if=virtio -redir tcp:$port::22 -monitor none -serial $SERIAL & -QEMU_PID=$! -trap "kill $QEMU_PID" INT QUIT - -# wait until the image is ready -SSH="ssh -oStrictHostKeyChecking=no -o UserKnownHostsFile=\"$HERE/image/known_hosts\" -p $port ubuntu@localhost" -SCP="scp -oStrictHostKeyChecking=no -o UserKnownHostsFile=\"$HERE/image/known_hosts\" -P $port" -for i in $(seq 100); do - if $SSH true; then - break - fi - sleep 1 -done -if [ $i = 100 ]; then - echo "Failed to setup qemu" - exit 1 -fi - -# install debs -$SSH rm -rf /tmp/debs -$SSH mkdir /tmp/debs -$SCP "$HERE"/debs/*.deb ubuntu@localhost:/tmp/debs -$SSH sudo mount -o remount,rw / -$SSH sudo dpkg -i /tmp/debs/*.deb - -# shut it down -$SSH sudo halt --poweroff || true - -# wait for qemu pid -for i in $(seq 100); do - if [ ! -e /proc/$QEMU_PID/exe ]; then - break - fi - i=$((i+1)) - sleep 1 -done - -# now run the tests against the image -(cd "$HERE"; - adt-run run-in-image/ --- ssh -s snappy -- -i image/snappy.img; -) - diff --git a/partition/bootloader.go b/partition/bootloader.go index 6007a00c67..c9189e941d 100644 --- a/partition/bootloader.go +++ b/partition/bootloader.go @@ -310,10 +310,10 @@ func (b *bootloaderType) HandleAssets() (err error) { // BootloaderDir returns the full path to the (mounted and writable) // bootloader-specific boot directory. func BootloaderDir() string { - if helpers.FileExists(bootloaderUbootDir) { - return bootloaderUbootDir - } else if helpers.FileExists(bootloaderGrubDir) { - return bootloaderGrubDir + if helpers.FileExists(bootloaderUbootDir()) { + return bootloaderUbootDir() + } else if helpers.FileExists(bootloaderGrubDir()) { + return bootloaderGrubDir() } return "" diff --git a/partition/bootloader_grub.go b/partition/bootloader_grub.go index 7884f60bb3..b1a50e6ab6 100644 --- a/partition/bootloader_grub.go +++ b/partition/bootloader_grub.go @@ -21,7 +21,9 @@ package partition import ( "fmt" + "path/filepath" + "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/helpers" "github.com/mvo5/goconfigparser" @@ -29,18 +31,14 @@ import ( const ( bootloaderGrubDirReal = "/boot/grub" - bootloaderGrubConfigFileReal = "/boot/grub/grub.cfg" - bootloaderGrubEnvFileReal = "/boot/grub/grubenv" + bootloaderGrubConfigFileReal = "grub.cfg" + bootloaderGrubEnvFileReal = "grubenv" bootloaderGrubEnvCmdReal = "/usr/bin/grub-editenv" ) // var to make it testable var ( - bootloaderGrubDir = bootloaderGrubDirReal - bootloaderGrubConfigFile = bootloaderGrubConfigFileReal - bootloaderGrubEnvFile = bootloaderGrubEnvFileReal - bootloaderGrubEnvCmd = bootloaderGrubEnvCmdReal ) @@ -50,13 +48,23 @@ type grub struct { const bootloaderNameGrub bootloaderName = "grub" +func bootloaderGrubDir() string { + return filepath.Join(dirs.GlobalRootDir, bootloaderGrubDirReal) +} +func bootloaderGrubConfigFile() string { + return filepath.Join(bootloaderGrubDir(), bootloaderGrubConfigFileReal) +} +func bootloaderGrubEnvFile() string { + return filepath.Join(bootloaderGrubDir(), bootloaderGrubEnvFileReal) +} + // newGrub create a new Grub bootloader object func newGrub(partition *Partition) bootLoader { - if !helpers.FileExists(bootloaderGrubConfigFile) { + if !helpers.FileExists(bootloaderGrubConfigFile()) { return nil } - b := newBootLoader(partition, bootloaderGrubDir) + b := newBootLoader(partition, bootloaderGrubConfigFile()) if b == nil { return nil } @@ -89,7 +97,7 @@ func (g *grub) ToggleRootFS(otherRootfs string) (err error) { func (g *grub) GetBootVar(name string) (value string, err error) { // Grub doesn't provide a get verb, so retrieve all values and // search for the required variable ourselves. - output, err := runCommandWithStdout(bootloaderGrubEnvCmd, bootloaderGrubEnvFile, "list") + output, err := runCommandWithStdout(bootloaderGrubEnvCmd, bootloaderGrubEnvFile(), "list") if err != nil { return "", err } @@ -108,7 +116,7 @@ func (g *grub) SetBootVar(name, value string) (err error) { // RunCommand() does not use a shell and thus adding quotes // stores them in the environment file (which is not desirable) arg := fmt.Sprintf("%s=%s", name, value) - return runCommand(bootloaderGrubEnvCmd, bootloaderGrubEnvFile, "set", arg) + return runCommand(bootloaderGrubEnvCmd, bootloaderGrubEnvFile(), "set", arg) } func (g *grub) GetNextBootRootFSName() (label string, err error) { @@ -129,5 +137,5 @@ func (g *grub) MarkCurrentBootSuccessful(currentRootfs string) (err error) { } func (g *grub) BootDir() string { - return bootloaderGrubDir + return bootloaderGrubDir() } diff --git a/partition/bootloader_grub_test.go b/partition/bootloader_grub_test.go index bfa632de30..3ed27f2a90 100644 --- a/partition/bootloader_grub_test.go +++ b/partition/bootloader_grub_test.go @@ -25,6 +25,7 @@ import ( "os" "path/filepath" + "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/helpers" . "gopkg.in/check.v1" @@ -37,19 +38,19 @@ func mockGrubFile(c *C, newPath string, mode os.FileMode) { func (s *PartitionTestSuite) makeFakeGrubEnv(c *C) { // create bootloader - err := os.MkdirAll(bootloaderGrubDir, 0755) + err := os.MkdirAll(bootloaderGrubDir(), 0755) c.Assert(err, IsNil) // these files just needs to exist - mockGrubFile(c, bootloaderGrubConfigFile, 0644) - mockGrubFile(c, bootloaderGrubEnvFile, 0644) + mockGrubFile(c, bootloaderGrubConfigFile(), 0644) + mockGrubFile(c, bootloaderGrubEnvFile(), 0644) // do not run commands for real runCommand = mockRunCommandWithCapture } func (s *PartitionTestSuite) TestNewGrubNoGrubReturnsNil(c *C) { - bootloaderGrubConfigFile = "no-such-dir" + dirs.GlobalRootDir = "/something/not/there" partition := New() g := newGrub(partition) @@ -88,12 +89,12 @@ func (s *PartitionTestSuite) TestToggleRootFS(c *C) { mp := singleCommand{"/bin/mountpoint", mountTarget} c.Assert(allCommands[0], DeepEquals, mp) - expectedGrubSet := singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile, "set", "snappy_mode=try"} + expectedGrubSet := singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile(), "set", "snappy_mode=try"} c.Assert(allCommands[1], DeepEquals, expectedGrubSet) // the https://developer.ubuntu.com/en/snappy/porting guide says // we always use the short names - expectedGrubSet = singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile, "set", "snappy_ab=b"} + expectedGrubSet = singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile(), "set", "snappy_ab=b"} c.Assert(allCommands[2], DeepEquals, expectedGrubSet) c.Assert(len(allCommands), Equals, 3) @@ -138,22 +139,22 @@ func (s *PartitionTestSuite) TestGrubMarkCurrentBootSuccessful(c *C) { mp := singleCommand{"/bin/mountpoint", mountTarget} c.Assert(allCommands[0], DeepEquals, mp) - expectedGrubSet := singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile, "set", "snappy_trial_boot=0"} + expectedGrubSet := singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile(), "set", "snappy_trial_boot=0"} c.Assert(allCommands[1], DeepEquals, expectedGrubSet) - expectedGrubSet2 := singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile, "set", "snappy_ab=a"} + expectedGrubSet2 := singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile(), "set", "snappy_ab=a"} c.Assert(allCommands[2], DeepEquals, expectedGrubSet2) - expectedGrubSet3 := singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile, "set", "snappy_mode=regular"} + expectedGrubSet3 := singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile(), "set", "snappy_mode=regular"} c.Assert(allCommands[3], DeepEquals, expectedGrubSet3) } func (s *PartitionTestSuite) TestSyncBootFilesWithAssets(c *C) { - err := os.MkdirAll(bootloaderGrubDir, 0755) + err := os.MkdirAll(bootloaderGrubDir(), 0755) c.Assert(err, IsNil) runCommand = mockRunCommand diff --git a/partition/bootloader_uboot.go b/partition/bootloader_uboot.go index be8f090e4f..1ed008ea9e 100644 --- a/partition/bootloader_uboot.go +++ b/partition/bootloader_uboot.go @@ -24,8 +24,10 @@ import ( "bytes" "fmt" "os" + "path/filepath" "strings" + "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/helpers" "github.com/mvo5/goconfigparser" @@ -34,29 +36,23 @@ import ( const ( bootloaderUbootDirReal = "/boot/uboot" - bootloaderUbootConfigFileReal = "/boot/uboot/uEnv.txt" + bootloaderUbootConfigFileReal = "uEnv.txt" // File created by u-boot itself when // bootloaderBootmodeTry == "try" which the // successfully booted system must remove to flag to u-boot that // this partition is "good". - bootloaderUbootStampFileReal = "/boot/uboot/snappy-stamp.txt" + bootloaderUbootStampFileReal = "snappy-stamp.txt" // DEPRECATED: - bootloaderUbootEnvFileReal = "/boot/uboot/snappy-system.txt" + bootloaderUbootEnvFileReal = "snappy-system.txt" // the real uboot env - bootloaderUbootFwEnvFileReal = "/boot/uboot/uboot.env" + bootloaderUbootFwEnvFileReal = "uboot.env" ) // var to make it testable var ( - bootloaderUbootDir = bootloaderUbootDirReal - bootloaderUbootConfigFile = bootloaderUbootConfigFileReal - bootloaderUbootStampFile = bootloaderUbootStampFileReal - bootloaderUbootEnvFile = bootloaderUbootEnvFileReal - bootloaderUbootFwEnvFile = bootloaderUbootFwEnvFileReal - atomicWriteFile = helpers.AtomicWriteFile ) @@ -77,19 +73,39 @@ type configFileChange struct { Value string } +func bootloaderUbootDir() string { + return filepath.Join(dirs.GlobalRootDir, bootloaderUbootDirReal) +} + +func bootloaderUbootConfigFile() string { + return filepath.Join(bootloaderUbootDir(), bootloaderUbootConfigFileReal) +} + +func bootloaderUbootStampFile() string { + return filepath.Join(bootloaderUbootDir(), bootloaderUbootStampFileReal) +} + +func bootloaderUbootEnvFile() string { + return filepath.Join(bootloaderUbootDir(), bootloaderUbootEnvFileReal) +} + +func bootloaderUbootFwEnvFile() string { + return filepath.Join(bootloaderUbootDir(), bootloaderUbootFwEnvFileReal) +} + // newUboot create a new Uboot bootloader object func newUboot(partition *Partition) bootLoader { - if !helpers.FileExists(bootloaderUbootConfigFile) { + if !helpers.FileExists(bootloaderUbootConfigFile()) { return nil } - b := newBootLoader(partition, bootloaderUbootDir) + b := newBootLoader(partition, bootloaderUbootDir()) if b == nil { return nil } u := uboot{bootloaderType: *b} - if !helpers.FileExists(bootloaderUbootFwEnvFile) { + if !helpers.FileExists(bootloaderUbootFwEnvFile()) { u.useLegacy = true } @@ -111,7 +127,7 @@ func (u *uboot) ToggleRootFS(otherRootfs string) (err error) { func getBootVarLegacy(name string) (value string, err error) { cfg := goconfigparser.New() cfg.AllowNoSectionHeader = true - if err := cfg.ReadFile(bootloaderUbootEnvFile); err != nil { + if err := cfg.ReadFile(bootloaderUbootEnvFile()); err != nil { return "", nil } @@ -131,11 +147,11 @@ func setBootVarLegacy(name, value string) error { }, } - return modifyNameValueFile(bootloaderUbootEnvFile, changes) + return modifyNameValueFile(bootloaderUbootEnvFile(), changes) } func setBootVarFwEnv(name, value string) error { - env, err := uenv.Open(bootloaderUbootFwEnvFile) + env, err := uenv.Open(bootloaderUbootFwEnvFile()) if err != nil { return err } @@ -150,7 +166,7 @@ func setBootVarFwEnv(name, value string) error { } func getBootVarFwEnv(name string) (string, error) { - env, err := uenv.Open(bootloaderUbootFwEnvFile) + env, err := uenv.Open(bootloaderUbootFwEnvFile()) if err != nil { return "", err } @@ -201,11 +217,11 @@ func (u *uboot) MarkCurrentBootSuccessful(currentRootfs string) error { } // legacy support, does not error if the file is not there - return os.RemoveAll(bootloaderUbootStampFile) + return os.RemoveAll(bootloaderUbootStampFile()) } func (u *uboot) BootDir() string { - return bootloaderUbootDir + return bootloaderUbootDir() } // Rewrite the specified file, applying the specified set of changes. diff --git a/partition/bootloader_uboot_test.go b/partition/bootloader_uboot_test.go index c5577c3f2f..24c1f23764 100644 --- a/partition/bootloader_uboot_test.go +++ b/partition/bootloader_uboot_test.go @@ -67,15 +67,15 @@ snappy_boot=if test "${snappy_mode}" = "try"; then if test -e mmc ${bootpart} ${ ` func (s *PartitionTestSuite) makeFakeUbootEnv(c *C) { - err := os.MkdirAll(bootloaderUbootDir, 0755) + err := os.MkdirAll(bootloaderUbootDir(), 0755) c.Assert(err, IsNil) // this file just needs to exist - err = ioutil.WriteFile(bootloaderUbootConfigFile, []byte(""), 0644) + err = ioutil.WriteFile(bootloaderUbootConfigFile(), []byte(""), 0644) c.Assert(err, IsNil) // this file needs specific data - err = ioutil.WriteFile(bootloaderUbootEnvFile, []byte(fakeUbootEnvData), 0644) + err = ioutil.WriteFile(bootloaderUbootEnvFile(), []byte(fakeUbootEnvData), 0644) c.Assert(err, IsNil) } @@ -253,9 +253,9 @@ func (s *PartitionTestSuite) TestUbootMarkCurrentBootSuccessful(c *C) { // "snappy booted" if the system boots successfully. If this // file exists when uboot starts, it will know that the previous // boot failed, and will therefore toggle to the other rootfs. - err := ioutil.WriteFile(bootloaderUbootStampFile, []byte(""), 0640) + err := ioutil.WriteFile(bootloaderUbootStampFile(), []byte(""), 0640) c.Assert(err, IsNil) - c.Assert(helpers.FileExists(bootloaderUbootStampFile), Equals, true) + c.Assert(helpers.FileExists(bootloaderUbootStampFile()), Equals, true) partition := New() u := newUboot(partition) @@ -267,8 +267,8 @@ func (s *PartitionTestSuite) TestUbootMarkCurrentBootSuccessful(c *C) { err = u.ToggleRootFS("b") c.Assert(err, IsNil) - c.Assert(helpers.FileExists(bootloaderUbootEnvFile), Equals, true) - bytes, err := ioutil.ReadFile(bootloaderUbootEnvFile) + c.Assert(helpers.FileExists(bootloaderUbootEnvFile()), Equals, true) + bytes, err := ioutil.ReadFile(bootloaderUbootEnvFile()) c.Assert(err, IsNil) c.Assert(strings.Contains(string(bytes), "snappy_mode=try"), Equals, true) c.Assert(strings.Contains(string(bytes), "snappy_mode=regular"), Equals, false) @@ -277,10 +277,10 @@ func (s *PartitionTestSuite) TestUbootMarkCurrentBootSuccessful(c *C) { err = u.MarkCurrentBootSuccessful("b") c.Assert(err, IsNil) - c.Assert(helpers.FileExists(bootloaderUbootStampFile), Equals, false) - c.Assert(helpers.FileExists(bootloaderUbootEnvFile), Equals, true) + c.Assert(helpers.FileExists(bootloaderUbootStampFile()), Equals, false) + c.Assert(helpers.FileExists(bootloaderUbootEnvFile()), Equals, true) - bytes, err = ioutil.ReadFile(bootloaderUbootEnvFile) + bytes, err = ioutil.ReadFile(bootloaderUbootEnvFile()) c.Assert(err, IsNil) c.Assert(strings.Contains(string(bytes), "snappy_mode=try"), Equals, false) c.Assert(strings.Contains(string(bytes), "snappy_mode=regular"), Equals, true) @@ -308,7 +308,7 @@ func (s *PartitionTestSuite) TestWriteDueToMissingValues(c *C) { s.makeFakeUbootEnv(c) // this file needs specific data - c.Assert(ioutil.WriteFile(bootloaderUbootEnvFile, []byte(""), 0644), IsNil) + c.Assert(ioutil.WriteFile(bootloaderUbootEnvFile(), []byte(""), 0644), IsNil) atomiCall := false atomicWriteFile = func(a string, b []byte, c os.FileMode, f helpers.AtomicWriteFlags) error { @@ -323,7 +323,7 @@ func (s *PartitionTestSuite) TestWriteDueToMissingValues(c *C) { c.Check(u.MarkCurrentBootSuccessful("a"), IsNil) c.Assert(atomiCall, Equals, true) - bytes, err := ioutil.ReadFile(bootloaderUbootEnvFile) + bytes, err := ioutil.ReadFile(bootloaderUbootEnvFile()) c.Assert(err, IsNil) c.Check(strings.Contains(string(bytes), "snappy_mode=try"), Equals, false) c.Check(strings.Contains(string(bytes), "snappy_mode=regular"), Equals, true) @@ -333,7 +333,7 @@ func (s *PartitionTestSuite) TestWriteDueToMissingValues(c *C) { func (s *PartitionTestSuite) TestUbootMarkCurrentBootSuccessfulFwEnv(c *C) { s.makeFakeUbootEnv(c) - env, err := uenv.Create(bootloaderUbootFwEnvFile, 4096) + env, err := uenv.Create(bootloaderUbootFwEnvFile(), 4096) c.Assert(err, IsNil) env.Set("snappy_ab", "b") env.Set("snappy_mode", "try") @@ -348,7 +348,7 @@ func (s *PartitionTestSuite) TestUbootMarkCurrentBootSuccessfulFwEnv(c *C) { err = u.MarkCurrentBootSuccessful("b") c.Assert(err, IsNil) - env, err = uenv.Open(bootloaderUbootFwEnvFile) + env, err = uenv.Open(bootloaderUbootFwEnvFile()) c.Assert(err, IsNil) c.Assert(env.String(), Equals, "snappy_ab=b\nsnappy_mode=regular\nsnappy_trial_boot=0\n") } @@ -356,14 +356,14 @@ func (s *PartitionTestSuite) TestUbootMarkCurrentBootSuccessfulFwEnv(c *C) { func (s *PartitionTestSuite) TestUbootSetEnvNoUselessWrites(c *C) { s.makeFakeUbootEnv(c) - env, err := uenv.Create(bootloaderUbootFwEnvFile, 4096) + env, err := uenv.Create(bootloaderUbootFwEnvFile(), 4096) c.Assert(err, IsNil) env.Set("snappy_ab", "b") env.Set("snappy_mode", "regular") err = env.Save() c.Assert(err, IsNil) - st, err := os.Stat(bootloaderUbootFwEnvFile) + st, err := os.Stat(bootloaderUbootFwEnvFile()) c.Assert(err, IsNil) time.Sleep(100 * time.Millisecond) @@ -375,11 +375,11 @@ func (s *PartitionTestSuite) TestUbootSetEnvNoUselessWrites(c *C) { err = setBootVarFwEnv(bootloaderRootfsVar, "b") c.Assert(err, IsNil) - env, err = uenv.Open(bootloaderUbootFwEnvFile) + env, err = uenv.Open(bootloaderUbootFwEnvFile()) c.Assert(err, IsNil) c.Assert(env.String(), Equals, "snappy_ab=b\nsnappy_mode=regular\n") - st2, err := os.Stat(bootloaderUbootFwEnvFile) + st2, err := os.Stat(bootloaderUbootFwEnvFile()) c.Assert(err, IsNil) c.Assert(st.ModTime(), Equals, st2.ModTime()) } @@ -403,7 +403,7 @@ func (s *PartitionTestSuite) TestUbootSetBootVarLegacy(c *C) { func (s *PartitionTestSuite) TestUbootSetBootVarFwEnv(c *C) { s.makeFakeUbootEnv(c) - env, err := uenv.Create(bootloaderUbootFwEnvFile, 4096) + env, err := uenv.Create(bootloaderUbootFwEnvFile(), 4096) c.Assert(err, IsNil) err = env.Save() c.Assert(err, IsNil) @@ -420,7 +420,7 @@ func (s *PartitionTestSuite) TestUbootSetBootVarFwEnv(c *C) { func (s *PartitionTestSuite) TestUbootGetBootVarFwEnv(c *C) { s.makeFakeUbootEnv(c) - env, err := uenv.Create(bootloaderUbootFwEnvFile, 4096) + env, err := uenv.Create(bootloaderUbootFwEnvFile(), 4096) c.Assert(err, IsNil) env.Set("key2", "value2") err = env.Save() diff --git a/partition/migrate_grub.go b/partition/migrate_grub.go index efc26c2578..c8c0e91655 100644 --- a/partition/migrate_grub.go +++ b/partition/migrate_grub.go @@ -106,7 +106,7 @@ func copyKernelAssets(prefixDir, grubTargetDir string) error { return fmt.Errorf("Incorrect matches for %v: %v", p, matches) } name := normalizeKernelInitrdName(filepath.Base(matches[0])) - targetPath := filepath.Join(bootloaderGrubDir, grubTargetDir, name) + targetPath := filepath.Join(bootloaderGrubDir(), grubTargetDir, name) os.MkdirAll(filepath.Dir(targetPath), 0755) // FIXME: valid? if helpers.FileExists(targetPath) { @@ -125,7 +125,7 @@ func copyKernelAssets(prefixDir, grubTargetDir string) error { // dynamic grub setup. Needed for when you rollback over the switch to // static grub. func MigrateToDynamicGrub() error { - grubConfigRaw, err := ioutil.ReadFile(bootloaderGrubConfigFile) + grubConfigRaw, err := ioutil.ReadFile(bootloaderGrubConfigFile()) if err != nil && !os.IsNotExist(err) { return err } @@ -149,5 +149,5 @@ func MigrateToDynamicGrub() error { } } - return helpers.AtomicWriteFile(bootloaderGrubConfigFile, []byte(newGrubConfig), 0644, 0) + return helpers.AtomicWriteFile(bootloaderGrubConfigFile(), []byte(newGrubConfig), 0644, 0) } diff --git a/partition/migrate_grub_test.go b/partition/migrate_grub_test.go index 60bb6fd871..0661a802a1 100644 --- a/partition/migrate_grub_test.go +++ b/partition/migrate_grub_test.go @@ -22,7 +22,6 @@ package partition import ( "bytes" "io/ioutil" - "path/filepath" . "gopkg.in/check.v1" ) @@ -43,8 +42,7 @@ if [ "${next_entry}" ] ; then ` func (s *PartitionTestSuite) TestMigrateDetectsOldConfig(c *C) { - bootloaderGrubConfigFile = filepath.Join(c.MkDir(), "grub.cfg") - err := ioutil.WriteFile(bootloaderGrubConfigFile, []byte(oldConfigHeader), 0644) + err := ioutil.WriteFile(bootloaderGrubConfigFile(), []byte(oldConfigHeader), 0644) c.Assert(err, IsNil) r := bytes.NewBufferString(oldConfigHeader) @@ -52,8 +50,7 @@ func (s *PartitionTestSuite) TestMigrateDetectsOldConfig(c *C) { } func (s *PartitionTestSuite) TestMigrateNotMisdetects(c *C) { - bootloaderGrubConfigFile = filepath.Join(c.MkDir(), "grub.cfg") - err := ioutil.WriteFile(bootloaderGrubConfigFile, []byte(newGrubConfig), 0644) + err := ioutil.WriteFile(bootloaderGrubConfigFile(), []byte(newGrubConfig), 0644) c.Assert(err, IsNil) r := bytes.NewBufferString(oldConfigHeader) diff --git a/partition/partition.go b/partition/partition.go index 782ad2e49a..722a6736a8 100644 --- a/partition/partition.go +++ b/partition/partition.go @@ -334,7 +334,15 @@ func (p *Partition) ToggleNextBoot() (err error) { } // MarkBootSuccessful marks the boot as successful -func (p *Partition) MarkBootSuccessful() (err error) { +func (p *Partition) MarkBootSuccessful() error { + if p.rootPartition() != nil { + return p.markBootSuccessfulSnappyAB() + } + + return p.markBootSuccessfulAllSnaps() +} + +func (p *Partition) markBootSuccessfulSnappyAB() error { bootloader, err := bootloader(p) if err != nil { return err @@ -344,6 +352,11 @@ func (p *Partition) MarkBootSuccessful() (err error) { return bootloader.MarkCurrentBootSuccessful(currentRootfs) } +// FIXME: stub +func (p *Partition) markBootSuccessfulAllSnaps() error { + panic("markBootSuccessfulAllSnaps is not implemented yet") +} + // IsNextBootOther return true if the next boot will use the other rootfs // partition. func (p *Partition) IsNextBootOther() bool { diff --git a/partition/partition_test.go b/partition/partition_test.go index b14e7c53cc..10e8cd2dde 100644 --- a/partition/partition_test.go +++ b/partition/partition_test.go @@ -23,11 +23,12 @@ import ( "errors" "io/ioutil" "os" - "path/filepath" "strings" "testing" . "gopkg.in/check.v1" + + "github.com/ubuntu-core/snappy/dirs" ) // Hook up check.v1 into the "go test" runner @@ -51,17 +52,12 @@ func (s *PartitionTestSuite) SetUpTest(c *C) { // custom mount target mountTarget = c.MkDir() - // setup fake paths for grub - bootloaderGrubDir = filepath.Join(s.tempdir, "boot", "grub") - bootloaderGrubConfigFile = filepath.Join(bootloaderGrubDir, "grub.cfg") - bootloaderGrubEnvFile = filepath.Join(bootloaderGrubDir, "grubenv") - - // and uboot - bootloaderUbootDir = filepath.Join(s.tempdir, "boot", "uboot") - bootloaderUbootConfigFile = filepath.Join(bootloaderUbootDir, "uEnv.txt") - bootloaderUbootEnvFile = filepath.Join(bootloaderUbootDir, "uEnv.txt") - bootloaderUbootFwEnvFile = filepath.Join(bootloaderUbootDir, "uboot.env") - bootloaderUbootStampFile = filepath.Join(bootloaderUbootDir, "snappy-stamp.txt") + // global roto + dirs.SetRootDir(s.tempdir) + err := os.MkdirAll(bootloaderGrubDir(), 0755) + c.Assert(err, IsNil) + err = os.MkdirAll(bootloaderUbootDir(), 0755) + c.Assert(err, IsNil) c.Assert(mounts, DeepEquals, mountEntryArray(nil)) } @@ -76,16 +72,6 @@ func (s *PartitionTestSuite) TearDownTest(c *C) { hardwareSpecFile = hardwareSpecFileReal mountTarget = mountTargetReal - // grub vars - bootloaderGrubConfigFile = bootloaderGrubConfigFileReal - bootloaderGrubEnvFile = bootloaderGrubEnvFileReal - - // uboot vars - bootloaderUbootDir = bootloaderUbootDirReal - bootloaderUbootConfigFile = bootloaderUbootConfigFileReal - bootloaderUbootEnvFile = bootloaderUbootEnvFileReal - bootloaderUbootStampFile = bootloaderUbootStampFileReal - c.Assert(mounts, DeepEquals, mountEntryArray(nil)) } diff --git a/pkg/snapfs/pkg.go b/pkg/squashfs/squashfs.go index d7dd5da7e7..53e5e784ff 100644 --- a/pkg/snapfs/pkg.go +++ b/pkg/squashfs/squashfs.go @@ -17,7 +17,7 @@ * */ -package snapfs +package squashfs import ( "fmt" @@ -40,12 +40,12 @@ func (s *Snap) Name() string { return filepath.Base(s.path) } -// New returns a new Snapfs snap +// New returns a new Squashfs snap func New(path string) *Snap { return &Snap{path: path} } -// Close is not doing anything for snapfs - COMPAT +// Close is not doing anything for squashfs - COMPAT func (s *Snap) Close() error { return nil } @@ -60,7 +60,7 @@ func (s *Snap) MetaMember(metaMember string) ([]byte, error) { return s.ReadFile(filepath.Join("meta", metaMember)) } -// ExtractHashes does notthing for snapfs snaps - COMAPT +// ExtractHashes does notthing for squashfs snaps - COMAPT func (s *Snap) ExtractHashes(dir string) error { return nil } @@ -94,7 +94,7 @@ func (s *Snap) Unpack(src, dstDir string) error { return runCommand("unsquashfs", "-f", "-i", "-d", dstDir, s.path, src) } -// ReadFile returns the content of a single file inside a snapfs snap +// ReadFile returns the content of a single file inside a squashfs snap func (s *Snap) ReadFile(path string) (content []byte, err error) { tmpdir, err := ioutil.TempDir("", "read-file") if err != nil { @@ -118,7 +118,7 @@ func (s *Snap) CopyBlob(targetFile string) error { // Verify verifies the snap func (s *Snap) Verify(unauthOk bool) error { - // FIXME: there is no verification yet for snapfs packages, this + // FIXME: there is no verification yet for squashfs packages, this // will be done via assertions later for now we rely on // the https security return nil diff --git a/pkg/snapfs/pkg_test.go b/pkg/squashfs/squashfs_test.go index 86b44ab433..42e4445e91 100644 --- a/pkg/snapfs/pkg_test.go +++ b/pkg/squashfs/squashfs_test.go @@ -17,7 +17,7 @@ * */ -package snapfs +package squashfs import ( "io/ioutil" diff --git a/po/snappy.pot b/po/snappy.pot index 35b4f9a040..41c4f5285f 100644 --- a/po/snappy.pot +++ b/po/snappy.pot @@ -7,7 +7,7 @@ msgid "" msgstr "Project-Id-Version: snappy\n" "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n" - "POT-Creation-Date: 2015-11-05 15:47-0200\n" + "POT-Creation-Date: 2015-11-17 19:13+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -113,6 +113,12 @@ msgstr "" msgid "First boot has already run" msgstr "" +msgid "Force policy generation." +msgstr "" + +msgid "Generate the apparmor policy" +msgstr "" + #. TRANSLATORS: the %s is a pkgname #, c-format msgid "Generated '%s' snap\n" @@ -204,11 +210,6 @@ msgstr "" msgid "Reboot to use %s version %s." msgstr "" -#. TRANSLATORS: the %s is a pkgname -#, c-format -msgid "Reboot to use the new %s." -msgstr "" - #. TRANSLATORS: the %s shows a comma separated list #. of package names #, c-format @@ -312,6 +313,9 @@ msgstr "" msgid "The package to rollback " msgstr "" +msgid "The path to the package.yaml used to generate the apparmor policy." +msgstr "" + msgid "The version to rollback to" msgstr "" @@ -378,6 +382,9 @@ msgstr "" msgid "installed: %s\n" msgstr "" +msgid "must supply path to package.yaml" +msgstr "" + msgid "package name is required" msgstr "" diff --git a/policy/policy.go b/policy/policy.go index 26f5563976..204aced8a7 100644 --- a/policy/policy.go +++ b/policy/policy.go @@ -91,7 +91,7 @@ func iterOp(op policyOp, glob, targetDir, prefix string) (err error) { } case install: // do the copy - if err := helpers.CopyFile(file, targetFile, helpers.CopyFlagSync); err != nil { + if err := helpers.CopyFile(file, targetFile, helpers.CopyFlagSync|helpers.CopyFlagOverwrite); err != nil { return err } default: diff --git a/progress/progress.go b/progress/progress.go index d6cbf6dedd..7f9037fd2a 100644 --- a/progress/progress.go +++ b/progress/progress.go @@ -49,7 +49,7 @@ type Meter interface { Write(p []byte) (n int, err error) // ask the user whether they agree to the given license's text - Agreed(intro, licenseFile string) bool + Agreed(intro, license string) bool // notify the user of miscelaneous events Notify(string) @@ -88,7 +88,7 @@ func (t *NullProgress) Spin(msg string) { } // Agreed does nothing -func (t *NullProgress) Agreed(intro, licenseFile string) bool { +func (t *NullProgress) Agreed(intro, license string) bool { return false } diff --git a/snappy/arch.go b/snappy/arch.go deleted file mode 100644 index ddaac3ea51..0000000000 --- a/snappy/arch.go +++ /dev/null @@ -1,48 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2014-2015 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 snappy - -import ( - "github.com/ubuntu-core/snappy/helpers" -) - -// ArchitectureType is the type for a supported snappy architecture -type ArchitectureType string - -const ( - // Archi386 is the i386 architecture - Archi386 ArchitectureType = "i386" - // ArchAmd64 is the amd64 architecture - ArchAmd64 = "amd64" - // ArchArmhf is the armhf architecture - ArchArmhf = "armhf" -) - -var arch = ArchitectureType(helpers.UbuntuArchitecture()) - -// Architecture returns the native architecture that snappy runs on -func Architecture() ArchitectureType { - return arch -} - -// SetArchitecture allows overriding the auto detected Architecture -func SetArchitecture(newArch ArchitectureType) { - arch = newArch -} diff --git a/snappy/build.go b/snappy/build.go index d1f1e729ba..bcc9695273 100644 --- a/snappy/build.go +++ b/snappy/build.go @@ -34,7 +34,7 @@ import ( "github.com/ubuntu-core/snappy/helpers" "github.com/ubuntu-core/snappy/pkg/clickdeb" - "github.com/ubuntu-core/snappy/pkg/snapfs" + "github.com/ubuntu-core/snappy/pkg/squashfs" "gopkg.in/yaml.v2" ) @@ -180,53 +180,6 @@ func parseReadme(readme string) (title, description string, err error) { return title, description, nil } -func handleBinaries(buildDir string, m *packageYaml) error { - for _, v := range m.Binaries { - hookName := filepath.Base(v.Name) - // handle the apparmor stuff - if err := handleApparmor(buildDir, m, hookName, &v.SecurityDefinitions); err != nil { - return err - } - } - - return nil -} - -func handleServices(buildDir string, m *packageYaml) error { - for _, v := range m.ServiceYamls { - hookName := filepath.Base(v.Name) - - // handle the apparmor stuff - if err := handleApparmor(buildDir, m, hookName, &v.SecurityDefinitions); err != nil { - return err - } - } - - return nil -} - -func handleConfigHookApparmor(buildDir string, m *packageYaml) error { - configHookFile := filepath.Join(buildDir, "meta", "hooks", "config") - if !helpers.FileExists(configHookFile) { - return nil - } - - hookName := "snappy-config" - s := &SecurityDefinitions{} - content, err := s.generateApparmorJSONContent() - if err != nil { - return err - } - configApparmorJSONFile := filepath.Join("meta", hookName+".apparmor") - if err := ioutil.WriteFile(filepath.Join(buildDir, configApparmorJSONFile), content, 0644); err != nil { - return err - } - m.Integration[hookName] = make(map[string]string) - m.Integration[hookName]["apparmor"] = configApparmorJSONFile - - return nil -} - // the du(1) command, useful to override for testing var duCmd = "du" @@ -509,21 +462,6 @@ func prepare(sourceDir, targetDir, buildDir string) (snapName string, err error) return "", err } - // generate compat hooks for binaries - if err := handleBinaries(buildDir, m); err != nil { - return "", err - } - - // generate compat hooks for services - if err := handleServices(buildDir, m); err != nil { - return "", err - } - - // generate config hook apparmor - if err := handleConfigHookApparmor(buildDir, m); err != nil { - return "", err - } - if err := writeDebianControl(buildDir, m); err != nil { return "", err } @@ -548,8 +486,9 @@ func prepare(sourceDir, targetDir, buildDir string) (snapName string, err error) return snapName, nil } -// BuildSnapfsSnap the given sourceDirectory and return the generated snap file -func BuildSnapfsSnap(sourceDir, targetDir string) (string, error) { +// BuildSquashfsSnap the given sourceDirectory and return the generated +// snap file +func BuildSquashfsSnap(sourceDir, targetDir string) (string, error) { // create build dir buildDir, err := ioutil.TempDir("", "snappy-build-") if err != nil { @@ -562,7 +501,7 @@ func BuildSnapfsSnap(sourceDir, targetDir string) (string, error) { return "", err } - d := snapfs.New(snapName) + d := squashfs.New(snapName) if err = d.Build(buildDir); err != nil { return "", err } diff --git a/snappy/build_test.go b/snappy/build_test.go index 326bba3fcb..458467bbc3 100644 --- a/snappy/build_test.go +++ b/snappy/build_test.go @@ -174,7 +174,6 @@ binaries: "title": "some title", "hooks": { "hello-world": { - "apparmor": "meta/hello-world.apparmor", "bin-path": "bin/hello-world" } } @@ -213,9 +212,7 @@ services: "installed-size": "17", "title": "some title", "hooks": { - "foo": { - "apparmor": "meta/foo.apparmor" - } + "foo": {} } }` readJSON, err := exec.Command("dpkg-deb", "-I", "hello_3.0.1_all.snap", "manifest").Output() @@ -500,7 +497,7 @@ version: 1.0.1 c.Assert(err, ErrorMatches, "can not handle type of file .*") } -func (s *SnapTestSuite) TestBuildSnapfsSimple(c *C) { +func (s *SnapTestSuite) TestBuildSquashfsSimple(c *C) { sourceDir := makeExampleSnapSourceDir(c, `name: hello version: 1.0.1 architecture: ["i386", "amd64"] @@ -509,7 +506,7 @@ integration: apparmor-profile: meta/hello.apparmor `) - resultSnap, err := BuildSnapfsSnap(sourceDir, "") + resultSnap, err := BuildSquashfsSnap(sourceDir, "") c.Assert(err, IsNil) defer os.Remove(resultSnap) diff --git a/snappy/click.go b/snappy/click.go index 0451f5fdd3..68f786daff 100644 --- a/snappy/click.go +++ b/snappy/click.go @@ -41,6 +41,7 @@ import ( "text/template" "time" + "github.com/ubuntu-core/snappy/arch" "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/helpers" "github.com/ubuntu-core/snappy/i18n" @@ -78,8 +79,10 @@ type clickHook struct { // ignore hooks of this type var ignoreHooks = map[string]bool{ - "bin-path": true, - "snappy-systemd": true, + "bin-path": true, + "snappy-systemd": true, + "apparmor": true, + "apparmor-profile": true, } // wait this time between TERM and KILL @@ -340,7 +343,7 @@ ubuntu-core-launcher {{.UdevAppName}} {{.AaProfile}} {{.Target}} "$@" NewAppVars string }{ AppName: m.Name, - AppArch: helpers.UbuntuArchitecture(), + AppArch: arch.UbuntuArchitecture(), AppPath: pkgPath, Version: m.Version, UdevAppName: udevPartName, @@ -666,74 +669,6 @@ func (m *packageYaml) removePackageBinaries(baseDir string) error { return nil } -func (m *packageYaml) addOneSecurityPolicy(name string, sd SecurityDefinitions, baseDir string) error { - profileName, err := getSecurityProfile(m, filepath.Base(name), baseDir) - if err != nil { - return err - } - content, err := generateSeccompPolicy(baseDir, name, sd) - if err != nil { - return err - } - - fn := filepath.Join(dirs.SnapSeccompDir, profileName) - if err := helpers.AtomicWriteFile(fn, content, 0644, 0); err != nil { - return err - } - - return nil -} - -func (m *packageYaml) addSecurityPolicy(baseDir string) error { - // TODO: move apparmor policy generation here too, its currently - // done via the click hooks but we really want to generate - // it all here - - for _, svc := range m.ServiceYamls { - if err := m.addOneSecurityPolicy(svc.Name, svc.SecurityDefinitions, baseDir); err != nil { - return err - } - } - - for _, bin := range m.Binaries { - if err := m.addOneSecurityPolicy(bin.Name, bin.SecurityDefinitions, baseDir); err != nil { - return err - } - } - - return nil -} - -func (m *packageYaml) removeOneSecurityPolicy(name, baseDir string) error { - profileName, err := getSecurityProfile(m, filepath.Base(name), baseDir) - if err != nil { - return err - } - fn := filepath.Join(dirs.SnapSeccompDir, profileName) - if err := os.Remove(fn); err != nil && !os.IsNotExist(err) { - return err - } - - return nil -} - -func (m *packageYaml) removeSecurityPolicy(baseDir string) error { - // TODO: move apparmor policy removal here - for _, service := range m.ServiceYamls { - if err := m.removeOneSecurityPolicy(service.Name, baseDir); err != nil { - return err - } - } - - for _, binary := range m.Binaries { - if err := m.removeOneSecurityPolicy(binary.Name, baseDir); err != nil { - return err - } - } - - return nil -} - type agreer interface { Agreed(intro, license string) bool } diff --git a/snappy/click_test.go b/snappy/click_test.go index 508fd48406..0cab022a2a 100644 --- a/snappy/click_test.go +++ b/snappy/click_test.go @@ -31,6 +31,7 @@ import ( "github.com/mvo5/goconfigparser" . "gopkg.in/check.v1" + "github.com/ubuntu-core/snappy/arch" "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/helpers" "github.com/ubuntu-core/snappy/pkg" @@ -38,6 +39,7 @@ import ( "github.com/ubuntu-core/snappy/policy" "github.com/ubuntu-core/snappy/progress" "github.com/ubuntu-core/snappy/systemd" + "github.com/ubuntu-core/snappy/timeout" ) func (s *SnapTestSuite) TestReadManifest(c *C) { @@ -160,13 +162,6 @@ Pattern: /var/lib/apparmor/click/${id} c.Assert(err, IsNil) c.Assert(symlinkTarget, Equals, filepath.Join(instDir, "path-to-systemd-file")) - p = fmt.Sprintf("%s/%s.%s_%s_%s", testSymlinkDir2, m.Name, testOrigin, "app", m.Version) - _, err = os.Stat(p) - c.Assert(err, IsNil) - symlinkTarget, err = filepath.EvalSymlinks(p) - c.Assert(err, IsNil) - c.Assert(symlinkTarget, Equals, filepath.Join(instDir, "path-to-apparmor-file")) - // now ensure we can remove err = removeClickHooks(m, testOrigin, false) c.Assert(err, IsNil) @@ -255,6 +250,7 @@ version: 1.0 explicit-license-agreement: Y`) _, err := installClick(pkg, 0, nil, testOrigin) c.Check(err, Equals, ErrLicenseNotAccepted) + c.Check(IsLicenseNotAccepted(err), Equals, true) } // if the snap asks for accepting a license, and an agreer is provided, and @@ -266,6 +262,7 @@ version: 1.0 explicit-license-agreement: Y`) _, err := installClick(pkg, 0, &MockProgressMeter{y: false}, testOrigin) c.Check(err, Equals, ErrLicenseNotAccepted) + c.Check(IsLicenseNotAccepted(err), Equals, true) } // if the snap asks for accepting a license, and an agreer is provided, but @@ -280,6 +277,7 @@ version: 1.0 explicit-license-agreement: Y`, false) _, err := installClick(pkg, 0, &MockProgressMeter{y: true}, testOrigin) c.Check(err, Equals, ErrLicenseNotProvided) + c.Check(IsLicenseNotAccepted(err), Equals, false) } // if the snap asks for accepting a license, and an agreer is provided, and @@ -291,6 +289,7 @@ version: 1.0 explicit-license-agreement: Y`) _, err := installClick(pkg, 0, &MockProgressMeter{y: true}, testOrigin) c.Check(err, Equals, nil) + c.Check(IsLicenseNotAccepted(err), Equals, false) } // Agreed is given reasonable values for intro and license @@ -302,6 +301,7 @@ explicit-license-agreement: Y`) ag := &MockProgressMeter{y: true} _, err := installClick(pkg, 0, ag, testOrigin) c.Assert(err, Equals, nil) + c.Check(IsLicenseNotAccepted(err), Equals, false) c.Check(ag.intro, Matches, ".*foobar.*requires.*license.*") c.Check(ag.license, Equals, "WTFPL") } @@ -325,6 +325,7 @@ license-version: 2 pkg := makeTestSnapPackage(c, yaml+"version: 2") _, err = installClick(pkg, 0, ag, testOrigin) c.Assert(err, Equals, nil) + c.Check(IsLicenseNotAccepted(err), Equals, false) c.Check(ag.intro, Equals, "") c.Check(ag.license, Equals, "") } @@ -347,6 +348,7 @@ version: 1.0 pkg := makeTestSnapPackage(c, yaml+"version: 2\nexplicit-license-agreement: Y\n") _, err = installClick(pkg, 0, ag, testOrigin) + c.Check(IsLicenseNotAccepted(err), Equals, false) c.Assert(err, Equals, nil) c.Check(ag.license, Equals, "WTFPL") } @@ -369,6 +371,7 @@ explicit-license-agreement: Y pkg := makeTestSnapPackage(c, yaml+"license-version: 3\nversion: 2") _, err = installClick(pkg, 0, ag, testOrigin) c.Assert(err, Equals, nil) + c.Check(IsLicenseNotAccepted(err), Equals, false) c.Check(ag.license, Equals, "WTFPL") } @@ -774,7 +777,7 @@ func (s *SnapTestSuite) TestSnappyGenerateSnapBinaryWrapper(c *C) { m := packageYaml{Name: "pastebinit", Version: "1.4.0.0.1"} - expected := fmt.Sprintf(expectedWrapper, helpers.UbuntuArchitecture()) + expected := fmt.Sprintf(expectedWrapper, arch.UbuntuArchitecture()) generatedWrapper, err := generateSnapBinaryWrapper(binary, pkgPath, aaProfile, &m) c.Assert(err, IsNil) @@ -793,7 +796,7 @@ func (s *SnapTestSuite) TestSnappyGenerateSnapBinaryWrapperFmk(c *C) { expected = strings.Replace(expected, `NAME="pastebinit"`, `NAME="fmk"`, 1) expected = strings.Replace(expected, "mvo", "", -1) expected = strings.Replace(expected, "pastebinit", "echo", -1) - expected = fmt.Sprintf(expected, helpers.UbuntuArchitecture()) + expected = fmt.Sprintf(expected, arch.UbuntuArchitecture()) generatedWrapper, err := generateSnapBinaryWrapper(binary, pkgPath, aaProfile, &m) c.Assert(err, IsNil) @@ -1217,11 +1220,11 @@ TimeoutStopSec=30 [Install] WantedBy=multi-user.target ` - expectedServiceAppWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target", ".canonical", "canonical", "\n", helpers.UbuntuArchitecture()) - expectedNetAppWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target\nAfter=snappy-wait4network.service\nRequires=snappy-wait4network.service", ".canonical", "canonical", "\n", helpers.UbuntuArchitecture()) - expectedServiceFmkWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "Before=ubuntu-snappy.frameworks.target\nAfter=ubuntu-snappy.frameworks-pre.target\nRequires=ubuntu-snappy.frameworks-pre.target", "", "", "BusName=foo.bar.baz\nType=dbus", helpers.UbuntuArchitecture()) - expectedSocketUsingWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "After=ubuntu-snappy.frameworks.target xkcd-webserver_xkcd-webserver_0.3.4.socket\nRequires=ubuntu-snappy.frameworks.target xkcd-webserver_xkcd-webserver_0.3.4.socket", ".canonical", "canonical", "\n", helpers.UbuntuArchitecture()) - expectedTypeForkingFmkWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target", ".canonical", "canonical", "Type=forking\n", helpers.UbuntuArchitecture()) + expectedServiceAppWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target", ".canonical", "canonical", "\n", arch.UbuntuArchitecture()) + expectedNetAppWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target\nAfter=snappy-wait4network.service\nRequires=snappy-wait4network.service", ".canonical", "canonical", "\n", arch.UbuntuArchitecture()) + expectedServiceFmkWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "Before=ubuntu-snappy.frameworks.target\nAfter=ubuntu-snappy.frameworks-pre.target\nRequires=ubuntu-snappy.frameworks-pre.target", "", "", "BusName=foo.bar.baz\nType=dbus", arch.UbuntuArchitecture()) + expectedSocketUsingWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "After=ubuntu-snappy.frameworks.target xkcd-webserver_xkcd-webserver_0.3.4.socket\nRequires=ubuntu-snappy.frameworks.target xkcd-webserver_xkcd-webserver_0.3.4.socket", ".canonical", "canonical", "\n", arch.UbuntuArchitecture()) + expectedTypeForkingFmkWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target", ".canonical", "canonical", "Type=forking\n", arch.UbuntuArchitecture()) ) func (s *SnapTestSuite) TestSnappyGenerateSnapServiceTypeForking(c *C) { @@ -1230,7 +1233,7 @@ func (s *SnapTestSuite) TestSnappyGenerateSnapServiceTypeForking(c *C) { Start: "bin/foo start", Stop: "bin/foo stop", PostStop: "bin/foo post-stop", - StopTimeout: DefaultTimeout, + StopTimeout: timeout.DefaultTimeout, Description: "A fun webserver", Forking: true, } @@ -1250,7 +1253,7 @@ func (s *SnapTestSuite) TestSnappyGenerateSnapServiceAppWrapper(c *C) { Start: "bin/foo start", Stop: "bin/foo stop", PostStop: "bin/foo post-stop", - StopTimeout: DefaultTimeout, + StopTimeout: timeout.DefaultTimeout, Description: "A fun webserver", } pkgPath := "/apps/xkcd-webserver.canonical/0.3.4/" @@ -1269,7 +1272,7 @@ func (s *SnapTestSuite) TestSnappyGenerateSnapServiceAppWrapperWithExternalPort( Start: "bin/foo start", Stop: "bin/foo stop", PostStop: "bin/foo post-stop", - StopTimeout: DefaultTimeout, + StopTimeout: timeout.DefaultTimeout, Description: "A fun webserver", Ports: &Ports{External: map[string]Port{"foo": Port{}}}, } @@ -1289,7 +1292,7 @@ func (s *SnapTestSuite) TestSnappyGenerateSnapServiceFmkWrapper(c *C) { Start: "bin/foo start", Stop: "bin/foo stop", PostStop: "bin/foo post-stop", - StopTimeout: DefaultTimeout, + StopTimeout: timeout.DefaultTimeout, Description: "A fun webserver", BusName: "foo.bar.baz", } @@ -1311,7 +1314,7 @@ func (s *SnapTestSuite) TestSnappyGenerateSnapServiceWrapperWhitelist(c *C) { Start: "bin/foo start", Stop: "bin/foo stop", PostStop: "bin/foo post-stop", - StopTimeout: DefaultTimeout, + StopTimeout: timeout.DefaultTimeout, Description: "A fun webserver\nExec=foo", } pkgPath := "/apps/xkcd-webserver.canonical/0.3.4/" @@ -1354,7 +1357,7 @@ func (s *SnapTestSuite) TestBinariesWhitelistSimple(c *C) { c.Assert(verifyBinariesYaml(Binary{ SecurityDefinitions: SecurityDefinitions{ SecurityPolicy: &SecurityPolicyDefinition{ - Apparmor: "foo"}, + AppArmor: "foo"}, }, }), IsNil) } @@ -1370,7 +1373,7 @@ func (s *SnapTestSuite) TestBinariesWhitelistIllegal(c *C) { c.Assert(verifyBinariesYaml(Binary{ SecurityDefinitions: SecurityDefinitions{ SecurityPolicy: &SecurityPolicyDefinition{ - Apparmor: "x\n"}, + AppArmor: "x\n"}, }, }), NotNil) } @@ -1459,59 +1462,6 @@ Pattern: /var/lib/systemd/click/${id} c.Assert(stripGlobalRootDirWasCalled, Equals, true) } -func (s *SnapTestSuite) TestPackageYamlAddSecurityPolicy(c *C) { - m, err := parsePackageYamlData([]byte(`name: foo -version: 1.0 -binaries: - - name: foo -services: - - name: bar - start: baz -`), false) - c.Assert(err, IsNil) - - dirs.SnapSeccompDir = c.MkDir() - err = m.addSecurityPolicy("/apps/foo.mvo/1.0/") - c.Assert(err, IsNil) - - binSeccompContent, err := ioutil.ReadFile(filepath.Join(dirs.SnapSeccompDir, "foo.mvo_foo_1.0")) - c.Assert(string(binSeccompContent), Equals, scFilterGenFakeResult) - - serviceSeccompContent, err := ioutil.ReadFile(filepath.Join(dirs.SnapSeccompDir, "foo.mvo_bar_1.0")) - c.Assert(string(serviceSeccompContent), Equals, scFilterGenFakeResult) - -} - -func (s *SnapTestSuite) TestPackageYamlRemoveSecurityPolicy(c *C) { - m, err := parsePackageYamlData([]byte(`name: foo -version: 1.0 -binaries: - - name: foo -services: - - name: bar - start: baz -`), false) - c.Assert(err, IsNil) - - dirs.SnapSeccompDir = c.MkDir() - binSeccomp := filepath.Join(dirs.SnapSeccompDir, "foo.mvo_foo_1.0") - serviceSeccomp := filepath.Join(dirs.SnapSeccompDir, "foo.mvo_bar_1.0") - c.Assert(helpers.FileExists(binSeccomp), Equals, false) - c.Assert(helpers.FileExists(serviceSeccomp), Equals, false) - - // add it now - err = m.addSecurityPolicy("/apps/foo.mvo/1.0/") - c.Assert(err, IsNil) - c.Assert(helpers.FileExists(binSeccomp), Equals, true) - c.Assert(helpers.FileExists(serviceSeccomp), Equals, true) - - // ensure that it removes the files on remove - err = m.removeSecurityPolicy("/apps/foo.mvo/1.0/") - c.Assert(err, IsNil) - c.Assert(helpers.FileExists(binSeccomp), Equals, false) - c.Assert(helpers.FileExists(serviceSeccomp), Equals, false) -} - func (s *SnapTestSuite) TestRemovePackageServiceKills(c *C) { // make Stop not work var sysdLog [][]string @@ -1599,7 +1549,7 @@ func (s *SnapTestSuite) TestSnappyGenerateSnapServiceWithSockte(c *C) { Start: "bin/foo start", Stop: "bin/foo stop", PostStop: "bin/foo post-stop", - StopTimeout: DefaultTimeout, + StopTimeout: timeout.DefaultTimeout, Description: "A fun webserver", Socket: true, } diff --git a/snappy/common_test.go b/snappy/common_test.go index b448d52e13..ff1ee6f802 100644 --- a/snappy/common_test.go +++ b/snappy/common_test.go @@ -41,7 +41,7 @@ const ( helloAppComposedName = "hello-app.testspacethename" ) -// here to make it easy to switch in tests to "BuildSnapfsSnap" +// here to make it easy to switch in tests to "BuildSquashfsSnap" var snapBuilderFunc = BuildLegacySnap // makeInstalledMockSnap creates a installed mock snap without any @@ -82,7 +82,19 @@ services: return "", err } - if err := addDefaultApparmorJSON(tempdir, "hello-app_hello_1.10.json"); err != nil { + if err := addMockDefaultApparmorProfile("hello-app_hello_1.10"); err != nil { + return "", err + } + + if err := addMockDefaultApparmorProfile("hello-app_svc1_1.10"); err != nil { + return "", err + } + + if err := addMockDefaultSeccompProfile("hello-app_hello_1.10"); err != nil { + return "", err + } + + if err := addMockDefaultSeccompProfile("hello-app_svc1_1.10"); err != nil { return "", err } @@ -113,6 +125,9 @@ func storeMinimalRemoteManifest(qn, name, origin, version, desc, channel string) return err } + if err := os.MkdirAll(dirs.SnapMetaDir, 0755); err != nil { + return err + } if err := ioutil.WriteFile(filepath.Join(dirs.SnapMetaDir, fmt.Sprintf("%s_%s.manifest", qn, version)), content, 0644); err != nil { return err } @@ -120,19 +135,38 @@ func storeMinimalRemoteManifest(qn, name, origin, version, desc, channel string) return nil } -func addDefaultApparmorJSON(tempdir, apparmorJSONPath string) error { - appArmorDir := filepath.Join(tempdir, "var", "lib", "apparmor", "clicks") +func addMockDefaultApparmorProfile(appid string) error { + appArmorDir := dirs.SnapAppArmorDir + if err := os.MkdirAll(appArmorDir, 0775); err != nil { return err } - const securityJSON = `{ - "policy_vendor": "ubuntu-core" - "policy_version": 15.04 + const securityProfile = ` +#include <tunables/global> +profile "foo" (attach_disconnected) { + #include <abstractions/base> }` - apparmorFile := filepath.Join(appArmorDir, apparmorJSONPath) - return ioutil.WriteFile(apparmorFile, []byte(securityJSON), 0644) + apparmorFile := filepath.Join(appArmorDir, appid) + return ioutil.WriteFile(apparmorFile, []byte(securityProfile), 0644) +} + +func addMockDefaultSeccompProfile(appid string) error { + seccompDir := dirs.SnapSeccompDir + + if err := os.MkdirAll(seccompDir, 0775); err != nil { + return err + } + + const securityProfile = ` +open +write +connect +` + + seccompFile := filepath.Join(seccompDir, appid) + return ioutil.WriteFile(seccompFile, []byte(securityProfile), 0644) } // makeTestSnapPackage creates a real snap package that can be installed on @@ -260,12 +294,7 @@ func (m *MockProgressMeter) Notify(msg string) { m.notified = append(m.notified, msg) } -// seccomp filter mocks -const scFilterGenFakeResult = ` -syscall1 -syscall2 -` - -func mockRunScFilterGen(argv ...string) ([]byte, error) { - return []byte(scFilterGenFakeResult), nil +// apparmor_parser mocks +func mockRunAppArmorParser(argv ...string) ([]byte, error) { + return nil, nil } diff --git a/snappy/errors.go b/snappy/errors.go index 254f8f6ea0..32d89c82aa 100644 --- a/snappy/errors.go +++ b/snappy/errors.go @@ -25,7 +25,7 @@ import ( "net/url" "strings" - "github.com/ubuntu-core/snappy/helpers" + "github.com/ubuntu-core/snappy/arch" ) var ( @@ -165,7 +165,7 @@ type ErrArchitectureNotSupported struct { } func (e *ErrArchitectureNotSupported) Error() string { - return fmt.Sprintf("package's supported architectures (%s) is incompatible with this system (%s)", strings.Join(e.Architectures, ", "), helpers.UbuntuArchitecture()) + return fmt.Sprintf("package's supported architectures (%s) is incompatible with this system (%s)", strings.Join(e.Architectures, ", "), arch.UbuntuArchitecture()) } // ErrInstallFailed is an error type for installation errors for snaps @@ -273,3 +273,19 @@ func (e *ErrInvalidYaml) Error() string { // %#v of string(yaml) so the yaml is presented as a human-readable string, but in a single greppable line return fmt.Sprintf("can not parse %s: %v (from: %#v)", e.File, e.Err, string(e.Yaml)) } + +// IsLicenseNotAccepted checks whether err is (directly or indirectly) +// due to a ErrLicenseNotAccepted +func IsLicenseNotAccepted(err error) bool { + if err == ErrLicenseNotAccepted { + return true + } + + if err, ok := err.(*ErrInstallFailed); ok { + if err.OrigErr == ErrLicenseNotAccepted { + return true + } + } + + return false +} diff --git a/snappy/hwaccess.go b/snappy/hwaccess.go index ffd92e0809..bda84904da 100644 --- a/snappy/hwaccess.go +++ b/snappy/hwaccess.go @@ -21,30 +21,23 @@ package snappy import ( "bufio" - "encoding/json" "fmt" "io/ioutil" "os" - "os/exec" "path/filepath" "strings" + "gopkg.in/yaml.v2" + "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/helpers" ) const udevDataGlob = "/run/udev/data/*" -var aaClickHookCmd = "aa-clickhook" - -type appArmorAdditionalJSON struct { - WritePath []string `json:"write_path,omitempty"` - ReadPath []string `json:"read_path,omitempty"` -} - -// return the json filename to add to the security json -func getHWAccessJSONFile(snapname string) string { - return filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("%s.json.additional", snapname)) +// return the yaml filename to add to the security yaml +func getHWAccessYamlFile(snapname string) string { + return filepath.Join(dirs.SnapAppArmorAdditionalDir, fmt.Sprintf("%s.hwaccess.yaml", snapname)) } // Return true if the device string is a valid device @@ -60,37 +53,43 @@ func validDevice(device string) bool { return false } -func readHWAccessJSONFile(snapname string) (appArmorAdditionalJSON, error) { - var appArmorAdditional appArmorAdditionalJSON +func readHWAccessYamlFile(snapname string) (SecurityOverrideDefinition, error) { + var appArmorAdditional SecurityOverrideDefinition - additionalFile := getHWAccessJSONFile(snapname) + additionalFile := getHWAccessYamlFile(snapname) f, err := os.Open(additionalFile) if err != nil { return appArmorAdditional, err } - dec := json.NewDecoder(f) - if err := dec.Decode(&appArmorAdditional); err != nil { + content, err := ioutil.ReadAll(f) + if err != nil { + return appArmorAdditional, err + } + if err := yaml.Unmarshal(content, &appArmorAdditional); err != nil { return appArmorAdditional, err } return appArmorAdditional, nil } -func writeHWAccessJSONFile(snapname string, appArmorAdditional appArmorAdditionalJSON) error { - if len(appArmorAdditional.WritePath) == 0 { - appArmorAdditional.ReadPath = nil +func writeHWAccessYamlFile(snapname string, appArmorAdditional SecurityOverrideDefinition) error { + if len(appArmorAdditional.WritePaths) == 0 { + appArmorAdditional.ReadPaths = nil } else { - appArmorAdditional.ReadPath = []string{udevDataGlob} + appArmorAdditional.ReadPaths = []string{udevDataGlob} } - out, err := json.MarshalIndent(appArmorAdditional, "", " ") + out, err := yaml.Marshal(appArmorAdditional) if err != nil { return err } - // append final newline - out = append(out, '\n') - additionalFile := getHWAccessJSONFile(snapname) + additionalFile := getHWAccessYamlFile(snapname) + if !helpers.FileExists(filepath.Dir(additionalFile)) { + if err := os.MkdirAll(filepath.Dir(additionalFile), 0755); err != nil { + return err + } + } if err := helpers.AtomicWriteFile(additionalFile, out, 0640, 0); err != nil { return err } @@ -98,14 +97,9 @@ func writeHWAccessJSONFile(snapname string, appArmorAdditional appArmorAdditiona return nil } -func regenerateAppArmorRulesImpl() error { - if output, err := exec.Command(aaClickHookCmd, "-f").CombinedOutput(); err != nil { - if exitCode, err := helpers.ExitCode(err); err == nil { - return &ErrApparmorGenerate{ - ExitCode: exitCode, - Output: output, - } - } +func regenerateAppArmorRulesImpl(snapname string) error { + err := regeneratePolicyForSnap(snapname) + if err != nil { return err } @@ -168,8 +162,14 @@ func AddHWAccess(snapname, device string) error { return ErrInvalidHWDevice } + // LP: #1499087 - ensure that the snapname is not mixed up with + // an appid, the "_" is reserved for that + if strings.Contains(snapname, "_") { + return ErrPackageNotFound + } + // check if there is anything apparmor related to add to - globExpr := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("%s_*.json", snapname)) + globExpr := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("%s_*", snapname)) matches, err := filepath.Glob(globExpr) if err != nil { return err @@ -179,22 +179,22 @@ func AddHWAccess(snapname, device string) error { } // read .additional file, its ok if the file does not exist (yet) - appArmorAdditional, err := readHWAccessJSONFile(snapname) + appArmorAdditional, err := readHWAccessYamlFile(snapname) if err != nil && !os.IsNotExist(err) { return err } // check for dupes, please golang make this simpler - for _, p := range appArmorAdditional.WritePath { + for _, p := range appArmorAdditional.WritePaths { if p == device { return ErrHWAccessAlreadyAdded } } // add the new write path - appArmorAdditional.WritePath = append(appArmorAdditional.WritePath, device) + appArmorAdditional.WritePaths = append(appArmorAdditional.WritePaths, device) // and write the data out - err = writeHWAccessJSONFile(snapname, appArmorAdditional) + err = writeHWAccessYamlFile(snapname, appArmorAdditional) if err != nil { return err } @@ -205,18 +205,18 @@ func AddHWAccess(snapname, device string) error { } // re-generate apparmor fules - return regenerateAppArmorRules() + return regenerateAppArmorRules(snapname) } // ListHWAccess returns a list of hardware-device strings that the snap // can access func ListHWAccess(snapname string) ([]string, error) { - appArmorAdditional, err := readHWAccessJSONFile(snapname) + appArmorAdditional, err := readHWAccessYamlFile(snapname) if err != nil && !os.IsNotExist(err) { return nil, err } - return appArmorAdditional.WritePath, nil + return appArmorAdditional.WritePaths, nil } func removeUdevRuleForSnap(snapname, device string) error { @@ -269,25 +269,25 @@ func RemoveHWAccess(snapname, device string) error { return ErrInvalidHWDevice } - appArmorAdditional, err := readHWAccessJSONFile(snapname) + appArmorAdditional, err := readHWAccessYamlFile(snapname) if err != nil { return err } // remove write path, please golang make this easier! - newWritePath := []string{} - for _, p := range appArmorAdditional.WritePath { + newWritePaths := []string{} + for _, p := range appArmorAdditional.WritePaths { if p != device { - newWritePath = append(newWritePath, p) + newWritePaths = append(newWritePaths, p) } } - if len(newWritePath) == len(appArmorAdditional.WritePath) { + if len(newWritePaths) == len(appArmorAdditional.WritePaths) { return ErrHWAccessRemoveNotFound } - appArmorAdditional.WritePath = newWritePath + appArmorAdditional.WritePaths = newWritePaths // and write it out again - err = writeHWAccessJSONFile(snapname, appArmorAdditional) + err = writeHWAccessYamlFile(snapname, appArmorAdditional) if err != nil { return err } @@ -301,19 +301,19 @@ func RemoveHWAccess(snapname, device string) error { } // re-generate apparmor rules - return regenerateAppArmorRules() + return regenerateAppArmorRules(snapname) } // RemoveAllHWAccess removes all hw access from the given snap. func RemoveAllHWAccess(snapname string) error { for _, fn := range []string{ udevRulesPathForPart(snapname), - getHWAccessJSONFile(snapname), + getHWAccessYamlFile(snapname), } { if err := os.Remove(fn); err != nil && !os.IsNotExist(err) { return err } } - return regenerateAppArmorRules() + return regenerateAppArmorRules(snapname) } diff --git a/snappy/hwaccess_test.go b/snappy/hwaccess_test.go index 6d24e103e2..5c4d762141 100644 --- a/snappy/hwaccess_test.go +++ b/snappy/hwaccess_test.go @@ -31,7 +31,7 @@ import ( func mockRegenerateAppArmorRules() *bool { regenerateAppArmorRulesWasCalled := false - regenerateAppArmorRules = func() error { + regenerateAppArmorRules = func(string) error { regenerateAppArmorRulesWasCalled = true return nil } @@ -44,16 +44,13 @@ func (s *SnapTestSuite) TestAddHWAccessSimple(c *C) { err := AddHWAccess("hello-app", "/dev/ttyUSB0") c.Assert(err, IsNil) - content, err := ioutil.ReadFile(filepath.Join(dirs.SnapAppArmorDir, "hello-app.json.additional")) + content, err := ioutil.ReadFile(filepath.Join(dirs.SnapAppArmorAdditionalDir, "hello-app.hwaccess.yaml")) c.Assert(err, IsNil) - c.Assert(string(content), Equals, `{ - "write_path": [ - "/dev/ttyUSB0" - ], - "read_path": [ - "/run/udev/data/*" - ] -} + c.Assert("\n"+string(content), Equals, ` +read-paths: +- /run/udev/data/* +write-paths: +- /dev/ttyUSB0 `) // ensure the regenerate code was called c.Assert(*regenerateAppArmorRulesWasCalled, Equals, true) @@ -69,7 +66,6 @@ func (s *SnapTestSuite) TestAddHWAccessInvalidDevice(c *C) { } func (s *SnapTestSuite) TestAddHWAccessMultiplePaths(c *C) { - aaClickHookCmd = "true" makeInstalledMockSnap(s.tempdir, "") err := AddHWAccess("hello-app", "/dev/ttyUSB0") @@ -77,23 +73,19 @@ func (s *SnapTestSuite) TestAddHWAccessMultiplePaths(c *C) { err = AddHWAccess("hello-app", "/sys/devices/gpio1") c.Assert(err, IsNil) - content, err := ioutil.ReadFile(filepath.Join(dirs.SnapAppArmorDir, "hello-app.json.additional")) + content, err := ioutil.ReadFile(filepath.Join(dirs.SnapAppArmorAdditionalDir, "hello-app.hwaccess.yaml")) c.Assert(err, IsNil) - c.Assert(string(content), Equals, `{ - "write_path": [ - "/dev/ttyUSB0", - "/sys/devices/gpio1" - ], - "read_path": [ - "/run/udev/data/*" - ] -} + c.Assert("\n"+string(content), Equals, ` +read-paths: +- /run/udev/data/* +write-paths: +- /dev/ttyUSB0 +- /sys/devices/gpio1 `) } func (s *SnapTestSuite) TestAddHWAccessAddSameDeviceTwice(c *C) { - aaClickHookCmd = "true" makeInstalledMockSnap(s.tempdir, "") err := AddHWAccess("hello-app", "/dev/ttyUSB0") @@ -114,12 +106,12 @@ func (s *SnapTestSuite) TestAddHWAccessUnknownPackage(c *C) { c.Assert(*regenerateAppArmorRulesWasCalled, Equals, false) } -func (s *SnapTestSuite) TestAddHWAccessHookFails(c *C) { - aaClickHookCmd = "false" - makeInstalledMockSnap(s.tempdir, "") +func (s *SnapTestSuite) TestAddHWAccessIllegalPackage(c *C) { + regenerateAppArmorRulesWasCalled := mockRegenerateAppArmorRules() - err := AddHWAccess("hello-app", "/dev/ttyUSB0") - c.Assert(err.Error(), Equals, "apparmor generate fails with 1: ''") + err := AddHWAccess("hello_svc1", "/dev/ttyUSB0") + c.Assert(err, Equals, ErrPackageNotFound) + c.Assert(*regenerateAppArmorRulesWasCalled, Equals, false) } func (s *SnapTestSuite) TestListHWAccessNoAdditionalAccess(c *C) { @@ -154,8 +146,6 @@ func (s *SnapTestSuite) TestRemoveHWAccessInvalidDevice(c *C) { } func (s *SnapTestSuite) TestRemoveHWAccess(c *C) { - aaClickHookCmd = "true" - makeInstalledMockSnap(s.tempdir, "") err := AddHWAccess("hello-app", "/dev/ttyUSB0") @@ -180,13 +170,12 @@ func (s *SnapTestSuite) TestRemoveHWAccess(c *C) { c.Assert(helpers.FileExists(filepath.Join(dirs.SnapUdevRulesDir, udevRulesFilename)), Equals, false) // check the json.additional got cleaned out - content, err := ioutil.ReadFile(filepath.Join(dirs.SnapAppArmorDir, "hello-app.json.additional")) + content, err := ioutil.ReadFile(filepath.Join(dirs.SnapAppArmorAdditionalDir, "hello-app.hwaccess.yaml")) c.Assert(err, IsNil) c.Assert(string(content), Equals, "{}\n") } func (s *SnapTestSuite) TestRemoveHWAccessMultipleDevices(c *C) { - aaClickHookCmd = "true" makeInstalledMockSnap(s.tempdir, "") // setup @@ -197,17 +186,14 @@ func (s *SnapTestSuite) TestRemoveHWAccessMultipleDevices(c *C) { c.Assert(writePaths, DeepEquals, []string{"/dev/bar", "/dev/bar*"}) // check the file only lists udevReadGlob once - content, err := ioutil.ReadFile(filepath.Join(dirs.SnapAppArmorDir, "hello-app.json.additional")) + content, err := ioutil.ReadFile(filepath.Join(dirs.SnapAppArmorAdditionalDir, "hello-app.hwaccess.yaml")) c.Assert(err, IsNil) - c.Assert(string(content), Equals, `{ - "write_path": [ - "/dev/bar", - "/dev/bar*" - ], - "read_path": [ - "/run/udev/data/*" - ] -} + c.Assert("\n"+string(content), Equals, ` +read-paths: +- /run/udev/data/* +write-paths: +- /dev/bar +- /dev/bar* `) // check the udev rule file contains all the rules @@ -227,16 +213,13 @@ KERNEL=="bar*", TAG:="snappy-assign", ENV{SNAPPY_APP}:="hello-app" c.Assert(writePaths, DeepEquals, []string{"/dev/bar*"}) // check udevReadGlob is still there - content, err = ioutil.ReadFile(filepath.Join(dirs.SnapAppArmorDir, "hello-app.json.additional")) + content, err = ioutil.ReadFile(filepath.Join(dirs.SnapAppArmorAdditionalDir, "hello-app.hwaccess.yaml")) c.Assert(err, IsNil) - c.Assert(string(content), Equals, `{ - "write_path": [ - "/dev/bar*" - ], - "read_path": [ - "/run/udev/data/*" - ] -} + c.Assert("\n"+string(content), Equals, ` +read-paths: +- /run/udev/data/* +write-paths: +- /dev/bar* `) // check the udevReadGlob Udev rule is still there content, err = ioutil.ReadFile(filepath.Join(dirs.SnapUdevRulesDir, "70-snappy_hwassign_hello-app.rules")) @@ -301,22 +284,6 @@ func (s *SnapTestSuite) TestRemoveAllHWAccess(c *C) { c.Check(RemoveAllHWAccess("hello-app"), IsNil) c.Check(helpers.FileExists(filepath.Join(dirs.SnapUdevRulesDir, "70-snappy_hwassign_foo-app.rules")), Equals, false) - c.Check(helpers.FileExists(filepath.Join(dirs.SnapAppArmorDir, "hello-app.json.additional")), Equals, false) + c.Check(helpers.FileExists(filepath.Join(dirs.SnapAppArmorAdditionalDir, "hello-app.hwaccess.yaml")), Equals, false) c.Check(*regenerateAppArmorRulesWasCalled, Equals, true) } - -func (s *SnapTestSuite) TestRegenerateAppaArmorRulesErr(c *C) { - script := `#!/bin/sh -echo meep -exit 1` - mockFailHookFile := filepath.Join(c.MkDir(), "failing-aa-hook") - err := ioutil.WriteFile(mockFailHookFile, []byte(script), 0755) - c.Assert(err, IsNil) - aaClickHookCmd = mockFailHookFile - - err = regenerateAppArmorRulesImpl() - c.Assert(err, DeepEquals, &ErrApparmorGenerate{ - ExitCode: 1, - Output: []byte("meep\n"), - }) -} diff --git a/snappy/install_test.go b/snappy/install_test.go index 83c741c579..e128230478 100644 --- a/snappy/install_test.go +++ b/snappy/install_test.go @@ -51,6 +51,35 @@ func (s *SnapTestSuite) TestInstallInstall(c *C) { c.Check(name, Equals, "foo") } +func (s *SnapTestSuite) TestInstallInstallLicense(c *C) { + snapFile := makeTestSnapPackage(c, ` +name: foo +version: 1.0 +icon: foo.svg +vendor: Foo Bar <foo@example.com> +explicit-license-agreement: Y +`) + ag := &MockProgressMeter{y: true} + name, err := Install(snapFile, AllowUnauthenticated|DoInstallGC, ag) + c.Assert(err, IsNil) + c.Check(name, Equals, "foo") + c.Check(ag.license, Equals, "WTFPL") +} + +func (s *SnapTestSuite) TestInstallInstallLicenseNo(c *C) { + snapFile := makeTestSnapPackage(c, ` +name: foo +version: 1.0 +icon: foo.svg +vendor: Foo Bar <foo@example.com> +explicit-license-agreement: Y +`) + ag := &MockProgressMeter{y: false} + _, err := Install(snapFile, AllowUnauthenticated|DoInstallGC, ag) + c.Assert(IsLicenseNotAccepted(err), Equals, true) + c.Check(ag.license, Equals, "WTFPL") +} + func (s *SnapTestSuite) installThree(c *C, flags InstallFlags) { dirs.SnapDataHomeGlob = filepath.Join(s.tempdir, "home", "*", "apps") homeDir := filepath.Join(s.tempdir, "home", "user1", "apps") diff --git a/snappy/pkgformat.go b/snappy/pkgformat.go index 24ad81f3a2..47f8552525 100644 --- a/snappy/pkgformat.go +++ b/snappy/pkgformat.go @@ -26,7 +26,7 @@ import ( "strings" "github.com/ubuntu-core/snappy/pkg/clickdeb" - "github.com/ubuntu-core/snappy/pkg/snapfs" + "github.com/ubuntu-core/snappy/pkg/squashfs" ) // PackageFile is the interface to interact with the low-level snap files @@ -54,7 +54,7 @@ func OpenPackageFile(path string) (PackageFile, error) { } // note that we only support little endian squashfs for now if bytes.HasPrefix(header, []byte{'h', 's', 'q', 's'}) { - return snapfs.New(path), nil + return squashfs.New(path), nil } if strings.HasPrefix(string(header), "!<arch>\ndebian") { return clickdeb.Open(path) diff --git a/snappy/purge_test.go b/snappy/purge_test.go index fb63e7917e..7df0fe7d88 100644 --- a/snappy/purge_test.go +++ b/snappy/purge_test.go @@ -48,7 +48,11 @@ func (s *purgeSuite) SetUpTest(c *C) { } dirs.SnapSeccompDir = c.MkDir() - runScFilterGen = mockRunScFilterGen + dirs.SnapAppArmorDir = c.MkDir() + + runAppArmorParser = mockRunAppArmorParser + + makeMockSecurityEnv(c) } func (s *purgeSuite) TestPurgeNonExistingRaisesError(c *C) { diff --git a/snappy/security.go b/snappy/security.go index eb819ffc55..55a00f2d8e 100644 --- a/snappy/security.go +++ b/snappy/security.go @@ -20,95 +20,303 @@ package snappy import ( - "encoding/json" + "bufio" + "bytes" + "errors" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" + "regexp" "strings" - "gopkg.in/yaml.v2" - "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/helpers" "github.com/ubuntu-core/snappy/logger" "github.com/ubuntu-core/snappy/pkg" + "github.com/ubuntu-core/snappy/policy" + "github.com/ubuntu-core/snappy/release" ) -type apparmorJSONTemplate struct { - Template string `json:"template"` - PolicyGroups []string `json:"policy_groups"` - PolicyVendor string `json:"policy_vendor"` - PolicyVersion float64 `json:"policy_version"` +type errPolicyNotFound struct { + // type of policy, e.g. template or cap + PolType string + // apparmor or seccomp + PolKind *securityPolicyType + // name of the policy + PolName string } -type securitySeccompOverride struct { - Template string `yaml:"security-template,omitempty"` - PolicyGroups []string `yaml:"caps,omitempty"` - Syscalls []string `yaml:"syscalls,omitempty"` - PolicyVendor string `yaml:"policy-vendor"` - PolicyVersion float64 `yaml:"policy-version"` +func (e *errPolicyNotFound) Error() string { + return fmt.Sprintf("could not find specified %s: %s (%s)", e.PolType, e.PolName, e.PolKind) } -const defaultTemplate = "default" +var ( + // Note: these are true for ubuntu-core but perhaps not other flavors + defaultTemplateName = "default" + defaultPolicyGroups = []string{"network-client"} -var defaultPolicyGroups = []string{"network-client"} + // AppArmor cache dir + aaCacheDir = "/var/cache/apparmor" -// TODO: autodetect, this won't work for personal -const defaultPolicyVendor = "ubuntu-core" -const defaultPolicyVersion = 15.04 + // ErrSystemVersionNotFound could not detect system version (eg, 15.04, + // 15.10, etc) + errSystemVersionNotFound = errors.New("could not detect system version") -func (s *SecurityDefinitions) generateApparmorJSONContent() ([]byte, error) { - t := apparmorJSONTemplate{ - Template: s.SecurityTemplate, - PolicyGroups: s.SecurityCaps, - PolicyVendor: defaultPolicyVendor, - PolicyVersion: defaultPolicyVersion, - } + errOriginNotFound = errors.New("could not detect origin") + errPolicyTypeNotFound = errors.New("could not find specified policy type") + errInvalidAppID = errors.New("invalid APP_ID") + errPolicyGen = errors.New("errors found when generating policy") - // FIXME: this is snappy specific, on other systems like the - // phone we may want different defaults. - if t.Template == "" && t.PolicyGroups == nil { - t.PolicyGroups = defaultPolicyGroups + // snappyConfig is the default securityDefinition for a snappy + // config fragment + snappyConfig = &SecurityDefinitions{ + SecurityCaps: []string{}, } - // never write a null value out into the json - if t.PolicyGroups == nil { - t.PolicyGroups = []string{} + lsbRelease = "/etc/lsb-release" + runAppArmorParser = runAppArmorParserImpl +) + +func runAppArmorParserImpl(argv ...string) ([]byte, error) { + cmd := exec.Command(argv[0], argv[1:]...) + return cmd.CombinedOutput() +} + +// SecurityOverrideDefinition is used to override apparmor or seccomp +// security defaults +type SecurityOverrideDefinition struct { + ReadPaths []string `yaml:"read-paths,omitempty" json:"read-paths,omitempty"` + WritePaths []string `yaml:"write-paths,omitempty" json:"write-paths,omitempty"` + Abstractions []string `yaml:"abstractions,omitempty" json:"abstractions,omitempty"` + Syscalls []string `yaml:"syscalls,omitempty" json:"syscalls,omitempty"` + + // deprecated keys, we warn when we see those + DeprecatedAppArmor interface{} `yaml:"apparmor,omitempty" json:"apparmor,omitempty"` + DeprecatedSeccomp interface{} `yaml:"seccomp,omitempty" json:"seccomp,omitempty"` +} + +// SecurityPolicyDefinition is used to provide hand-crafted policy +type SecurityPolicyDefinition struct { + AppArmor string `yaml:"apparmor" json:"apparmor"` + Seccomp string `yaml:"seccomp" json:"seccomp"` +} + +// SecurityDefinitions contains the common apparmor/seccomp definitions +type SecurityDefinitions struct { + // SecurityTemplate is a template name like "default" + SecurityTemplate string `yaml:"security-template,omitempty" json:"security-template,omitempty"` + // SecurityOverride is a override for the high level security json + SecurityOverride *SecurityOverrideDefinition `yaml:"security-override,omitempty" json:"security-override,omitempty"` + // SecurityPolicy is a hand-crafted low-level policy + SecurityPolicy *SecurityPolicyDefinition `yaml:"security-policy,omitempty" json:"security-policy,omitempty"` + + // SecurityCaps is are the apparmor/seccomp capabilities for an app + SecurityCaps []string `yaml:"caps,omitempty" json:"caps,omitempty"` +} + +// securityPolicyType is a kind of securityPolicy, we currently +// have "apparmor" and "seccomp" +type securityPolicyType struct { + name string + basePolicyDir string +} + +var securityPolicyTypeAppArmor = securityPolicyType{ + name: "apparmor", + basePolicyDir: "/usr/share/apparmor/easyprof", +} + +var securityPolicyTypeSeccomp = securityPolicyType{ + name: "seccomp", + basePolicyDir: "/usr/share/seccomp", +} + +func (sp *securityPolicyType) policyDir() string { + return filepath.Join(dirs.GlobalRootDir, sp.basePolicyDir) +} + +func (sp *securityPolicyType) frameworkPolicyDir() string { + frameworkPolicyDir := filepath.Join(policy.SecBase, sp.name) + return filepath.Join(dirs.GlobalRootDir, frameworkPolicyDir) +} + +// findTemplate returns the security template content from the template name. +func (sp *securityPolicyType) findTemplate(templateName string) (string, error) { + if templateName == "" { + templateName = defaultTemplateName } - if t.Template == "" { - t.Template = defaultTemplate + subdir := filepath.Join("templates", defaultPolicyVendor(), defaultPolicyVersion()) + systemTemplateDir := filepath.Join(sp.policyDir(), subdir, templateName) + fwTemplateDir := filepath.Join(sp.frameworkPolicyDir(), "templates", templateName) + + // Read system and framwork policy, but always prefer system policy + fns := []string{systemTemplateDir, fwTemplateDir} + for _, fn := range fns { + content, err := ioutil.ReadFile(fn) + // it is ok if the file does not exists + if os.IsNotExist(err) { + continue + } + // but any other error is a failure + if err != nil { + return "", err + } + + return string(content), nil } - outStr, err := json.MarshalIndent(t, "", " ") + return "", &errPolicyNotFound{"template", sp, templateName} +} + +// helper for findSingleCap that implements readlines(). +func readSingleCapFile(fn string) ([]string, error) { + p := []string{} + + r, err := os.Open(fn) if err != nil { return nil, err } + defer r.Close() + + s := bufio.NewScanner(r) + for s.Scan() { + p = append(p, s.Text()) + } + if err := s.Err(); err != nil { + return nil, err + } - return outStr, nil + return p, nil } -func handleApparmor(buildDir string, m *packageYaml, hookName string, s *SecurityDefinitions) error { - hasSecPol := s.SecurityPolicy != nil && s.SecurityPolicy.Apparmor != "" - hasSecOvr := s.SecurityOverride != nil && s.SecurityOverride.Apparmor != "" +// findSingleCap returns the security template content for a single +// security-cap. +func (sp *securityPolicyType) findSingleCap(capName, systemPolicyDir, fwPolicyDir string) ([]string, error) { + found := false + p := []string{} + + policyDirs := []string{systemPolicyDir, fwPolicyDir} + for _, dir := range policyDirs { + fn := filepath.Join(dir, capName) + newCaps, err := readSingleCapFile(fn) + // its ok if the file does not exist + if os.IsNotExist(err) { + continue + } + // but any other error is not ok + if err != nil { + return nil, err + } + p = append(p, newCaps...) + found = true + break + } - if hasSecPol || hasSecOvr { - return nil + if found == false { + return nil, &errPolicyNotFound{"cap", sp, capName} + } + + return p, nil +} + +// findCaps returns the security template content for the given list +// of security-caps. +func (sp *securityPolicyType) findCaps(caps []string, templateName string) ([]string, error) { + // XXX: this is snappy specific, on other systems like the phone we may + // want different defaults. + if templateName == "" && caps == nil { + caps = defaultPolicyGroups + } + + // Nothing to find if caps is empty + if len(caps) == 0 { + return nil, nil + } + + subdir := filepath.Join("policygroups", defaultPolicyVendor(), defaultPolicyVersion()) + parentDir := filepath.Join(sp.policyDir(), subdir) + fwParentDir := filepath.Join(sp.frameworkPolicyDir(), "policygroups") + + var p []string + for _, c := range caps { + newCap, err := sp.findSingleCap(c, parentDir, fwParentDir) + if err != nil { + return nil, err + } + p = append(p, newCap...) } - // generate apparmor template - apparmorJSONFile := m.Integration[hookName]["apparmor"] - securityJSONContent, err := s.generateApparmorJSONContent() + return p, nil +} + +func defaultPolicyVendor() string { + // FIXME: slightly ugly that we have to give a prefix here + return fmt.Sprintf("ubuntu-%s", release.Get().Flavor) +} + +// findUbuntuVersion determines the version (eg, 15.04, 15.10, etc) of the +// system, which is needed for determining the security policy +// policy-version +func findUbuntuVersion() (string, error) { + content, err := ioutil.ReadFile(lsbRelease) if err != nil { - return err + logger.Noticef("Failed to read %q: %v", lsbRelease, err) + return "", err } - if err := helpers.AtomicWriteFile(filepath.Join(buildDir, apparmorJSONFile), securityJSONContent, 0644, 0); err != nil { - return err + + for _, line := range strings.Split(string(content), "\n") { + if strings.HasPrefix(line, "DISTRIB_RELEASE=") { + tmp := strings.Split(line, "=") + if len(tmp) != 2 { + return "", errSystemVersionNotFound + } + return tmp[1], nil + } } - return nil + return "", errSystemVersionNotFound +} +func defaultPolicyVersion() string { + // note that we can not use release.Get().Series here + // because that will return "rolling" for the development + // version but apparmor stores its templates under the + // version number (e.g. 16.04) instead + ver, err := findUbuntuVersion() + if err != nil { + // when this happens we are in trouble + panic(err) + } + return ver +} + +const allowed = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789` + +// Generate a string suitable for use in a DBus object +func dbusPath(s string) string { + buf := bytes.NewBuffer(make([]byte, 0, len(s))) + + for _, c := range []byte(s) { + if strings.IndexByte(allowed, c) >= 0 { + fmt.Fprintf(buf, "%c", c) + } else { + fmt.Fprintf(buf, "_%02x", c) + } + } + + return buf.String() +} + +// Calculate whitespace prefix based on occurrence of s in t +func findWhitespacePrefix(t string, s string) string { + subs := regexp.MustCompile(`(?m)^( *)` + regexp.QuoteMeta(s)).FindStringSubmatch(t) + if subs == nil { + return "" + } + + return subs[1] } func getSecurityProfile(m *packageYaml, appName, baseDir string) (string, error) { @@ -122,106 +330,619 @@ func getSecurityProfile(m *packageYaml, appName, baseDir string) (string, error) return fmt.Sprintf("%s.%s_%s_%s", m.Name, origin, cleanedName, m.Version), err } -var runScFilterGen = runScFilterGenImpl +type securityAppID struct { + AppID string + Pkgname string + Appname string + Version string +} -func runScFilterGenImpl(argv ...string) ([]byte, error) { - cmd := exec.Command(argv[0], argv[1:]...) - return cmd.Output() +func newAppID(appID string) (*securityAppID, error) { + tmp := strings.Split(appID, "_") + if len(tmp) != 3 { + return nil, errInvalidAppID + } + id := securityAppID{ + AppID: appID, + Pkgname: tmp[0], + Appname: tmp[1], + Version: tmp[2], + } + return &id, nil } -// seccomp specific -func generateSeccompPolicy(baseDir, appName string, sd SecurityDefinitions) ([]byte, error) { - if sd.SecurityPolicy != nil && sd.SecurityPolicy.Seccomp != "" { - fn := filepath.Join(baseDir, sd.SecurityPolicy.Seccomp) - content, err := ioutil.ReadFile(fn) - if err != nil { - logger.Noticef("Failed to read %q: %v", fn, err) +// TODO: once verified, reorganize all these +func (sa *securityAppID) appArmorVars() string { + aavars := fmt.Sprintf(` +# Specified profile variables +@{APP_APPNAME}="%s" +@{APP_ID_DBUS}="%s" +@{APP_PKGNAME_DBUS}="%s" +@{APP_PKGNAME}="%s" +@{APP_VERSION}="%s" +@{INSTALL_DIR}="{/apps,/oem}" +# Deprecated: +@{CLICK_DIR}="{/apps,/oem}"`, sa.Appname, dbusPath(sa.AppID), dbusPath(sa.Pkgname), sa.Pkgname, sa.Version) + return aavars +} + +func genAppArmorPathRule(path string, access string) (string, error) { + if !strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "@{") { + logger.Noticef("Bad path: %s", path) + return "", errPolicyGen + } + + owner := "" + if strings.HasPrefix(path, "/home") || strings.HasPrefix(path, "@{HOME") { + owner = "owner " + } + + rules := "" + if strings.HasSuffix(path, "/") { + rules += fmt.Sprintf("%s %s,\n", path, access) + rules += fmt.Sprintf("%s%s** %s,\n", owner, path, access) + } else if strings.HasSuffix(path, "/**") || strings.HasSuffix(path, "/*") { + rules += fmt.Sprintf("%s/ %s,\n", filepath.Dir(path), access) + rules += fmt.Sprintf("%s%s %s,\n", owner, path, access) + } else { + rules += fmt.Sprintf("%s%s %s,\n", owner, path, access) + } + + return rules, nil +} + +func mergeAppArmorTemplateAdditionalContent(appArmorTemplate, aaPolicy string, overrides *SecurityOverrideDefinition) (string, error) { + // ensure we have + if overrides == nil { + overrides = &SecurityOverrideDefinition{} + } + + if overrides.ReadPaths == nil { + aaPolicy = strings.Replace(aaPolicy, "###READS###\n", "# No read paths specified\n", 1) + } else { + s := "# Additional read-paths from security-override\n" + prefix := findWhitespacePrefix(appArmorTemplate, "###READS###") + for _, readpath := range overrides.ReadPaths { + rules, err := genAppArmorPathRule(strings.Trim(readpath, " "), "rk") + if err != nil { + return "", err + } + lines := strings.Split(rules, "\n") + for _, rule := range lines { + s += fmt.Sprintf("%s%s\n", prefix, rule) + } + } + aaPolicy = strings.Replace(aaPolicy, "###READS###\n", s, 1) + } + + if overrides.WritePaths == nil { + aaPolicy = strings.Replace(aaPolicy, "###WRITES###\n", "# No write paths specified\n", 1) + } else { + s := "# Additional write-paths from security-override\n" + prefix := findWhitespacePrefix(appArmorTemplate, "###WRITES###") + for _, writepath := range overrides.WritePaths { + rules, err := genAppArmorPathRule(strings.Trim(writepath, " "), "rwk") + if err != nil { + return "", err + } + lines := strings.Split(rules, "\n") + for _, rule := range lines { + s += fmt.Sprintf("%s%s\n", prefix, rule) + } + } + aaPolicy = strings.Replace(aaPolicy, "###WRITES###\n", s, 1) + } + + if overrides.Abstractions == nil { + aaPolicy = strings.Replace(aaPolicy, "###ABSTRACTIONS###\n", "# No abstractions specified\n", 1) + } else { + s := "# Additional abstractions from security-override\n" + prefix := findWhitespacePrefix(appArmorTemplate, "###ABSTRACTIONS###") + for _, abs := range overrides.Abstractions { + s += fmt.Sprintf("%s#include <abstractions/%s>\n", prefix, abs) + } + aaPolicy = strings.Replace(aaPolicy, "###ABSTRACTIONS###\n", s, 1) + } + + return aaPolicy, nil +} + +func getAppArmorTemplatedPolicy(m *packageYaml, appID *securityAppID, template string, caps []string, overrides *SecurityOverrideDefinition) (string, error) { + t, err := securityPolicyTypeAppArmor.findTemplate(template) + if err != nil { + return "", err + } + p, err := securityPolicyTypeAppArmor.findCaps(caps, template) + if err != nil { + return "", err + } + + aaPolicy := strings.Replace(t, "\n###VAR###\n", appID.appArmorVars()+"\n", 1) + aaPolicy = strings.Replace(aaPolicy, "\n###PROFILEATTACH###", fmt.Sprintf("\nprofile \"%s\"", appID.AppID), 1) + + aacaps := "" + if len(p) == 0 { + aacaps += "# No caps (policy groups) specified\n" + } else { + aacaps += "# Rules specified via caps (policy groups)\n" + prefix := findWhitespacePrefix(t, "###POLICYGROUPS###") + for _, line := range p { + if len(line) == 0 { + aacaps += "\n" + } else { + aacaps += fmt.Sprintf("%s%s\n", prefix, line) + } + } + } + aaPolicy = strings.Replace(aaPolicy, "###POLICYGROUPS###\n", aacaps, 1) + + return mergeAppArmorTemplateAdditionalContent(t, aaPolicy, overrides) +} + +func getSeccompTemplatedPolicy(m *packageYaml, appID *securityAppID, templateName string, caps []string, overrides *SecurityOverrideDefinition) (string, error) { + t, err := securityPolicyTypeSeccomp.findTemplate(templateName) + if err != nil { + return "", err + } + p, err := securityPolicyTypeSeccomp.findCaps(caps, templateName) + if err != nil { + return "", err + } + + scPolicy := t + "\n" + strings.Join(p, "\n") + + if overrides != nil && overrides.Syscalls != nil { + scPolicy += "\n# Addtional syscalls from security-override\n" + for _, syscall := range overrides.Syscalls { + scPolicy += fmt.Sprintf("%s\n", syscall) + } + } + + scPolicy = strings.Replace(scPolicy, "\ndeny ", "\n# EXPLICITLY DENIED: ", -1) + + return scPolicy, nil +} + +var finalCurtain = regexp.MustCompile(`}\s*$`) + +func getAppArmorCustomPolicy(m *packageYaml, appID *securityAppID, fn string, overrides *SecurityOverrideDefinition) (string, error) { + custom, err := ioutil.ReadFile(fn) + if err != nil { + return "", err + } + + aaPolicy := strings.Replace(string(custom), "\n###VAR###\n", appID.appArmorVars()+"\n", 1) + aaPolicy = strings.Replace(aaPolicy, "\n###PROFILEATTACH###", fmt.Sprintf("\nprofile \"%s\"", appID.AppID), 1) + + // a custom policy may not have the overrides defined that we + // use for the hw-assign work. so we insert them here + aaPolicy = finalCurtain.ReplaceAllString(aaPolicy, ` +###READS### +###WRITES### +###ABSTRACTIONS### +} +`) + + return mergeAppArmorTemplateAdditionalContent("", aaPolicy, overrides) +} + +func getSeccompCustomPolicy(m *packageYaml, appID *securityAppID, fn string) (string, error) { + custom, err := ioutil.ReadFile(fn) + if err != nil { + return "", err + } + + return string(custom), nil +} + +var loadAppArmorPolicy = func(fn string) ([]byte, error) { + args := []string{ + "/sbin/apparmor_parser", + "-r", + "--write-cache", + "-L", aaCacheDir, + fn, + } + content, err := runAppArmorParser(args...) + if err != nil { + logger.Noticef("%v failed", args) + } + return content, err +} + +func (m *packageYaml) removeOneSecurityPolicy(name, baseDir string) error { + profileName, err := getSecurityProfile(m, filepath.Base(name), baseDir) + if err != nil { + return err + } + + // seccomp profile + fn := filepath.Join(dirs.SnapSeccompDir, profileName) + if err := os.Remove(fn); err != nil && !os.IsNotExist(err) { + return err + } + + // apparmor cache + fn = filepath.Join(aaCacheDir, profileName) + if err := os.Remove(fn); err != nil && !os.IsNotExist(err) { + return err + } + + // apparmor profile + fn = filepath.Join(dirs.SnapAppArmorDir, profileName) + if err := os.Remove(fn); err != nil && !os.IsNotExist(err) { + return err + } + + return nil +} + +func removePolicy(m *packageYaml, baseDir string) error { + for _, service := range m.ServiceYamls { + if err := m.removeOneSecurityPolicy(service.Name, baseDir); err != nil { + return err } - return content, err } - os.MkdirAll(dirs.SnapSeccompDir, 0755) + for _, binary := range m.Binaries { + if err := m.removeOneSecurityPolicy(binary.Name, baseDir); err != nil { + return err + } + } + + if err := m.removeOneSecurityPolicy("snappy-config", baseDir); err != nil { + return err + } + + return nil +} + +func (sd *SecurityDefinitions) mergeAppArmorSecurityOverrides(new *SecurityOverrideDefinition) { + // nothing to do + if new == nil { + return + } - // defaults - policyVendor := defaultPolicyVendor - policyVersion := defaultPolicyVersion - template := defaultTemplate - caps := []string{} - for _, p := range defaultPolicyGroups { - caps = append(caps, p) + // ensure we have valid structs to work with + if sd.SecurityOverride == nil { + sd.SecurityOverride = &SecurityOverrideDefinition{} } - syscalls := []string{} - if sd.SecurityOverride != nil { - if sd.SecurityOverride.Seccomp == "" { - logger.Noticef("No seccomp policy found") - return nil, ErrNoSeccompPolicy + sd.SecurityOverride.ReadPaths = append(sd.SecurityOverride.ReadPaths, new.ReadPaths...) + sd.SecurityOverride.WritePaths = append(sd.SecurityOverride.WritePaths, new.WritePaths...) + sd.SecurityOverride.Abstractions = append(sd.SecurityOverride.Abstractions, new.Abstractions...) +} + +type securityPolicyResult struct { + id *securityAppID + + aaPolicy string + aaFn string + + scPolicy string + scFn string +} + +func (sd *SecurityDefinitions) warnDeprecatedKeys() { + if sd.SecurityOverride != nil && sd.SecurityOverride.DeprecatedAppArmor != nil { + logger.Noticef("The security-override.apparmor key is no longer supported, please use use security-override directly") + } + if sd.SecurityOverride != nil && sd.SecurityOverride.DeprecatedSeccomp != nil { + logger.Noticef("The security-override.seccomp key is no longer supported, please use use security-override directly") + } +} + +func (sd *SecurityDefinitions) generatePolicyForServiceBinaryResult(m *packageYaml, name string, baseDir string) (*securityPolicyResult, error) { + res := &securityPolicyResult{} + appID, err := getSecurityProfile(m, name, baseDir) + if err != nil { + logger.Noticef("Failed to obtain security profile for %s: %v", name, err) + return nil, err + } + + res.id, err = newAppID(appID) + if err != nil { + logger.Noticef("Failed to obtain APP_ID for %s: %v", name, err) + return nil, err + } + + // warn about deprecated + sd.warnDeprecatedKeys() + + // add the hw-override parts and merge with the other overrides + origin := "" + if m.Type != pkg.TypeFramework && m.Type != pkg.TypeOem { + origin, err = originFromYamlPath(filepath.Join(baseDir, "meta", "package.yaml")) + if err != nil { + return nil, err } + } + + hwaccessOverrides, err := readHWAccessYamlFile(m.qualifiedName(origin)) + if err != nil && !os.IsNotExist(err) { + return nil, err + } - fn := filepath.Join(baseDir, sd.SecurityOverride.Seccomp) - var s securitySeccompOverride - err := readSeccompOverride(fn, &s) + sd.mergeAppArmorSecurityOverrides(&hwaccessOverrides) + if sd.SecurityPolicy != nil { + res.aaPolicy, err = getAppArmorCustomPolicy(m, res.id, filepath.Join(baseDir, sd.SecurityPolicy.AppArmor), sd.SecurityOverride) + if err != nil { + logger.Noticef("Failed to generate custom AppArmor policy for %s: %v", name, err) + return nil, err + } + res.scPolicy, err = getSeccompCustomPolicy(m, res.id, filepath.Join(baseDir, sd.SecurityPolicy.Seccomp)) + if err != nil { + logger.Noticef("Failed to generate custom seccomp policy for %s: %v", name, err) + return nil, err + } + } else { + res.aaPolicy, err = getAppArmorTemplatedPolicy(m, res.id, sd.SecurityTemplate, sd.SecurityCaps, sd.SecurityOverride) if err != nil { - logger.Noticef("Failed to read %q: %v", fn, err) + logger.Noticef("Failed to generate AppArmor policy for %s: %v", name, err) return nil, err } - if s.Template != "" { - template = s.Template + res.scPolicy, err = getSeccompTemplatedPolicy(m, res.id, sd.SecurityTemplate, sd.SecurityCaps, sd.SecurityOverride) + if err != nil { + logger.Noticef("Failed to generate seccomp policy for %s: %v", name, err) + return nil, err } - if s.PolicyVendor != "" { - policyVendor = s.PolicyVendor + } + res.scFn = filepath.Join(dirs.SnapSeccompDir, res.id.AppID) + res.aaFn = filepath.Join(dirs.SnapAppArmorDir, res.id.AppID) + + return res, nil +} + +func (sd *SecurityDefinitions) generatePolicyForServiceBinary(m *packageYaml, name string, baseDir string) error { + p, err := sd.generatePolicyForServiceBinaryResult(m, name, baseDir) + if err != nil { + return err + } + + os.MkdirAll(filepath.Dir(p.scFn), 0755) + err = helpers.AtomicWriteFile(p.scFn, []byte(p.scPolicy), 0644, 0) + if err != nil { + logger.Noticef("Failed to write seccomp policy for %s: %v", name, err) + return err + } + + os.MkdirAll(filepath.Dir(p.aaFn), 0755) + err = helpers.AtomicWriteFile(p.aaFn, []byte(p.aaPolicy), 0644, 0) + if err != nil { + logger.Noticef("Failed to write AppArmor policy for %s: %v", name, err) + return err + } + out, err := loadAppArmorPolicy(p.aaFn) + if err != nil { + logger.Noticef("Failed to load AppArmor policy for %s: %v\n:%s", name, err, out) + return err + } + + return nil +} + +// FIXME: move into something more generic - SnapPart.HasConfig? +func hasConfig(baseDir string) bool { + return helpers.FileExists(filepath.Join(baseDir, "meta", "hooks", "config")) +} + +func generatePolicy(m *packageYaml, baseDir string) error { + var foundError error + + // generate default security config for snappy-config + if hasConfig(baseDir) { + if err := snappyConfig.generatePolicyForServiceBinary(m, "snappy-config", baseDir); err != nil { + foundError = err + logger.Noticef("Failed to obtain APP_ID for %s: %v", "snappy-config", err) } - if s.PolicyVersion != 0 { - policyVersion = s.PolicyVersion + } + + for _, service := range m.ServiceYamls { + err := service.generatePolicyForServiceBinary(m, service.Name, baseDir) + if err != nil { + foundError = err + logger.Noticef("Failed to generate policy for service %s: %v", service.Name, err) + continue } - caps = s.PolicyGroups - syscalls = s.Syscalls - } else { - if sd.SecurityTemplate != "" { - template = sd.SecurityTemplate + } + + for _, binary := range m.Binaries { + err := binary.generatePolicyForServiceBinary(m, binary.Name, baseDir) + if err != nil { + foundError = err + logger.Noticef("Failed to generate policy for binary %s: %v", binary.Name, err) + continue + } + } + + // FIXME: if there are multiple errors only the last one + // will be preserved + if foundError != nil { + return foundError + } + + return nil +} + +// regeneratePolicyForSnap is used to regenerate all security policy for a +// given snap +func regeneratePolicyForSnap(snapname string) error { + globExpr := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("%s_*", snapname)) + matches, err := filepath.Glob(globExpr) + if err != nil { + return err + } + if len(matches) == 0 { + // Nothing to regenerate is not an error + return nil + } + + appliedVersion := "" + for _, profile := range matches { + appID, err := newAppID(filepath.Base(profile)) + if err != nil { + return err } - if sd.SecurityCaps != nil { - caps = sd.SecurityCaps + if appID.Version != appliedVersion { + // FIXME: dirs.SnapAppsDir is too simple, gadget + fn := filepath.Join(dirs.SnapAppsDir, appID.Pkgname, appID.Version, "meta", "package.yaml") + if !helpers.FileExists(fn) { + continue + } + err := GeneratePolicyFromFile(fn, true) + if err != nil { + return err + } + appliedVersion = appID.Version } } - // Build up the command line - args := []string{ - "sc-filtergen", - fmt.Sprintf("--include-policy-dir=%s", filepath.Dir(dirs.SnapSeccompDir)), - fmt.Sprintf("--policy-vendor=%s", policyVendor), - fmt.Sprintf("--policy-version=%.2f", policyVersion), - fmt.Sprintf("--template=%s", template), + return nil +} + +// compare if the given policy matches the current system policy +// return an error if not +func comparePolicyToCurrent(p *securityPolicyResult) error { + if err := compareSinglePolicyToCurrent(p.aaFn, p.aaPolicy); err != nil { + return err + } + if err := compareSinglePolicyToCurrent(p.scFn, p.scPolicy); err != nil { + return err } - if len(caps) > 0 { - args = append(args, fmt.Sprintf("--policy-groups=%s", strings.Join(caps, ","))) + + return nil +} + +// helper for comparePolicyToCurrent that takes a single apparmor or seccomp +// policy and compares it to the system version +func compareSinglePolicyToCurrent(oldPolicyFn, newPolicy string) error { + oldPolicy, err := ioutil.ReadFile(oldPolicyFn) + if err != nil { + return err } - if len(syscalls) > 0 { - args = append(args, fmt.Sprintf("--syscalls=%s", strings.Join(syscalls, ","))) + if string(oldPolicy) != newPolicy { + return fmt.Errorf("policy differs %s", oldPolicyFn) } + return nil +} - content, err := runScFilterGen(args...) +// CompareGeneratePolicyFromFile is used to simulate security policy +// generation and returns if the policy would have changed +func CompareGeneratePolicyFromFile(fn string) error { + m, err := parsePackageYamlFileWithVersion(fn) if err != nil { - logger.Noticef("%v failed", args) + return err } - return content, err + baseDir := filepath.Dir(filepath.Dir(fn)) + + for _, service := range m.ServiceYamls { + p, err := service.generatePolicyForServiceBinaryResult(m, service.Name, baseDir) + + // FIXME: use apparmor_profile -p on both AppArmor profiles + + if err != nil { + // FIXME: what to do here? + return err + } + if err := comparePolicyToCurrent(p); err != nil { + return err + } + } + + for _, binary := range m.Binaries { + p, err := binary.generatePolicyForServiceBinaryResult(m, binary.Name, baseDir) + if err != nil { + // FIXME: what to do here? + return err + } + if err := comparePolicyToCurrent(p); err != nil { + return err + } + } + + // now compare the snappy-config profile + if hasConfig(baseDir) { + p, err := snappyConfig.generatePolicyForServiceBinaryResult(m, "snappy-config", baseDir) + if err != nil { + return nil + } + if err := comparePolicyToCurrent(p); err != nil { + return err + } + } + + return nil +} + +// FIXME: refactor so that we don't need this +func parsePackageYamlFileWithVersion(fn string) (*packageYaml, error) { + m, err := parsePackageYamlFile(fn) + + // FIXME: duplicated code from snapp.go:NewSnapPartFromYaml, + // version is overriden by sideloaded versions + m.Version = filepath.Base(filepath.Dir(filepath.Dir(fn))) + + return m, err } -func readSeccompOverride(yamlPath string, s *securitySeccompOverride) error { - yamlData, err := ioutil.ReadFile(yamlPath) +// GeneratePolicyFromFile is used to generate security policy on the system +// from the specified manifest file name +func GeneratePolicyFromFile(fn string, force bool) error { + // FIXME: force not used yet + m, err := parsePackageYamlFileWithVersion(fn) + if err != nil { + return err + } + + if m.Type == "" || m.Type == pkg.TypeApp { + _, err = originFromYamlPath(fn) + if err != nil { + if err == ErrInvalidPart { + err = errOriginNotFound + } + return err + } + } + + // TODO: verify cache files here + + baseDir := filepath.Dir(filepath.Dir(fn)) + err = generatePolicy(m, baseDir) if err != nil { return err } - err = yaml.Unmarshal(yamlData, &s) + return err +} + +// RegenerateAllPolicy will re-generate all policy that needs re-generating +func RegenerateAllPolicy(force bool) error { + installed, err := NewMetaLocalRepository().Installed() if err != nil { - return &ErrInvalidYaml{File: "package.yaml[seccomp override]", Err: err, Yaml: yamlData} + return err } - // These must always be specified together - if (s.PolicyVersion == 0 && s.PolicyVendor != "") || (s.PolicyVersion != 0 && s.PolicyVendor == "") { - return ErrInvalidSeccompPolicy + + for _, p := range installed { + part, ok := p.(*SnapPart) + if !ok { + continue + } + basedir := part.basedir + yFn := filepath.Join(basedir, "meta", "package.yaml") + + // FIXME: use ErrPolicyNeedsRegenerating here to check if + // re-generation is needed + if err := CompareGeneratePolicyFromFile(yFn); err == nil { + continue + } + + // re-generate! + logger.Noticef("re-generating security policy for %s", yFn) + if err := GeneratePolicyFromFile(yFn, force); err != nil { + return err + } } return nil diff --git a/snappy/security_test.go b/snappy/security_test.go index 2295fe6e7a..3b64fbb436 100644 --- a/snappy/security_test.go +++ b/snappy/security_test.go @@ -24,10 +24,13 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" . "gopkg.in/check.v1" "github.com/ubuntu-core/snappy/dirs" + "github.com/ubuntu-core/snappy/helpers" + "github.com/ubuntu-core/snappy/logger" "github.com/ubuntu-core/snappy/pkg" ) @@ -36,6 +39,8 @@ type SecurityTestSuite struct { m *packageYaml scFilterGenCall []string scFilterGenCallReturn []byte + + loadAppArmorPolicyCalled bool } var _ = Suite(&SecurityTestSuite{}) @@ -44,132 +49,75 @@ func (a *SecurityTestSuite) SetUpTest(c *C) { a.buildDir = c.MkDir() os.MkdirAll(filepath.Join(a.buildDir, "meta"), 0755) + // set global sandbox + dirs.SetRootDir(c.MkDir()) + a.m = &packageYaml{ Name: "foo", Version: "1.0", Integration: make(map[string]clickAppHook), } - a.scFilterGenCall = nil - a.scFilterGenCallReturn = nil - runScFilterGen = func(argv ...string) ([]byte, error) { - a.scFilterGenCall = append(a.scFilterGenCall, argv...) - return a.scFilterGenCallReturn, nil + // and mock some stuff + a.loadAppArmorPolicyCalled = false + loadAppArmorPolicy = func(fn string) ([]byte, error) { + a.loadAppArmorPolicyCalled = true + return nil, nil + } + runUdevAdm = func(args ...string) error { + return nil } } -func (a *SecurityTestSuite) verifyApparmorFile(c *C, expected string) { - - // ensure the integraton hook is setup correctly for click-apparmor - c.Assert(a.m.Integration["app"]["apparmor"], Equals, "meta/app.apparmor") +func (a *SecurityTestSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") +} - apparmorJSONFile := a.m.Integration["app"]["apparmor"] - content, err := ioutil.ReadFile(filepath.Join(a.buildDir, apparmorJSONFile)) +func ensureFileContentMatches(c *C, fn, expectedContent string) { + content, err := ioutil.ReadFile(fn) c.Assert(err, IsNil) - c.Assert(string(content), Equals, expected) + c.Assert(string(content), Equals, expectedContent) } -func (a *SecurityTestSuite) TestSnappyNoSeccompOverrideEntry(c *C) { - sd := SecurityDefinitions{SecurityOverride: &SecurityOverrideDefinition{}} - - _, err := generateSeccompPolicy(c.MkDir(), "appName", sd) - c.Assert(err, Equals, ErrNoSeccompPolicy) +func makeMockSecurityEnv(c *C) { + makeMockApparmorTemplate(c, "default", []byte("")) + makeMockSeccompTemplate(c, "default", []byte("")) + makeMockApparmorCap(c, "network-client", []byte(``)) + makeMockSeccompCap(c, "network-client", []byte(``)) } -// no special security settings generate the default -func (a *SecurityTestSuite) TestSnappyHandleApparmorSecurityDefault(c *C) { - sec := &SecurityDefinitions{} - - a.m.Binaries = append(a.m.Binaries, Binary{Name: "app", SecurityDefinitions: *sec}) - a.m.legacyIntegration(false) - - err := handleApparmor(a.buildDir, a.m, "app", sec) +func makeMockApparmorTemplate(c *C, templateName string, content []byte) { + mockTemplate := filepath.Join(securityPolicyTypeAppArmor.policyDir(), "templates", defaultPolicyVendor(), defaultPolicyVersion(), templateName) + err := os.MkdirAll(filepath.Dir(mockTemplate), 0755) c.Assert(err, IsNil) - - // verify file content - a.verifyApparmorFile(c, `{ - "template": "default", - "policy_groups": [ - "network-client" - ], - "policy_vendor": "ubuntu-core", - "policy_version": 15.04 -}`) -} - -func (a *SecurityTestSuite) TestSnappyHandleApparmorCaps(c *C) { - sec := &SecurityDefinitions{ - SecurityCaps: []string{"cap1", "cap2"}, - } - - a.m.Binaries = append(a.m.Binaries, Binary{Name: "app", SecurityDefinitions: *sec}) - a.m.legacyIntegration(false) - - err := handleApparmor(a.buildDir, a.m, "app", sec) + err = ioutil.WriteFile(mockTemplate, content, 0644) c.Assert(err, IsNil) - - // verify file content - a.verifyApparmorFile(c, `{ - "template": "default", - "policy_groups": [ - "cap1", - "cap2" - ], - "policy_vendor": "ubuntu-core", - "policy_version": 15.04 -}`) } -func (a *SecurityTestSuite) TestSnappyHandleApparmorTemplate(c *C) { - sec := &SecurityDefinitions{ - SecurityTemplate: "docker-client", - } - - a.m.Binaries = append(a.m.Binaries, Binary{Name: "app", SecurityDefinitions: *sec}) - a.m.legacyIntegration(false) - - err := handleApparmor(a.buildDir, a.m, "app", sec) +func makeMockApparmorCap(c *C, capname string, content []byte) { + mockPG := filepath.Join(securityPolicyTypeAppArmor.policyDir(), "policygroups", defaultPolicyVendor(), defaultPolicyVersion(), capname) + err := os.MkdirAll(filepath.Dir(mockPG), 0755) c.Assert(err, IsNil) - // verify file content - a.verifyApparmorFile(c, `{ - "template": "docker-client", - "policy_groups": [], - "policy_vendor": "ubuntu-core", - "policy_version": 15.04 -}`) + err = ioutil.WriteFile(mockPG, []byte(content), 0644) + c.Assert(err, IsNil) } -func (a *SecurityTestSuite) TestSnappyHandleApparmorOverride(c *C) { - sec := &SecurityDefinitions{ - SecurityOverride: &SecurityOverrideDefinition{ - Apparmor: "meta/custom.json", - }, - } - - a.m.Binaries = append(a.m.Binaries, Binary{Name: "app", SecurityDefinitions: *sec}) - a.m.legacyIntegration(false) - - err := handleApparmor(a.buildDir, a.m, "app", sec) +func makeMockSeccompTemplate(c *C, templateName string, content []byte) { + mockTemplate := filepath.Join(securityPolicyTypeSeccomp.policyDir(), "templates", defaultPolicyVendor(), defaultPolicyVersion(), templateName) + err := os.MkdirAll(filepath.Dir(mockTemplate), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(mockTemplate, content, 0644) c.Assert(err, IsNil) - - c.Assert(a.m.Integration["app"]["apparmor"], Equals, "meta/custom.json") } -func (a *SecurityTestSuite) TestSnappyHandleApparmorPolicy(c *C) { - sec := &SecurityDefinitions{ - SecurityPolicy: &SecurityPolicyDefinition{ - Apparmor: "meta/custom-policy.apparmor", - }, - } - - a.m.Binaries = append(a.m.Binaries, Binary{Name: "app", SecurityDefinitions: *sec}) - a.m.legacyIntegration(false) - - err := handleApparmor(a.buildDir, a.m, "app", sec) +func makeMockSeccompCap(c *C, capname string, content []byte) { + mockPG := filepath.Join(securityPolicyTypeSeccomp.policyDir(), "policygroups", defaultPolicyVendor(), defaultPolicyVersion(), capname) + err := os.MkdirAll(filepath.Dir(mockPG), 0755) c.Assert(err, IsNil) - c.Assert(a.m.Integration["app"]["apparmor-profile"], Equals, "meta/custom-policy.apparmor") + err = ioutil.WriteFile(mockPG, []byte(content), 0644) + c.Assert(err, IsNil) } func (a *SecurityTestSuite) TestSnappyGetSecurityProfile(c *C) { @@ -205,99 +153,867 @@ func (a *SecurityTestSuite) TestSnappyGetSecurityProfileFramework(c *C) { c.Check(ap, Equals, "foo_bin-app_1.0") } -func (a *SecurityTestSuite) TestSnappySeccompSecurityTemplate(c *C) { - // simple case, just a security-template - sd := SecurityDefinitions{ - SecurityTemplate: "something", +func (a *SecurityTestSuite) TestSecurityGenDbusPath(c *C) { + c.Assert(dbusPath("foo"), Equals, "foo") + c.Assert(dbusPath("foo bar"), Equals, "foo_20bar") + c.Assert(dbusPath("foo/bar"), Equals, "foo_2fbar") +} + +func (a *SecurityTestSuite) TestSecurityFindWhitespacePrefix(c *C) { + t := ` ###POLICYGROUPS###` + c.Assert(findWhitespacePrefix(t, "###POLICYGROUPS###"), Equals, " ") + + t = `not there` + c.Assert(findWhitespacePrefix(t, "###POLICYGROUPS###"), Equals, "") + + t = `not there` + c.Assert(findWhitespacePrefix(t, "###POLICYGROUPS###"), Equals, "") +} + +func (a *SecurityTestSuite) TestSecurityFindWhitespacePrefixNeedsQuoting(c *C) { + s := `I need quoting: [` + t := `` + c.Assert(findWhitespacePrefix(s, t), Equals, t) +} + +// FIXME: need additional test for frameworkPolicy +func (a *SecurityTestSuite) TestSecurityFindTemplateApparmor(c *C) { + makeMockApparmorTemplate(c, "mock-template", []byte(`something`)) + + t, err := securityPolicyTypeAppArmor.findTemplate("mock-template") + c.Assert(err, IsNil) + c.Assert(t, Matches, "something") +} + +func (a *SecurityTestSuite) TestSecurityFindTemplateApparmorNotFound(c *C) { + _, err := securityPolicyTypeAppArmor.findTemplate("not-available-templ") + c.Assert(err, DeepEquals, &errPolicyNotFound{"template", &securityPolicyTypeAppArmor, "not-available-templ"}) +} + +// FIXME: need additional test for frameworkPolicy +func (a *SecurityTestSuite) TestSecurityFindCaps(c *C) { + for _, f := range []string{"cap1", "cap2"} { + makeMockApparmorCap(c, f, []byte(f)) } - _, err := generateSeccompPolicy(c.MkDir(), "appName", sd) + cap, err := securityPolicyTypeAppArmor.findCaps([]string{"cap1", "cap2"}, "mock-template") c.Assert(err, IsNil) + c.Assert(cap, DeepEquals, []string{"cap1", "cap2"}) +} - // sc-filtergen is called with mostly defaults - c.Assert(a.scFilterGenCall, DeepEquals, []string{ - "sc-filtergen", - fmt.Sprintf("--include-policy-dir=%s", filepath.Dir(dirs.SnapSeccompDir)), - "--policy-vendor=ubuntu-core", - "--policy-version=15.04", - "--template=something", - "--policy-groups=network-client", - }) +func (a *SecurityTestSuite) TestSecurityFindCapsMultipleErrorHandling(c *C) { + makeMockApparmorCap(c, "existing-cap", []byte("something")) + + _, err := securityPolicyTypeAppArmor.findCaps([]string{"existing-cap", "not-existing-cap"}, "mock-template") + c.Check(err, ErrorMatches, "could not find specified cap: not-existing-cap.*") + + _, err = securityPolicyTypeAppArmor.findCaps([]string{"not-existing-cap", "existing-cap"}, "mock-template") + c.Check(err, ErrorMatches, "could not find specified cap: not-existing-cap.*") + + _, err = securityPolicyTypeAppArmor.findCaps([]string{"existing-cap"}, "mock-template") + c.Check(err, IsNil) } -func (a *SecurityTestSuite) TestSnappySeccompSecurityCaps(c *C) { - // slightly complexer case, custom caps - sd := SecurityDefinitions{ - SecurityTemplate: "something", - SecurityCaps: []string{"cap1", "cap2"}, +func (a *SecurityTestSuite) TestSecurityGetAppArmorVars(c *C) { + appID := &securityAppID{ + Appname: "foo", + Version: "1.0", + AppID: "id", + Pkgname: "pkgname", } + c.Assert(appID.appArmorVars(), Equals, ` +# Specified profile variables +@{APP_APPNAME}="foo" +@{APP_ID_DBUS}="id" +@{APP_PKGNAME_DBUS}="pkgname" +@{APP_PKGNAME}="pkgname" +@{APP_VERSION}="1.0" +@{INSTALL_DIR}="{/apps,/oem}" +# Deprecated: +@{CLICK_DIR}="{/apps,/oem}"`) +} - _, err := generateSeccompPolicy(c.MkDir(), "appName", sd) +func (a *SecurityTestSuite) TestSecurityGenAppArmorPathRuleSimple(c *C) { + pr, err := genAppArmorPathRule("/some/path", "rk") c.Assert(err, IsNil) + c.Assert(pr, Equals, "/some/path rk,\n") +} - // sc-filtergen is called with mostly defaults - c.Assert(a.scFilterGenCall, DeepEquals, []string{ - "sc-filtergen", - fmt.Sprintf("--include-policy-dir=%s", filepath.Dir(dirs.SnapSeccompDir)), - "--policy-vendor=ubuntu-core", - "--policy-version=15.04", - "--template=something", - "--policy-groups=cap1,cap2", - }) +func (a *SecurityTestSuite) TestSecurityGenAppArmorPathRuleDir(c *C) { + pr, err := genAppArmorPathRule("/some/path/", "rk") + c.Assert(err, IsNil) + c.Assert(pr, Equals, `/some/path/ rk, +/some/path/** rk, +`) +} + +func (a *SecurityTestSuite) TestSecurityGenAppArmorPathRuleDirGlob(c *C) { + pr, err := genAppArmorPathRule("/some/path/**", "rk") + c.Assert(err, IsNil) + c.Assert(pr, Equals, `/some/path/ rk, +/some/path/** rk, +`) } -func (a *SecurityTestSuite) TestSnappySeccompSecurityOverride(c *C) { - // complex case, custom seccomp-override - baseDir := c.MkDir() - fn := filepath.Join(baseDir, "seccomp-override") - err := ioutil.WriteFile(fn, []byte(` -security-template: security-template -caps: [cap1, cap2] -syscalls: [read, write] -policy-vendor: policy-vendor -policy-version: 18.10`), 0644) +func (a *SecurityTestSuite) TestSecurityGenAppArmorPathRuleHome(c *C) { + pr, err := genAppArmorPathRule("/home/something", "rk") c.Assert(err, IsNil) + c.Assert(pr, Equals, "owner /home/something rk,\n") +} + +func (a *SecurityTestSuite) TestSecurityGenAppArmorPathRuleError(c *C) { + _, err := genAppArmorPathRule("some/path", "rk") + c.Assert(err, Equals, errPolicyGen) +} + +var mockApparmorTemplate = []byte(` +# Description: Allows unrestricted access to the system +# Usage: reserved - sd := SecurityDefinitions{ +# vim:syntax=apparmor + +#include <tunables/global> + +# Define vars with unconfined since autopilot rules may reference them +###VAR### + +# v2 compatible wildly permissive profile +###PROFILEATTACH### (attach_disconnected) { + capability, + network, + / rwkl, + /** rwlkm, + # Ubuntu Core is a minimal system so don't use 'pix' here. There are few + # profiles to transition to, and those that exist either won't work right + # anyway (eg, ubuntu-core-launcher) or would need to be modified to work + # with snaps (dhclient). + /** ix, + + mount, + remount, + + ###ABSTRACTIONS### + + ###POLICYGROUPS### + + ###READS### + + ###WRITES### +}`) + +var expectedGeneratedAaProfile = ` +# Description: Allows unrestricted access to the system +# Usage: reserved + +# vim:syntax=apparmor + +#include <tunables/global> + +# Define vars with unconfined since autopilot rules may reference them +# Specified profile variables +@{APP_APPNAME}="" +@{APP_ID_DBUS}="" +@{APP_PKGNAME_DBUS}="foo" +@{APP_PKGNAME}="foo" +@{APP_VERSION}="1.0" +@{INSTALL_DIR}="{/apps,/oem}" +# Deprecated: +@{CLICK_DIR}="{/apps,/oem}" + +# v2 compatible wildly permissive profile +profile "" (attach_disconnected) { + capability, + network, + / rwkl, + /** rwlkm, + # Ubuntu Core is a minimal system so don't use 'pix' here. There are few + # profiles to transition to, and those that exist either won't work right + # anyway (eg, ubuntu-core-launcher) or would need to be modified to work + # with snaps (dhclient). + /** ix, + + mount, + remount, + + # No abstractions specified + + # Rules specified via caps (policy groups) + capito + + # No read paths specified + + # No write paths specified +}` + +func (a *SecurityTestSuite) TestSecurityGenAppArmorTemplatePolicy(c *C) { + makeMockApparmorTemplate(c, "mock-template", mockApparmorTemplate) + makeMockApparmorCap(c, "cap1", []byte(`capito`)) + + m := &packageYaml{ + Name: "foo", + Version: "1.0", + } + appid := &securityAppID{ + Pkgname: "foo", + Version: "1.0", + } + template := "mock-template" + caps := []string{"cap1"} + overrides := &SecurityOverrideDefinition{} + p, err := getAppArmorTemplatedPolicy(m, appid, template, caps, overrides) + c.Check(err, IsNil) + c.Check(p, Equals, expectedGeneratedAaProfile) +} + +var mockSeccompTemplate = []byte(` +# Description: Allows access to app-specific directories and basic runtime +# Usage: common +# + +# Dangerous syscalls that we don't ever want to allow + +# kexec +deny kexec_load + +# fine +alarm +`) + +var expectedGeneratedSeccompProfile = ` +# Description: Allows access to app-specific directories and basic runtime +# Usage: common +# + +# Dangerous syscalls that we don't ever want to allow + +# kexec +# EXPLICITLY DENIED: kexec_load + +# fine +alarm + +#cap1 +capino` + +func (a *SecurityTestSuite) TestSecurityGenSeccompTemplatedPolicy(c *C) { + makeMockSeccompTemplate(c, "mock-template", mockSeccompTemplate) + makeMockSeccompCap(c, "cap1", []byte("#cap1\ncapino\n")) + + m := &packageYaml{ + Name: "foo", + Version: "1.0", + } + appid := &securityAppID{ + Pkgname: "foo", + Version: "1.0", + } + template := "mock-template" + caps := []string{"cap1"} + overrides := &SecurityOverrideDefinition{} + p, err := getSeccompTemplatedPolicy(m, appid, template, caps, overrides) + c.Check(err, IsNil) + c.Check(p, Equals, expectedGeneratedSeccompProfile) +} + +var aaCustomPolicy = ` +# Description: Some custom aa policy +# Usage: reserved + +# vim:syntax=apparmor + +#include <tunables/global> + +# Define vars with unconfined since autopilot rules may reference them +###VAR### + +# v2 compatible wildly permissive profile +###PROFILEATTACH### (attach_disconnected) { + capability, +} +` +var expectedAaCustomPolicy = ` +# Description: Some custom aa policy +# Usage: reserved + +# vim:syntax=apparmor + +#include <tunables/global> + +# Define vars with unconfined since autopilot rules may reference them +# Specified profile variables +@{APP_APPNAME}="" +@{APP_ID_DBUS}="foo_5fbar_5f1_2e0" +@{APP_PKGNAME_DBUS}="foo" +@{APP_PKGNAME}="foo" +@{APP_VERSION}="1.0" +@{INSTALL_DIR}="{/apps,/oem}" +# Deprecated: +@{CLICK_DIR}="{/apps,/oem}" + +# v2 compatible wildly permissive profile +profile "foo_bar_1.0" (attach_disconnected) { + capability, + +# No read paths specified +# No write paths specified +# No abstractions specified +} +` + +func (a *SecurityTestSuite) TestSecurityGetApparmorCustomPolicy(c *C) { + m := &packageYaml{ + Name: "foo", + Version: "1.0", + } + appid := &securityAppID{ + AppID: "foo_bar_1.0", + Pkgname: "foo", + Version: "1.0", + } + customPolicy := filepath.Join(c.MkDir(), "foo") + err := ioutil.WriteFile(customPolicy, []byte(aaCustomPolicy), 0644) + c.Assert(err, IsNil) + + p, err := getAppArmorCustomPolicy(m, appid, customPolicy, nil) + c.Check(err, IsNil) + c.Check(p, Equals, expectedAaCustomPolicy) +} + +func (a *SecurityTestSuite) TestSecurityGetSeccompCustomPolicy(c *C) { + // yes, getSeccompCustomPolicy does not care for packageYaml or appid + m := &packageYaml{} + appid := &securityAppID{} + + customPolicy := filepath.Join(c.MkDir(), "foo") + err := ioutil.WriteFile(customPolicy, []byte(`canary`), 0644) + c.Assert(err, IsNil) + + p, err := getSeccompCustomPolicy(m, appid, customPolicy) + c.Check(err, IsNil) + c.Check(p, Equals, `canary`) +} + +func (a *SecurityTestSuite) TestSecurityGetAppID(c *C) { + id, err := newAppID("pkg_app_1.0") + c.Assert(err, IsNil) + c.Assert(id, DeepEquals, &securityAppID{ + AppID: "pkg_app_1.0", + Pkgname: "pkg", + Appname: "app", + Version: "1.0", + }) +} + +func (a *SecurityTestSuite) TestSecurityGetAppIDInvalid(c *C) { + _, err := newAppID("invalid") + c.Assert(err, Equals, errInvalidAppID) +} + +func (a *SecurityTestSuite) TestSecurityMergeApparmorSecurityOverridesNilDoesNotCrash(c *C) { + sd := &SecurityDefinitions{} + sd.mergeAppArmorSecurityOverrides(nil) + c.Assert(sd, DeepEquals, &SecurityDefinitions{}) +} + +func (a *SecurityTestSuite) TestSecurityMergeApparmorSecurityOverridesTrivial(c *C) { + sd := &SecurityDefinitions{} + hwaccessOverrides := &SecurityOverrideDefinition{} + sd.mergeAppArmorSecurityOverrides(hwaccessOverrides) + + c.Assert(sd, DeepEquals, &SecurityDefinitions{ + SecurityOverride: hwaccessOverrides, + }) +} + +func (a *SecurityTestSuite) TestSecurityMergeApparmorSecurityOverridesOverrides(c *C) { + sd := &SecurityDefinitions{} + hwaccessOverrides := &SecurityOverrideDefinition{ + ReadPaths: []string{"read1"}, + WritePaths: []string{"write1"}, + } + sd.mergeAppArmorSecurityOverrides(hwaccessOverrides) + + c.Assert(sd, DeepEquals, &SecurityDefinitions{ + SecurityOverride: hwaccessOverrides, + }) +} + +func (a *SecurityTestSuite) TestSecurityMergeApparmorSecurityOverridesMerges(c *C) { + sd := &SecurityDefinitions{ SecurityOverride: &SecurityOverrideDefinition{ - Seccomp: "seccomp-override", + ReadPaths: []string{"orig1"}, }, } + hwaccessOverrides := &SecurityOverrideDefinition{ + ReadPaths: []string{"read1"}, + WritePaths: []string{"write1"}, + } + sd.mergeAppArmorSecurityOverrides(hwaccessOverrides) + + c.Assert(sd, DeepEquals, &SecurityDefinitions{ + SecurityOverride: &SecurityOverrideDefinition{ + ReadPaths: []string{"orig1", "read1"}, + WritePaths: []string{"write1"}, + }, + }) +} - _, err = generateSeccompPolicy(baseDir, "appName", sd) +func (a *SecurityTestSuite) TestSecurityGeneratePolicyForServiceBinaryEmpty(c *C) { + makeMockApparmorTemplate(c, "default", []byte(`# apparmor +###POLICYGROUPS### +`)) + makeMockApparmorCap(c, "network-client", []byte(` +aa-network-client`)) + makeMockSeccompTemplate(c, "default", []byte(`write`)) + makeMockSeccompCap(c, "network-client", []byte(` +sc-network-client +`)) + + // empty SecurityDefinition means "network-client" cap + sd := &SecurityDefinitions{} + m := &packageYaml{ + Name: "pkg", + Version: "1.0", + } + + // generate the apparmor profile + err := sd.generatePolicyForServiceBinary(m, "binary", "/apps/app.origin/1.0") c.Assert(err, IsNil) - // sc-filtergen is called with custom seccomp options - c.Assert(a.scFilterGenCall, DeepEquals, []string{ - "sc-filtergen", - fmt.Sprintf("--include-policy-dir=%s", filepath.Dir(dirs.SnapSeccompDir)), - "--policy-vendor=policy-vendor", - "--policy-version=18.10", - "--template=security-template", - "--policy-groups=cap1,cap2", - "--syscalls=read,write", - }) + // ensure the apparmor policy got loaded + c.Assert(a.loadAppArmorPolicyCalled, Equals, true) + + aaProfile := filepath.Join(dirs.SnapAppArmorDir, "pkg.origin_binary_1.0") + ensureFileContentMatches(c, aaProfile, `# apparmor +# Rules specified via caps (policy groups) + +aa-network-client +`) + scProfile := filepath.Join(dirs.SnapSeccompDir, "pkg.origin_binary_1.0") + ensureFileContentMatches(c, scProfile, `write + +sc-network-client`) + } -func (a *SecurityTestSuite) TestSnappySeccompSecurityPolicy(c *C) { - // ships pre-generated seccomp policy, ensure that sc-filtergen - // is not called - baseDir := c.MkDir() - fn := filepath.Join(baseDir, "seccomp-policy") - err := ioutil.WriteFile(fn, []byte(` +var mockSecurityPackageYaml = ` +name: hello-world +vendor: someone +version: 1.0 +binaries: + - name: binary1 + caps: [] +services: + - name: service1 + caps: [] +` + +func (a *SecurityTestSuite) TestSecurityGeneratePolicyFromFileSimple(c *C) { + // we need to create some fake data + makeMockApparmorTemplate(c, "default", []byte(`# some header +###POLICYGROUPS### +`)) + makeMockSeccompTemplate(c, "default", []byte(` +deny kexec read -write`), 0644) +write +`)) + + mockPackageYamlFn, err := makeInstalledMockSnap(dirs.GlobalRootDir, mockSecurityPackageYaml) c.Assert(err, IsNil) - sd := SecurityDefinitions{ - SecurityPolicy: &SecurityPolicyDefinition{ - Seccomp: "seccomp-policy", - }, + // the acutal thing that gets tested + err = GeneratePolicyFromFile(mockPackageYamlFn, false) + c.Assert(err, IsNil) + + // ensure the apparmor policy got loaded + c.Assert(a.loadAppArmorPolicyCalled, Equals, true) + + // apparmor + generatedProfileFn := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("hello-world.%s_binary1_1.0", testOrigin)) + ensureFileContentMatches(c, generatedProfileFn, `# some header +# No caps (policy groups) specified +`) + // ... and seccomp + generatedProfileFn = filepath.Join(dirs.SnapSeccompDir, fmt.Sprintf("hello-world.%s_binary1_1.0", testOrigin)) + ensureFileContentMatches(c, generatedProfileFn, ` +# EXPLICITLY DENIED: kexec +read +write + +`) +} + +func (a *SecurityTestSuite) TestSecurityGeneratePolicyFileForConfig(c *C) { + // we need to create some fake data + makeMockApparmorTemplate(c, "default", []byte(`# some header +###POLICYGROUPS### +`)) + makeMockSeccompTemplate(c, "default", []byte(` +deny kexec +read +write +`)) + + mockPackageYamlFn, err := makeInstalledMockSnap(dirs.GlobalRootDir, mockSecurityPackageYaml) + c.Assert(err, IsNil) + configHook := filepath.Join(filepath.Dir(mockPackageYamlFn), "hooks", "config") + os.MkdirAll(filepath.Dir(configHook), 0755) + err = ioutil.WriteFile(configHook, []byte("true"), 0755) + c.Assert(err, IsNil) + + // generate config + err = GeneratePolicyFromFile(mockPackageYamlFn, false) + c.Assert(err, IsNil) + + // and for snappy-config + generatedProfileFn := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("hello-world.%s_snappy-config_1.0", testOrigin)) + ensureFileContentMatches(c, generatedProfileFn, `# some header +# No caps (policy groups) specified +`) + +} + +func (a *SecurityTestSuite) TestSecurityCompareGeneratePolicyFromFileSimple(c *C) { + // we need to create some fake data + makeMockApparmorTemplate(c, "default", []byte(`# some header +###POLICYGROUPS### +`)) + makeMockSeccompTemplate(c, "default", []byte(` +deny kexec +read +write +`)) + mockPackageYamlFn, err := makeInstalledMockSnap(dirs.GlobalRootDir, mockSecurityPackageYaml) + c.Assert(err, IsNil) + + err = GeneratePolicyFromFile(mockPackageYamlFn, false) + c.Assert(err, IsNil) + + // nothing changed, compare is happy + err = CompareGeneratePolicyFromFile(mockPackageYamlFn) + c.Assert(err, IsNil) + + // now change the templates + makeMockApparmorTemplate(c, "default", []byte(`# some different header +###POLICYGROUPS### +`)) + // ...and ensure that the difference is found + err = CompareGeneratePolicyFromFile(mockPackageYamlFn) + c.Assert(err, ErrorMatches, "policy differs.*") +} + +func (a *SecurityTestSuite) TestSecurityGeneratePolicyFromFileHwAccess(c *C) { + // we need to create some fake data + makeMockApparmorTemplate(c, "default", []byte(`# some header +###POLICYGROUPS### +###READS### +###WRITES### +`)) + makeMockSeccompTemplate(c, "default", []byte(` +deny kexec +read +write +`)) + mockPackageYamlFn, err := makeInstalledMockSnap(dirs.GlobalRootDir, mockSecurityPackageYaml) + c.Assert(err, IsNil) + err = GeneratePolicyFromFile(mockPackageYamlFn, false) + c.Assert(err, IsNil) + + // ensure that AddHWAccess does the right thing + a.loadAppArmorPolicyCalled = false + err = AddHWAccess("hello-world."+testOrigin, "/dev/kmesg") + c.Assert(err, IsNil) + + // ensure the apparmor policy got loaded + c.Check(a.loadAppArmorPolicyCalled, Equals, true) + + // apparmor got updated with the new read path + generatedProfileFn := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("hello-world.%s_binary1_1.0", testOrigin)) + ensureFileContentMatches(c, generatedProfileFn, `# some header +# No caps (policy groups) specified +# Additional read-paths from security-override +/run/udev/data/ rk, +/run/udev/data/* rk, + +# Additional write-paths from security-override +/dev/kmesg rwk, + +`) +} + +func (a *SecurityTestSuite) TestSecurityRegenerateAll(c *C) { + // we need to create some fake data + makeMockApparmorTemplate(c, "default", []byte(`# some header +###POLICYGROUPS### +`)) + makeMockSeccompTemplate(c, "default", []byte(` +deny kexec +read +write +`)) + mockPackageYamlFn, err := makeInstalledMockSnap(dirs.GlobalRootDir, mockSecurityPackageYaml) + c.Assert(err, IsNil) + + err = GeneratePolicyFromFile(mockPackageYamlFn, false) + c.Assert(err, IsNil) + + // now change the templates + makeMockApparmorTemplate(c, "default", []byte(`# some different header +###POLICYGROUPS### +`)) + // ...and regenerate the templates + err = RegenerateAllPolicy(false) + c.Assert(err, IsNil) + + // ensure apparmor got updated with the new read path + generatedProfileFn := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("hello-world.%s_binary1_1.0", testOrigin)) + ensureFileContentMatches(c, generatedProfileFn, `# some different header +# No caps (policy groups) specified +`) + +} + +func (a *SecurityTestSuite) TestSnappyFindUbuntuVersion(c *C) { + realLsbRelease := lsbRelease + defer func() { lsbRelease = realLsbRelease }() + + lsbRelease = filepath.Join(c.MkDir(), "mock-lsb-release") + s := `DISTRIB_RELEASE=18.09` + err := ioutil.WriteFile(lsbRelease, []byte(s), 0644) + c.Assert(err, IsNil) + + ver, err := findUbuntuVersion() + c.Assert(err, IsNil) + c.Assert(ver, Equals, "18.09") +} + +func (a *SecurityTestSuite) TestSnappyFindUbuntuVersionNotFound(c *C) { + realLsbRelease := lsbRelease + defer func() { lsbRelease = realLsbRelease }() + + lsbRelease = filepath.Join(c.MkDir(), "mock-lsb-release") + s := `silly stuff` + err := ioutil.WriteFile(lsbRelease, []byte(s), 0644) + c.Assert(err, IsNil) + + _, err = findUbuntuVersion() + c.Assert(err, Equals, errSystemVersionNotFound) +} + +func makeCustomAppArmorPolicy(c *C) string { + content := []byte(`# custom apparmor policy +###VAR### + +###PROFILEATTACH### (attach_disconnected) { + stuff + +} +`) + fn := filepath.Join(c.MkDir(), "custom-aa-policy") + err := ioutil.WriteFile(fn, content, 0644) + c.Assert(err, IsNil) + + return fn +} + +func (a *SecurityTestSuite) TestSecurityGenerateCustomPolicyAdditionalIsConsidered(c *C) { + m := &packageYaml{ + Name: "foo", + Version: "1.0", + } + appid := &securityAppID{ + Pkgname: "foo", + Version: "1.0", } + fn := makeCustomAppArmorPolicy(c) - _, err = generateSeccompPolicy(baseDir, "appName", sd) + content, err := getAppArmorCustomPolicy(m, appid, fn, nil) c.Assert(err, IsNil) + c.Assert(content, Matches, `(?ms).*^# No read paths specified$`) + c.Assert(content, Matches, `(?ms).*^# No write paths specified$`) + c.Assert(content, Matches, `(?ms).*^# No abstractions specified$`) +} + +var mockSecurityDeprecatedPackageYaml = ` +name: hello-world +vendor: someone +version: 1.0 +binaries: + - name: binary1 + caps: [] +` + +var mockSecurityDeprecatedPackageYamlApparmor1 = ` + security-override: + apparmor: + read-path: [foo] +` +var mockSecurityDeprecatedPackageYamlApparmor2 = ` + security-override: + apparmor: {} +` +var mockSecurityDeprecatedPackageYamlSeccomp1 = ` + security-override: + seccomp: {} +` + +var mockSecurityDeprecatedPackageYamlSeccomp2 = ` + security-override: + seccomp: + syscalls: [1] +` + +type mockLogger struct { + notice []string + debug []string +} + +func (l *mockLogger) Notice(msg string) { + l.notice = append(l.notice, msg) +} + +func (l *mockLogger) Debug(msg string) { + l.debug = append(l.debug, msg) +} + +func (a *SecurityTestSuite) TestSecurityWarnsNot(c *C) { + makeMockApparmorTemplate(c, "default", []byte(``)) + makeMockSeccompTemplate(c, "default", []byte(``)) + + ml := &mockLogger{} + logger.SetLogger(ml) + + mockPackageYamlFn, err := makeInstalledMockSnap(dirs.GlobalRootDir, mockSecurityDeprecatedPackageYaml) + c.Assert(err, IsNil) + + err = GeneratePolicyFromFile(mockPackageYamlFn, false) + c.Assert(err, IsNil) + + c.Assert(ml.notice, DeepEquals, []string(nil)) +} + +func (a *SecurityTestSuite) TestSecurityWarnsOnDeprecatedApparmor(c *C) { + makeMockApparmorTemplate(c, "default", []byte(``)) + makeMockSeccompTemplate(c, "default", []byte(``)) + + for _, s := range []string{mockSecurityDeprecatedPackageYamlApparmor1, mockSecurityDeprecatedPackageYamlApparmor2} { + + ml := &mockLogger{} + logger.SetLogger(ml) + + mockPackageYamlFn, err := makeInstalledMockSnap(dirs.GlobalRootDir, mockSecurityDeprecatedPackageYaml+s) + c.Assert(err, IsNil) + + err = GeneratePolicyFromFile(mockPackageYamlFn, false) + c.Assert(err, IsNil) + + c.Assert(ml.notice, DeepEquals, []string{"The security-override.apparmor key is no longer supported, please use use security-override directly"}) + } +} + +func (a *SecurityTestSuite) TestSecurityWarnsOnDeprecatedSeccomp(c *C) { + makeMockApparmorTemplate(c, "default", []byte(``)) + makeMockSeccompTemplate(c, "default", []byte(``)) + + for _, s := range []string{mockSecurityDeprecatedPackageYamlSeccomp1, mockSecurityDeprecatedPackageYamlSeccomp2} { + + ml := &mockLogger{} + logger.SetLogger(ml) + + mockPackageYamlFn, err := makeInstalledMockSnap(dirs.GlobalRootDir, mockSecurityDeprecatedPackageYaml+s) + c.Assert(err, IsNil) + + err = GeneratePolicyFromFile(mockPackageYamlFn, false) + c.Assert(err, IsNil) + + c.Assert(ml.notice, DeepEquals, []string{"The security-override.seccomp key is no longer supported, please use use security-override directly"}) + } +} + +func makeInstalledMockSnapSideloaded(c *C) string { + mockPackageYamlFn, err := makeInstalledMockSnap(dirs.GlobalRootDir, mockSecurityPackageYaml) + c.Assert(err, IsNil) + // pretend its sideloaded + basePath := regexp.MustCompile(`(.*)/hello-world.` + testOrigin).FindString(mockPackageYamlFn) + oldPath := filepath.Join(basePath, "1.0") + newPath := filepath.Join(basePath, "IsSideloadVer") + err = os.Rename(oldPath, newPath) + mockPackageYamlFn = filepath.Join(basePath, "IsSideloadVer", "meta", "package.yaml") + + return mockPackageYamlFn +} + +func (a *SecurityTestSuite) TestSecurityGeneratePolicyFromFileSideload(c *C) { + // we need to create some fake data + makeMockApparmorTemplate(c, "default", []byte(``)) + makeMockSeccompTemplate(c, "default", []byte(``)) + + mockPackageYamlFn := makeInstalledMockSnapSideloaded(c) + + // the acutal thing that gets tested + err := GeneratePolicyFromFile(mockPackageYamlFn, false) + c.Assert(err, IsNil) + + // ensure the apparmor policy got loaded + c.Assert(a.loadAppArmorPolicyCalled, Equals, true) + + // apparmor + generatedProfileFn := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("hello-world.%s_binary1_IsSideloadVer", testOrigin)) + c.Assert(helpers.FileExists(generatedProfileFn), Equals, true) + + // ... and seccomp + generatedProfileFn = filepath.Join(dirs.SnapSeccompDir, fmt.Sprintf("hello-world.%s_binary1_IsSideloadVer", testOrigin)) + c.Assert(helpers.FileExists(generatedProfileFn), Equals, true) +} + +func (a *SecurityTestSuite) TestSecurityCompareGeneratePolicyFromFileSideload(c *C) { + // we need to create some fake data + makeMockApparmorTemplate(c, "default", []byte(``)) + makeMockSeccompTemplate(c, "default", []byte(``)) + + mockPackageYamlFn := makeInstalledMockSnapSideloaded(c) + // generate policy + err := GeneratePolicyFromFile(mockPackageYamlFn, false) + c.Assert(err, IsNil) + + // nothing changed, ensure compare is happy even for sideloaded pkgs + err = CompareGeneratePolicyFromFile(mockPackageYamlFn) + c.Assert(err, IsNil) +} + +func (a *SecurityTestSuite) TestSecurityGeneratePolicyForServiceBinaryFramework(c *C) { + makeMockSecurityEnv(c) + + sd := &SecurityDefinitions{} + m := &packageYaml{ + Name: "framework-name", + Type: "framework", + Version: "1.0", + } + + // generate the apparmor profile + err := sd.generatePolicyForServiceBinary(m, "binary", "/apps/framework-anem/1.0") + c.Assert(err, IsNil) + + // ensure its available with the right names + aaProfile := filepath.Join(dirs.SnapAppArmorDir, "framework-name_binary_1.0") + ensureFileContentMatches(c, aaProfile, ``) + scProfile := filepath.Join(dirs.SnapSeccompDir, "framework-name_binary_1.0") + ensureFileContentMatches(c, scProfile, ` +`) +} + +func (a *SecurityTestSuite) TestSecurityGeneratePolicyForServiceBinaryErrors(c *C) { + makeMockSecurityEnv(c) + + sd := &SecurityDefinitions{} + m := &packageYaml{ + Name: "app", + Version: "1.0", + } - // sc-filtergen is not called at all - c.Assert(a.scFilterGenCall, DeepEquals, []string(nil)) + // ensure invalid packages generate an error + err := sd.generatePolicyForServiceBinary(m, "binary", "/apps/app-no-origin/1.0") + c.Assert(err, ErrorMatches, "invalid package on system") } diff --git a/snappy/service_test.go b/snappy/service_test.go index 2c5fbd57c5..9b663c8017 100644 --- a/snappy/service_test.go +++ b/snappy/service_test.go @@ -76,7 +76,6 @@ func (s *ServiceActorSuite) SetUpTest(c *C) { os.Setenv("TZ", "") dirs.SetRootDir(c.MkDir()) - c.Assert(os.MkdirAll(dirs.SnapMetaDir, 0755), IsNil) // TODO: this mkdir hack is so enable doesn't fail; remove when enable is the same as the rest c.Assert(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/etc/systemd/system/multi-user.target.wants"), 0755), IsNil) systemd.SystemctlCmd = s.myRun diff --git a/snappy/snapp.go b/snappy/snapp.go index 5c70fa0d1a..d0f83de16d 100644 --- a/snappy/snapp.go +++ b/snappy/snapp.go @@ -28,7 +28,6 @@ import ( "net/http" "net/url" "os" - "os/exec" "path/filepath" "reflect" "regexp" @@ -38,6 +37,7 @@ import ( "gopkg.in/yaml.v2" + "github.com/ubuntu-core/snappy/arch" "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/helpers" "github.com/ubuntu-core/snappy/logger" @@ -48,6 +48,7 @@ import ( "github.com/ubuntu-core/snappy/progress" "github.com/ubuntu-core/snappy/release" "github.com/ubuntu-core/snappy/systemd" + "github.com/ubuntu-core/snappy/timeout" ) const ( @@ -93,32 +94,6 @@ type Ports struct { External map[string]Port `yaml:"external,omitempty" json:"external,omitempty"` } -// SecurityOverrideDefinition is used to override apparmor or seccomp -// security defaults -type SecurityOverrideDefinition struct { - Apparmor string `yaml:"apparmor" json:"apparmor"` - Seccomp string `yaml:"seccomp" json:"seccomp"` -} - -// SecurityPolicyDefinition is used to provide hand-crafted policy -type SecurityPolicyDefinition struct { - Apparmor string `yaml:"apparmor" json:"apparmor"` - Seccomp string `yaml:"seccomp" json:"seccomp"` -} - -// SecurityDefinitions contains the common apparmor/seccomp definitions -type SecurityDefinitions struct { - // SecurityTemplate is a template like "default" - SecurityTemplate string `yaml:"security-template,omitempty" json:"security-template,omitempty"` - // SecurityOverride is a override for the high level security json - SecurityOverride *SecurityOverrideDefinition `yaml:"security-override,omitempty" json:"security-override,omitempty"` - // SecurityPolicy is a hand-crafted low-level policy - SecurityPolicy *SecurityPolicyDefinition `yaml:"security-policy,omitempty" json:"security-policy,omitempty"` - - // SecurityCaps is are the apparmor/seccomp capabilities for an app - SecurityCaps []string `yaml:"caps,omitempty" json:"caps,omitempty"` -} - // NeedsAppArmorUpdate checks whether the security definitions are impacted by // changes to policies or templates. func (sd *SecurityDefinitions) NeedsAppArmorUpdate(policies, templates map[string]bool) bool { @@ -149,12 +124,12 @@ type ServiceYaml struct { Name string `yaml:"name" json:"name,omitempty"` Description string `yaml:"description,omitempty" json:"description,omitempty"` - Start string `yaml:"start,omitempty" json:"start,omitempty"` - Stop string `yaml:"stop,omitempty" json:"stop,omitempty"` - PostStop string `yaml:"poststop,omitempty" json:"poststop,omitempty"` - StopTimeout Timeout `yaml:"stop-timeout,omitempty" json:"stop-timeout,omitempty"` - BusName string `yaml:"bus-name,omitempty" json:"bus-name,omitempty"` - Forking bool `yaml:"forking,omitempty" json:"forking,omitempty"` + Start string `yaml:"start,omitempty" json:"start,omitempty"` + Stop string `yaml:"stop,omitempty" json:"stop,omitempty"` + PostStop string `yaml:"poststop,omitempty" json:"poststop,omitempty"` + StopTimeout timeout.Timeout `yaml:"stop-timeout,omitempty" json:"stop-timeout,omitempty"` + BusName string `yaml:"bus-name,omitempty" json:"bus-name,omitempty"` + Forking bool `yaml:"forking,omitempty" json:"forking,omitempty"` // set to yes if we need to create a systemd socket for this service Socket bool `yaml:"socket,omitempty" json:"socket,omitempty"` @@ -342,7 +317,7 @@ func parsePackageYamlData(yamlData []byte, hasConfig bool) (*packageYaml, error) for i := range m.ServiceYamls { if m.ServiceYamls[i].StopTimeout == 0 { - m.ServiceYamls[i].StopTimeout = DefaultTimeout + m.ServiceYamls[i].StopTimeout = timeout.DefaultTimeout } } @@ -468,25 +443,6 @@ func (m *packageYaml) checkLicenseAgreement(ag agreer, d PackageFile, currentAct return nil } -func (m *packageYaml) legacyIntegrateSecDef(hookName string, s *SecurityDefinitions) { - // see if we have a custom security policy - if s.SecurityPolicy != nil && s.SecurityPolicy.Apparmor != "" { - m.Integration[hookName]["apparmor-profile"] = s.SecurityPolicy.Apparmor - return - } - - // see if we have a security override - if s.SecurityOverride != nil && s.SecurityOverride.Apparmor != "" { - m.Integration[hookName]["apparmor"] = s.SecurityOverride.Apparmor - return - } - - // apparmor template - m.Integration[hookName]["apparmor"] = filepath.Join("meta", hookName+".apparmor") - - return -} - // legacyIntegration sets up the Integration property of packageYaml from its other attributes func (m *packageYaml) legacyIntegration(hasConfig bool) { if m.Integration != nil { @@ -505,8 +461,6 @@ func (m *packageYaml) legacyIntegration(hasConfig bool) { } // legacy click hook m.Integration[hookName]["bin-path"] = v.Exec - - m.legacyIntegrateSecDef(hookName, &v.SecurityDefinitions) } for _, v := range m.ServiceYamls { @@ -515,9 +469,6 @@ func (m *packageYaml) legacyIntegration(hasConfig bool) { if _, ok := m.Integration[hookName]; !ok { m.Integration[hookName] = clickAppHook{} } - - // handle the apparmor stuff - m.legacyIntegrateSecDef(hookName, &v.SecurityDefinitions) } if hasConfig { @@ -839,8 +790,8 @@ func (s *SnapPart) Install(inter progress.Meter, flags InstallFlags) (name strin return "", err } - // legacy, the hooks (e.g. apparmor) need this. Once we converted - // all hooks this can go away + // legacy, the hooks need this. Once we converted all hooks this can go + // away clickMetaDir := filepath.Join(s.basedir, ".click", "info") if err := os.MkdirAll(clickMetaDir, 0755); err != nil { return "", err @@ -1009,7 +960,10 @@ func (s *SnapPart) activate(inhibitHooks bool, inter interacter) error { } // generate the security policy from the package.yaml - if err := s.m.addSecurityPolicy(s.basedir); err != nil { + // Note that this must happen before binaries/services are + // generated because serices may get started + appsDir := filepath.Join(dirs.SnapAppsDir, QualifiedName(s), s.Version()) + if err := generatePolicy(s.m, appsDir); err != nil { return err } @@ -1068,7 +1022,7 @@ func (s *SnapPart) deactivate(inhibitHooks bool, inter interacter) error { return err } - if err := s.m.removeSecurityPolicy(s.basedir); err != nil { + if err := removePolicy(s.m, s.basedir); err != nil { return err } @@ -1233,7 +1187,7 @@ func (s *SnapPart) CanInstall(allowOEM bool, inter interacter) error { } // verify we have a valid architecture - if !helpers.IsSupportedArchitecture(s.m.Architectures) { + if !arch.IsSupportedArchitecture(s.m.Architectures) { return &ErrArchitectureNotSupported{s.m.Architectures} } @@ -1266,34 +1220,35 @@ func (s *SnapPart) CanInstall(allowOEM bool, inter interacter) error { return nil } -var timestampUpdater = helpers.UpdateTimestamp - -func updateAppArmorJSONTimestamp(fullName, thing, version string) error { - fn := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("%s_%s_%s.json", fullName, thing, version)) - return timestampUpdater(fn) -} - -// RequestAppArmorUpdate checks whether changes to the given policies and -// templates impacts the snap, and updates the timestamp of the relevant json -// symlinks (thus requesting aaClickHookCmd regenerate the appropriate bits). -func (s *SnapPart) RequestAppArmorUpdate(policies, templates map[string]bool) error { - - fullName := QualifiedName(s) +// RequestSecurityPolicyUpdate checks whether changes to the given policies and +// templates impacts the snap, and updates the policy if needed +func (s *SnapPart) RequestSecurityPolicyUpdate(policies, templates map[string]bool) error { + var foundError error for _, svc := range s.ServiceYamls() { if svc.NeedsAppArmorUpdate(policies, templates) { - if err := updateAppArmorJSONTimestamp(fullName, svc.Name, s.Version()); err != nil { - return err + err := svc.generatePolicyForServiceBinary(s.m, svc.Name, s.basedir) + if err != nil { + logger.Noticef("Failed to regenerate policy for %s: %v", svc.Name, err) + foundError = err } } } for _, bin := range s.Binaries() { if bin.NeedsAppArmorUpdate(policies, templates) { - if err := updateAppArmorJSONTimestamp(fullName, bin.Name, s.Version()); err != nil { - return err + err := bin.generatePolicyForServiceBinary(s.m, bin.Name, s.basedir) + if err != nil { + logger.Noticef("Failed to regenerate policy for %s: %v", bin.Name, err) + foundError = err } } } + // FIXME: if there are multiple errors only the last one + // will be preserved + if foundError != nil { + return foundError + } + return nil } @@ -1311,23 +1266,12 @@ func (s *SnapPart) RefreshDependentsSecurity(oldPart *SnapPart, inter interacter } for _, dep := range deps { - err := dep.RequestAppArmorUpdate(upPol, upTpl) + err := dep.RequestSecurityPolicyUpdate(upPol, upTpl) if err != nil { return err } } - cmd := exec.Command(aaClickHookCmd) - if output, err := cmd.CombinedOutput(); err != nil { - if exitCode, err := helpers.ExitCode(err); err == nil { - return &ErrApparmorGenerate{ - ExitCode: exitCode, - Output: output, - } - } - return err - } - return nil } @@ -1753,7 +1697,7 @@ func setUbuntuStoreHeaders(req *http.Request) { // frameworks frameworks, _ := ActiveSnapIterByType(BareName, pkg.TypeFramework) req.Header.Set("X-Ubuntu-Frameworks", strings.Join(addCoreFmk(frameworks), ",")) - req.Header.Set("X-Ubuntu-Architecture", string(Architecture())) + req.Header.Set("X-Ubuntu-Architecture", string(arch.UbuntuArchitecture())) req.Header.Set("X-Ubuntu-Release", release.String()) req.Header.Set("X-Ubuntu-Wire-Protocol", UbuntuCoreWireProtocol) req.Header.Set("X-Ubuntu-Device-Channel", release.Get().Channel) @@ -1971,7 +1915,7 @@ func makeSnapHookEnv(part *SnapPart) (env []string) { Origin string }{ part.Name(), - helpers.UbuntuArchitecture(), + arch.UbuntuArchitecture(), part.basedir, part.Version(), QualifiedName(part), diff --git a/snappy/snapp_snapfs_test.go b/snappy/snapp_snapfs_test.go index afc7c772c6..d081ee5ece 100644 --- a/snappy/snapp_snapfs_test.go +++ b/snappy/snapp_snapfs_test.go @@ -24,44 +24,42 @@ import ( "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/helpers" - "github.com/ubuntu-core/snappy/pkg/snapfs" + "github.com/ubuntu-core/snappy/pkg/squashfs" . "gopkg.in/check.v1" ) -type SnapfsTestSuite struct { +type SquashfsTestSuite struct { } -func (s *SnapfsTestSuite) SetUpTest(c *C) { - // mocks - aaClickHookCmd = "/bin/true" +func (s *SquashfsTestSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) - // ensure we use the right builder func (snapfs) - snapBuilderFunc = BuildSnapfsSnap + // ensure we use the right builder func (squashfs) + snapBuilderFunc = BuildSquashfsSnap } -func (s *SnapfsTestSuite) TearDownTest(c *C) { +func (s *SquashfsTestSuite) TearDownTest(c *C) { snapBuilderFunc = BuildLegacySnap } -var _ = Suite(&SnapfsTestSuite{}) +var _ = Suite(&SquashfsTestSuite{}) const packageHello = `name: hello-app version: 1.10 icon: meta/hello.svg ` -func (s *SnapfsTestSuite) TestMakeSnapMakesSnapfs(c *C) { +func (s *SquashfsTestSuite) TestMakeSnapMakesSquashfs(c *C) { snapPkg := makeTestSnapPackage(c, packageHello) part, err := NewSnapPartFromSnapFile(snapPkg, "origin", true) c.Assert(err, IsNil) // ensure the right backend got picked up - c.Assert(part.deb, FitsTypeOf, &snapfs.Snap{}) + c.Assert(part.deb, FitsTypeOf, &squashfs.Snap{}) } -func (s *SnapfsTestSuite) TestInstallViaSnapfsWorks(c *C) { +func (s *SquashfsTestSuite) TestInstallViaSquashfsWorks(c *C) { snapPkg := makeTestSnapPackage(c, packageHello) part, err := NewSnapPartFromSnapFile(snapPkg, "origin", true) c.Assert(err, IsNil) diff --git a/snappy/snapp_test.go b/snappy/snapp_test.go index af44de2184..b7311476d2 100644 --- a/snappy/snapp_test.go +++ b/snappy/snapp_test.go @@ -30,6 +30,7 @@ import ( "path/filepath" "strings" + "github.com/ubuntu-core/snappy/arch" "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/helpers" "github.com/ubuntu-core/snappy/partition" @@ -51,8 +52,6 @@ type SnapTestSuite struct { var _ = Suite(&SnapTestSuite{}) func (s *SnapTestSuite) SetUpTest(c *C) { - s.clickhook = aaClickHookCmd - aaClickHookCmd = "/bin/true" s.secbase = policy.SecBase s.tempdir = c.MkDir() newPartition = func() (p partition.Interface) { @@ -63,7 +62,6 @@ func (s *SnapTestSuite) SetUpTest(c *C) { policy.SecBase = filepath.Join(s.tempdir, "security") os.MkdirAll(dirs.SnapServicesDir, 0755) os.MkdirAll(dirs.SnapSeccompDir, 0755) - os.MkdirAll(dirs.SnapMetaDir, 0755) release.Override(release.Release{Flavor: "core", Series: "15.04"}) @@ -97,18 +95,18 @@ func (s *SnapTestSuite) SetUpTest(c *C) { err := ioutil.WriteFile(aaExec, []byte(mockAaExecScript), 0755) c.Assert(err, IsNil) - runScFilterGen = mockRunScFilterGen + runAppArmorParser = mockRunAppArmorParser + + makeMockSecurityEnv(c) } func (s *SnapTestSuite) TearDownTest(c *C) { // ensure all functions are back to their original state - aaClickHookCmd = s.clickhook policy.SecBase = s.secbase regenerateAppArmorRules = regenerateAppArmorRulesImpl ActiveSnapIterByType = activeSnapIterByTypeImpl duCmd = "du" stripGlobalRootDir = stripGlobalRootDirImpl - runScFilterGen = runScFilterGenImpl runUdevAdm = runUdevAdmImpl } @@ -774,7 +772,7 @@ architectures: c.Assert(err, IsNil) _, err = part.Install(&MockProgressMeter{}, 0) - errorMsg := fmt.Sprintf("package's supported architectures (yadayada, blahblah) is incompatible with this system (%s)", helpers.UbuntuArchitecture()) + errorMsg := fmt.Sprintf("package's supported architectures (yadayada, blahblah) is incompatible with this system (%s)", arch.UbuntuArchitecture()) c.Assert(err.Error(), Equals, errorMsg) } @@ -984,7 +982,10 @@ binaries: - name: testme-override exec: bin/testme-override security-override: - apparmor: meta/testme-override.apparmor + read-paths: + - "/foo" + syscalls: + - "bar" - name: testme-policy exec: bin/testme-policy security-policy: @@ -1004,12 +1005,13 @@ func (s *SnapTestSuite) TestPackageYamlSecurityBinaryParsing(c *C) { c.Assert(m.Binaries[1].Name, Equals, "testme-override") c.Assert(m.Binaries[1].Exec, Equals, "bin/testme-override") c.Assert(m.Binaries[1].SecurityCaps, HasLen, 0) - c.Assert(m.Binaries[1].SecurityOverride.Apparmor, Equals, "meta/testme-override.apparmor") + c.Assert(m.Binaries[1].SecurityOverride.ReadPaths[0], Equals, "/foo") + c.Assert(m.Binaries[1].SecurityOverride.Syscalls[0], Equals, "bar") c.Assert(m.Binaries[2].Name, Equals, "testme-policy") c.Assert(m.Binaries[2].Exec, Equals, "bin/testme-policy") c.Assert(m.Binaries[2].SecurityCaps, HasLen, 0) - c.Assert(m.Binaries[2].SecurityPolicy.Apparmor, Equals, "meta/testme-policy.profile") + c.Assert(m.Binaries[2].SecurityPolicy.AppArmor, Equals, "meta/testme-policy.profile") } var securityServicePackageYaml = []byte(`name: test-snap @@ -1126,9 +1128,6 @@ func (s *SnapTestSuite) TestUsesStoreMetaData(c *C) { c.Assert(err, IsNil) c.Assert(makeSnapActive(yamlPath), IsNil) - err = os.MkdirAll(dirs.SnapMetaDir, 0755) - c.Assert(err, IsNil) - data = "name: afoo\nalias: afoo\ndescription: something nice\ndownloadsize: 10\norigin: someplace" err = ioutil.WriteFile(filepath.Join(dirs.SnapMetaDir, "afoo_1.manifest"), []byte(data), 0644) c.Assert(err, IsNil) @@ -1199,16 +1198,10 @@ func (s *SnapTestSuite) TestRefreshDependentsSecurity(c *C) { oldDir := dirs.SnapAppArmorDir defer func() { dirs.SnapAppArmorDir = oldDir - timestampUpdater = helpers.UpdateTimestamp }() - touched := []string{} dirs.SnapAppArmorDir = c.MkDir() - fn := filepath.Join(dirs.SnapAppArmorDir, "foo."+testOrigin+"_hello_1.0.json") + fn := filepath.Join(dirs.SnapAppArmorDir, "foo."+testOrigin+"_hello_1.0") c.Assert(os.Symlink(fn, fn), IsNil) - timestampUpdater = func(s string) error { - touched = append(touched, s) - return nil - } _, err := makeInstalledMockSnap(s.tempdir, `name: foo version: 1.0 @@ -1216,8 +1209,8 @@ frameworks: - fmk binaries: - name: hello - security-override: - apparmor: fmk_foo + caps: + - fmk_foo `) c.Assert(err, IsNil) @@ -1234,13 +1227,13 @@ binaries: _, err = makeInstalledMockSnap(d2, "name: fmk\ntype: framework\nversion: 2") c.Assert(err, IsNil) c.Assert(os.MkdirAll(filepath.Join(d2, dp), 0755), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(d2, dp, "foo"), []byte("x"), 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(d2, dp, "fmk_foo"), []byte("x"), 0644), IsNil) pb := &MockProgressMeter{} m, err := parsePackageYamlData([]byte(yaml), false) part := &SnapPart{m: m, origin: testOrigin, basedir: d1} c.Assert(part.RefreshDependentsSecurity(&SnapPart{basedir: d2}, pb), IsNil) - c.Check(touched, DeepEquals, []string{fn}) + // TODO: verify it was updated } func (s *SnapTestSuite) TestRemoveChecksFrameworks(c *C) { @@ -1298,51 +1291,28 @@ func (s *SnapTestSuite) TestNeedsAppArmorUpdatePolicyAbsent(c *C) { c.Check(sd.NeedsAppArmorUpdate(map[string]bool{"foo_bar": true}, nil), Equals, false) } -func (s *SnapTestSuite) TestRequestAppArmorUpdateService(c *C) { - var updated []string - timestampUpdater = func(s string) error { - updated = append(updated, s) - return nil - } - defer func() { timestampUpdater = helpers.UpdateTimestamp }() +func (s *SnapTestSuite) TestRequestSecurityPolicyUpdateService(c *C) { // if one of the services needs updating, it's updated and returned svc := ServiceYaml{Name: "svc", SecurityDefinitions: SecurityDefinitions{SecurityTemplate: "foo"}} - part := &SnapPart{m: &packageYaml{Name: "part", ServiceYamls: []ServiceYaml{svc}, Version: "42"}, origin: testOrigin} - err := part.RequestAppArmorUpdate(nil, map[string]bool{"foo": true}) - c.Assert(err, IsNil) - c.Assert(updated, HasLen, 1) - c.Check(filepath.Base(updated[0]), Equals, "part."+testOrigin+"_svc_42.json") + part := &SnapPart{m: &packageYaml{Name: "part", ServiceYamls: []ServiceYaml{svc}, Version: "42"}, origin: testOrigin, basedir: filepath.Join(dirs.SnapAppsDir, "part."+testOrigin, "42")} + err := part.RequestSecurityPolicyUpdate(nil, map[string]bool{"foo": true}) + c.Assert(err, NotNil) } -func (s *SnapTestSuite) TestRequestAppArmorUpdateBinary(c *C) { - var updated []string - timestampUpdater = func(s string) error { - updated = append(updated, s) - return nil - } - defer func() { timestampUpdater = helpers.UpdateTimestamp }() +func (s *SnapTestSuite) TestRequestSecurityPolicyUpdateBinary(c *C) { // if one of the binaries needs updating, the part needs updating bin := Binary{Name: "echo", SecurityDefinitions: SecurityDefinitions{SecurityTemplate: "foo"}} - part := &SnapPart{m: &packageYaml{Name: "part", Binaries: []Binary{bin}, Version: "42"}, origin: testOrigin} - err := part.RequestAppArmorUpdate(nil, map[string]bool{"foo": true}) - c.Assert(err, IsNil) - c.Assert(updated, HasLen, 1) - c.Check(filepath.Base(updated[0]), Equals, "part."+testOrigin+"_echo_42.json") + part := &SnapPart{m: &packageYaml{Name: "part", Binaries: []Binary{bin}, Version: "42"}, origin: testOrigin, basedir: filepath.Join(dirs.SnapAppsDir, "part."+testOrigin, "42")} + err := part.RequestSecurityPolicyUpdate(nil, map[string]bool{"foo": true}) + c.Check(err, NotNil) // XXX: we should do better than this } -func (s *SnapTestSuite) TestRequestAppArmorUpdateNothing(c *C) { - var updated []string - timestampUpdater = func(s string) error { - updated = append(updated, s) - return nil - } - defer func() { timestampUpdater = helpers.UpdateTimestamp }() +func (s *SnapTestSuite) TestRequestSecurityPolicyUpdateNothing(c *C) { svc := ServiceYaml{Name: "svc", SecurityDefinitions: SecurityDefinitions{SecurityTemplate: "foo"}} bin := Binary{Name: "echo", SecurityDefinitions: SecurityDefinitions{SecurityTemplate: "foo"}} part := &SnapPart{m: &packageYaml{ServiceYamls: []ServiceYaml{svc}, Binaries: []Binary{bin}, Version: "42"}, origin: testOrigin} - err := part.RequestAppArmorUpdate(nil, nil) + err := part.RequestSecurityPolicyUpdate(nil, nil) c.Check(err, IsNil) - c.Check(updated, HasLen, 0) } func (s *SnapTestSuite) TestDetectIllegalYamlBinaries(c *C) { @@ -1559,47 +1529,6 @@ func (s *SnapTestSuite) TestIntegrateConfig(c *C) { c.Check(m.Integration["snappy-config"], DeepEquals, clickAppHook{"apparmor": "meta/snappy-config.apparmor"}) } -func (s *SnapTestSuite) TestIntegrateBinary(c *C) { - m := &packageYaml{ - Binaries: []Binary{ - { - Name: "testme", - Exec: "bin/testme", - }, - { - Name: "testme-override", - Exec: "bin/testme-override", - SecurityDefinitions: SecurityDefinitions{ - SecurityOverride: &SecurityOverrideDefinition{Apparmor: "meta/testme-override.apparmor"}, - }, - }, - { - Name: "testme-policy", - Exec: "bin/testme-policy", - SecurityDefinitions: SecurityDefinitions{ - SecurityPolicy: &SecurityPolicyDefinition{Apparmor: "meta/testme-policy.profile"}, - }, - }, - }, - } - m.legacyIntegration(false) - - c.Check(m.Integration, DeepEquals, map[string]clickAppHook{ - "testme": { - "apparmor": "meta/testme.apparmor", - "bin-path": "bin/testme", - }, - "testme-override": { - "apparmor": "meta/testme-override.apparmor", - "bin-path": "bin/testme-override", - }, - "testme-policy": { - "apparmor-profile": "meta/testme-policy.profile", - "bin-path": "bin/testme-policy", - }, - }) -} - func (s *SnapTestSuite) TestIntegrateService(c *C) { m := &packageYaml{ ServiceYamls: []ServiceYaml{ @@ -1610,12 +1539,6 @@ func (s *SnapTestSuite) TestIntegrateService(c *C) { } m.legacyIntegration(false) - - // no binaries, no service, no integrate - c.Check(m.Integration, DeepEquals, map[string]clickAppHook{ - "svc": clickAppHook{ - "apparmor": "meta/svc.apparmor", - }}) } func (s *SnapTestSuite) TestCpiURLDependsOnEnviron(c *C) { diff --git a/snappy/systemimage.go b/snappy/systemimage.go index f373d7aba8..ad3f981e7d 100644 --- a/snappy/systemimage.go +++ b/snappy/systemimage.go @@ -301,14 +301,6 @@ func (s *SystemImagePart) NeedsReboot() bool { return false } -// MarkBootSuccessful marks the *currently* booted rootfs as "good" -// (it booted :) -// Note: Not part of the Part interface. -func (s *SystemImagePart) MarkBootSuccessful() (err error) { - - return s.partition.MarkBootSuccessful() -} - // Channel returns the system-image-server channel used func (s *SystemImagePart) Channel() string { return s.channelName diff --git a/systemd/systemd.go b/systemd/systemd.go index f1f3580b2a..afc0ae2626 100644 --- a/systemd/systemd.go +++ b/systemd/systemd.go @@ -33,6 +33,7 @@ import ( "text/template" "time" + "github.com/ubuntu-core/snappy/arch" "github.com/ubuntu-core/snappy/helpers" "github.com/ubuntu-core/snappy/logger" ) @@ -346,7 +347,7 @@ WantedBy={{.ServiceSystemdTarget}} fmt.Sprintf("%s_%s_%s", desc.AppName, desc.ServiceName, desc.Version), servicesSystemdTarget, origin, - helpers.UbuntuArchitecture(), + arch.UbuntuArchitecture(), "%h", "", desc.SocketFileName, diff --git a/systemd/systemd_test.go b/systemd/systemd_test.go index 82632c6c13..86eee099ac 100644 --- a/systemd/systemd_test.go +++ b/systemd/systemd_test.go @@ -28,7 +28,7 @@ import ( . "gopkg.in/check.v1" - "github.com/ubuntu-core/snappy/helpers" + "github.com/ubuntu-core/snappy/arch" ) type testreporter struct { @@ -225,13 +225,13 @@ WantedBy=multi-user.target ` var ( - expectedAppService = fmt.Sprintf(expectedServiceFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target", ".mvo", "mvo", "\n", helpers.UbuntuArchitecture()) - expectedFmkService = fmt.Sprintf(expectedServiceFmt, "Before=ubuntu-snappy.frameworks.target\nAfter=ubuntu-snappy.frameworks-pre.target\nRequires=ubuntu-snappy.frameworks-pre.target", "", "", "\n", helpers.UbuntuArchitecture()) - expectedDbusService = fmt.Sprintf(expectedServiceFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target", ".mvo", "mvo", "BusName=foo.bar.baz\nType=dbus", helpers.UbuntuArchitecture()) + expectedAppService = fmt.Sprintf(expectedServiceFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target", ".mvo", "mvo", "\n", arch.UbuntuArchitecture()) + expectedFmkService = fmt.Sprintf(expectedServiceFmt, "Before=ubuntu-snappy.frameworks.target\nAfter=ubuntu-snappy.frameworks-pre.target\nRequires=ubuntu-snappy.frameworks-pre.target", "", "", "\n", arch.UbuntuArchitecture()) + expectedDbusService = fmt.Sprintf(expectedServiceFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target", ".mvo", "mvo", "BusName=foo.bar.baz\nType=dbus", arch.UbuntuArchitecture()) // things that need network - expectedNetAppService = fmt.Sprintf(expectedServiceFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target\nAfter=snappy-wait4network.service\nRequires=snappy-wait4network.service", ".mvo", "mvo", "\n", helpers.UbuntuArchitecture()) - expectedNetFmkService = fmt.Sprintf(expectedServiceFmt, "Before=ubuntu-snappy.frameworks.target\nAfter=ubuntu-snappy.frameworks-pre.target\nRequires=ubuntu-snappy.frameworks-pre.target\nAfter=snappy-wait4network.service\nRequires=snappy-wait4network.service", "", "", "\n", helpers.UbuntuArchitecture()) + expectedNetAppService = fmt.Sprintf(expectedServiceFmt, "After=ubuntu-snappy.frameworks.target\nRequires=ubuntu-snappy.frameworks.target\nAfter=snappy-wait4network.service\nRequires=snappy-wait4network.service", ".mvo", "mvo", "\n", arch.UbuntuArchitecture()) + expectedNetFmkService = fmt.Sprintf(expectedServiceFmt, "Before=ubuntu-snappy.frameworks.target\nAfter=ubuntu-snappy.frameworks-pre.target\nRequires=ubuntu-snappy.frameworks-pre.target\nAfter=snappy-wait4network.service\nRequires=snappy-wait4network.service", "", "", "\n", arch.UbuntuArchitecture()) ) func (s *SystemdTestSuite) TestGenAppServiceFile(c *C) { diff --git a/testutil/checkers.go b/testutil/checkers.go index f15e27a057..8967d402b6 100644 --- a/testutil/checkers.go +++ b/testutil/checkers.go @@ -21,19 +21,20 @@ package testutil import ( "fmt" - "gopkg.in/check.v1" "reflect" "strings" + + "gopkg.in/check.v1" ) type containsChecker struct { *check.CheckerInfo } -// Contains is a Checker that looks for a needle in a haystack. -// The needle can be any object. The haystack can be an array, slice or string. +// Contains is a Checker that looks for a elem in a container. +// The elem can be any object. The container can be an array, slice or string. var Contains check.Checker = &containsChecker{ - &check.CheckerInfo{Name: "Contains", Params: []string{"haystack", "needle"}}, + &check.CheckerInfo{Name: "Contains", Params: []string{"container", "elem"}}, } func (c *containsChecker) Check(params []interface{}, names []string) (result bool, error string) { @@ -43,41 +44,42 @@ func (c *containsChecker) Check(params []interface{}, names []string) (result bo error = fmt.Sprint(v) } }() - var haystack interface{} = params[0] - var needle interface{} = params[1] - switch haystackV := reflect.ValueOf(haystack); haystackV.Kind() { - case reflect.Slice, reflect.Array: - // Ensure that type of elements in haystack is compatible with needle - if needleV := reflect.ValueOf(needle); haystackV.Type().Elem() != needleV.Type() { - panic(fmt.Sprintf("haystack contains items of type %s but needle is a %s", - haystackV.Type().Elem(), needleV.Type())) + var container interface{} = params[0] + var elem interface{} = params[1] + // Ensure that type of elements in container is compatible with elem + switch containerV := reflect.ValueOf(container); containerV.Kind() { + case reflect.Slice, reflect.Array, reflect.Map: + if elemV := reflect.ValueOf(elem); containerV.Type().Elem() != elemV.Type() { + return false, fmt.Sprintf( + "container has items of type %s but expected element is a %s", + containerV.Type().Elem(), elemV.Type()) } - for len, i := haystackV.Len(), 0; i < len; i++ { - itemV := haystackV.Index(i) - if itemV.Interface() == needle { + } + switch containerV := reflect.ValueOf(container); containerV.Kind() { + case reflect.Slice, reflect.Array: + for length, i := containerV.Len(), 0; i < length; i++ { + itemV := containerV.Index(i) + if itemV.Interface() == elem { return true, "" } } return false, "" case reflect.Map: - // Ensure that type of elements in haystack is compatible with needle - if needleV := reflect.ValueOf(needle); haystackV.Type().Elem() != needleV.Type() { - panic(fmt.Sprintf("haystack contains items of type %s but needle is a %s", - haystackV.Type().Elem(), needleV.Type())) - } - for _, keyV := range haystackV.MapKeys() { - itemV := haystackV.MapIndex(keyV) - if itemV.Interface() == needle { + for _, keyV := range containerV.MapKeys() { + itemV := containerV.MapIndex(keyV) + if itemV.Interface() == elem { return true, "" } } return false, "" case reflect.String: - // When haystack is a string, we expect needle to be a string as well - needle := params[1].(string) - haystack := params[0].(string) - return strings.Contains(haystack, needle), "" + // When container is a string, we expect elem to be a string as well + elemV := reflect.ValueOf(elem) + if elemV.Kind() != reflect.String { + return false, fmt.Sprintf("element is a %T but expected a string", elem) + } + return strings.Contains(containerV.String(), elemV.String()), "" default: - panic(fmt.Sprintf("haystack is of unsupported type %T", params[0])) + return false, fmt.Sprintf("%T is not a supported container", container) } } diff --git a/testutil/checkers_test.go b/testutil/checkers_test.go index 44ba562e42..8a018a0ed6 100644 --- a/testutil/checkers_test.go +++ b/testutil/checkers_test.go @@ -20,52 +20,100 @@ package testutil import ( - . "gopkg.in/check.v1" + "reflect" "testing" + + . "gopkg.in/check.v1" ) func Test2(t *testing.T) { TestingT(t) } -type CheckersSuite struct{} +type CheckersS struct{} + +var _ = Suite(&CheckersS{}) + +func testInfo(c *C, checker Checker, name string, paramNames []string) { + info := checker.Info() + if info.Name != name { + c.Fatalf("Got name %s, expected %s", info.Name, name) + } + if !reflect.DeepEqual(info.Params, paramNames) { + c.Fatalf("Got param names %#v, expected %#v", info.Params, paramNames) + } +} -var _ = Suite(&CheckersSuite{}) +func testCheck(c *C, checker Checker, result bool, error string, params ...interface{}) ([]interface{}, []string) { + info := checker.Info() + if len(params) != len(info.Params) { + c.Fatalf("unexpected param count in test; expected %d got %d", len(info.Params), len(params)) + } + names := append([]string{}, info.Params...) + resultActual, errorActual := checker.Check(params, names) + if resultActual != result || errorActual != error { + c.Fatalf("%s.Check(%#v) returned (%#v, %#v) rather than (%#v, %#v)", + info.Name, params, resultActual, errorActual, result, error) + } + return params, names +} -func (s *CheckersSuite) TestUnsupportedTypes(c *C) { - c.ExpectFailure("haystack is of unsupported type int") - c.Assert(5, Contains, "foo") +func (s *CheckersS) TestUnsupportedTypes(c *C) { + testInfo(c, Contains, "Contains", []string{"container", "elem"}) + testCheck(c, Contains, false, "int is not a supported container", 5, nil) + testCheck(c, Contains, false, "bool is not a supported container", false, nil) + testCheck(c, Contains, false, "element is a int but expected a string", "container", 1) } -func (s *CheckersSuite) TestContainsVerifiesTypes(c *C) { - c.ExpectFailure("haystack contains items of type int but needle is a string") - c.Assert([...]int{1, 2, 3}, Contains, "foo") - c.Assert([]int{1, 2, 3}, Contains, "foo") +func (s *CheckersS) TestContainsVerifiesTypes(c *C) { + testInfo(c, Contains, "Contains", []string{"container", "elem"}) + testCheck(c, Contains, + false, "container has items of type int but expected element is a string", + [...]int{1, 2, 3}, "foo") + testCheck(c, Contains, + false, "container has items of type int but expected element is a string", + []int{1, 2, 3}, "foo") // This looks tricky, Contains looks at _values_, not at keys - c.Assert(map[string]int{"foo": 1, "bar": 2}, Contains, "foo") + testCheck(c, Contains, + false, "container has items of type int but expected element is a string", + map[string]int{"foo": 1, "bar": 2}, "foo") } -func (s *CheckersSuite) TestContainsString(c *C) { +func (s *CheckersS) TestContainsString(c *C) { c.Assert("foo", Contains, "f") c.Assert("foo", Contains, "fo") c.Assert("foo", Not(Contains), "foobar") } -func (s *CheckersSuite) TestContainsArray(c *C) { +type myString string + +func (s *CheckersS) TestContainsCustomString(c *C) { + c.Assert(myString("foo"), Contains, myString("f")) + c.Assert(myString("foo"), Contains, myString("fo")) + c.Assert(myString("foo"), Not(Contains), myString("foobar")) + c.Assert("foo", Contains, myString("f")) + c.Assert("foo", Contains, myString("fo")) + c.Assert("foo", Not(Contains), myString("foobar")) + c.Assert(myString("foo"), Contains, "f") + c.Assert(myString("foo"), Contains, "fo") + c.Assert(myString("foo"), Not(Contains), "foobar") +} + +func (s *CheckersS) TestContainsArray(c *C) { c.Assert([...]int{1, 2, 3}, Contains, 1) c.Assert([...]int{1, 2, 3}, Contains, 2) c.Assert([...]int{1, 2, 3}, Contains, 3) c.Assert([...]int{1, 2, 3}, Not(Contains), 4) } -func (s *CheckersSuite) TestContainsSlice(c *C) { +func (s *CheckersS) TestContainsSlice(c *C) { c.Assert([]int{1, 2, 3}, Contains, 1) c.Assert([]int{1, 2, 3}, Contains, 2) c.Assert([]int{1, 2, 3}, Contains, 3) c.Assert([]int{1, 2, 3}, Not(Contains), 4) } -func (s *CheckersSuite) TestContainsMap(c *C) { +func (s *CheckersS) TestContainsMap(c *C) { c.Assert(map[string]int{"foo": 1, "bar": 2}, Contains, 1) c.Assert(map[string]int{"foo": 1, "bar": 2}, Contains, 2) c.Assert(map[string]int{"foo": 1, "bar": 2}, Not(Contains), 3) diff --git a/snappy/timeout.go b/timeout/timeout.go index 10e7cb34b2..0c81206462 100644 --- a/snappy/timeout.go +++ b/timeout/timeout.go @@ -17,7 +17,7 @@ * */ -package snappy +package timeout import ( "encoding/json" diff --git a/snappy/timeout_test.go b/timeout/timeout_test.go index 0806fe5339..a1477a55d2 100644 --- a/snappy/timeout_test.go +++ b/timeout/timeout_test.go @@ -17,16 +17,25 @@ * */ -package snappy +package timeout import ( "encoding/json" + "testing" "time" . "gopkg.in/check.v1" ) -func (s *SnapTestSuite) TestTimeoutMarshal(c *C) { +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type TimeoutTestSuite struct { +} + +var _ = Suite(&TimeoutTestSuite{}) + +func (s *TimeoutTestSuite) TestTimeoutMarshal(c *C) { bs, err := Timeout(DefaultTimeout).MarshalJSON() c.Assert(err, IsNil) c.Check(string(bs), Equals, `"30s"`) @@ -36,13 +45,13 @@ type testT struct { T Timeout } -func (s *SnapTestSuite) TestTimeoutMarshalIndirect(c *C) { +func (s *TimeoutTestSuite) TestTimeoutMarshalIndirect(c *C) { bs, err := json.Marshal(testT{DefaultTimeout}) c.Assert(err, IsNil) c.Check(string(bs), Equals, `{"T":"30s"}`) } -func (s *SnapTestSuite) TestTimeoutUnmarshal(c *C) { +func (s *TimeoutTestSuite) TestTimeoutUnmarshal(c *C) { var t testT c.Assert(json.Unmarshal([]byte(`{"T": "17ms"}`), &t), IsNil) c.Check(t, DeepEquals, testT{T: Timeout(17 * time.Millisecond)}) |
