From: Michael Vogt Date: Fri, 13 Jan 2017 18:39:51 +0000 (+0000) Subject: Import snapd_2.21.orig.tar.xz X-Git-Tag: archive/raspbian/2.27.6-1+rpi1~1^2^2~2 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=3753df99cd23cd636b5b26d5fbbb4dc4156d9161;p=snapd.git Import snapd_2.21.orig.tar.xz [dgit import orig snapd_2.21.orig.tar.xz] --- 3753df99cd23cd636b5b26d5fbbb4dc4156d9161 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1a36b2fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +share +tags +.coverage +cmd/version_generated.go +cmd/VERSION +*~ +*.swp +vendor/*/ +.spread-reuse*.yaml +po/snappy.pot + +# snap-confine bits +cmd/snap-confine/snap-confine +cmd/snap-confine/snap-discard-ns +cmd/snap-confine/snap-confine-unit-tests +cmd/snap-confine/snap-confine.apparmor +cmd/snap-confine/decode-mount-opts +*~ +.*.swp +*.o +*.a + +# manual pages +cmd/snap-confine/manpages/*.[1-9] + +# test-driver +*.log +*.trs + +# Automake +Makefile +Makefile.in +snap-confine-*.tar.gz +.deps + +# Autoconf +aclocal.m4 +autom4te.cache +compile +config.guess +config.h +config.h.in +config.status +config.sub +configure +depcomp +install-sh +missing +stamp-h1 +test-driver diff --git a/.precommit b/.precommit new file mode 100755 index 00000000..d3456d61 --- /dev/null +++ b/.precommit @@ -0,0 +1,32 @@ +#!/bin/sh +set -e +echo "$@" +# put me in the project root, call me ".precommit". +# And put this here-document in ~/.bazaar/plugins/precommit_script.py: +< + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 00000000..eb5a6973 --- /dev/null +++ b/HACKING.md @@ -0,0 +1,201 @@ +# Hacking on snapd + +Hacking on snapd is fun and straightfoward. The code is extensively +unit tested and we use the [spread](https://github.com/snapcore/spread) +integration test framework for the integration/system level tests. + +## Development + +### Setting up a GOPATH + +When working with the source of Go programs, you should define a path within +your home directory (or other workspace) which will be your `GOPATH`. `GOPATH` +is similar to Java's `CLASSPATH` or Python's `~/.local`. `GOPATH` is documented +[online](http://golang.org/pkg/go/build/) and inside the go tool itself + + go help gopath + +Various conventions exist for naming the location of your `GOPATH`, but it +should exist, and be writable by you. For example + + export GOPATH=${HOME}/work + mkdir $GOPATH + +will define and create `$HOME/work` as your local `GOPATH`. The `go` tool +itself will create three subdirectories inside your `GOPATH` when required; +`src`, `pkg` and `bin`, which hold the source of Go programs, compiled packages +and compiled binaries, respectively. + +Setting `GOPATH` correctly is critical when developing Go programs. Set and +export it as part of your login script. + +Add `$GOPATH/bin` to your `PATH`, so you can run the go programs you install: + + PATH="$PATH:$GOPATH/bin" + +### Getting the snapd sources + +The easiest way to get the source for `snapd` is to use the `go get` command. + + go get -d -v github.com/snapcore/snapd/... + +This command will checkout the source of `snapd` and inspect it for any unmet +Go package dependencies, downloading those as well. `go get` will also build +and install `snapd` and its dependencies. To also build and install `snapd` +itself into `$GOPATH/bin`, omit the `-d` flag. More details on the `go get` +flags are available using + + go help get + +At this point you will have the git local repository of the `snapd` source at +`$GOPATH/src/github.com/snapcore/snapd`. The source for any +dependent packages will also be available inside `$GOPATH`. + +### Dependencies handling + +Dependencies are handled via `govendor`. Get it via: + + go get -u github.com/kardianos/govendor + +After a fresh checkout, move to the snapd source directory: + + cd $GOPATH/src/github.com/snapcore/snapd + +And then, run: + + govendor sync + +You can use the script `get-deps.sh` to run the two previous steps. + +If a dependency need updating + + govendor fetch github.com/path/of/dependency + +### Building + +To build, once the sources are available and `GOPATH` is set, you can just run + + go build -o /tmp/snap github.com/snapcore/snapd/cmd/snap + +to get the `snap` binary in /tmp (or without -o to get it in the current +working directory). Alternatively: + + go install github.com/snapcore/snapd/... + +to have it available in `$GOPATH/bin` + +### Contributing + +Contributions are always welcome! Please make sure that you sign the +Canonical contributor licence agreement at +http://www.ubuntu.com/legal/contributors + +Snapd can be found on Github, so in order to fork the source and contribute, +go to https://github.com/snapcore/snapd. Check out [Github's help +pages](https://help.github.com/) to find out how to set up your local branch, +commit changes and create pull requests. + +We value good tests, so when you fix a bug or add a new feature we highly +encourage you to create a test in `$source_test.go`. See also the section +about Testing. + +### Testing + +To run the various tests that we have to ensure a high quality source just run: + + ./run-checks + +This will check if the source format is consistent, that it builds, all tests +work as expected and that "go vet" has nothing to complain. + +You can run individual test for a sub-package by changing into that directory and: + + go test -check.f $testname + +If a test hangs, you can enable verbose mode: + + go test -v -check.vv + +(or -check.v for less verbose output). + +There is more to read about the testing framework on the [website](https://labix.org/gocheck) + +### Running the spread tests + +To run the spread tests locally you need the latest version of spread +from https://github.com/snapcore/spread. It can be installed via: + + $ sudo apt install qemu-kvm autopkgtest + $ sudo snap install --devmode spread + +Then setup the environment via: + + $ mkdir -p .spread/qemu + $ cd .spread/qemu + # For xenial (same works for yakkety/zesty) + $ adt-buildvm-ubuntu-cloud -r xenial + $ mv adt-xenial-amd64-cloud.img ubuntu-16.04.img + # For trusty + $ adt-buildvm-ubuntu-cloud -r trusty --post-command='sudo apt-get install -y --install-recommends linux-generic-lts-xenial && update-grub' + $ mv adt-trusty-amd64-cloud.img ubuntu-14.04-64.img + + +And you can run the tests via: + + $ spread -v qemu: + +For quick reuse you can use: + + $ spread -reuse qemu: + +It will print how to reuse the systems. Make sure to use +`export REUSE_PROJECT=1` in your environment too. + + +### Testing snapd + +To test the `snapd` REST API daemon on a snappy system you need to +transfer it to the snappy system and then run: + + sudo systemctl stop snapd.service snapd.socket + sudo /lib/systemd/systemd-activate -E SNAPD_DEBUG=3 -E SNAPD_DEBUG_HTTP=3 -l /run/snapd.socket -l /run/snapd-snap.socket ./snapd + +or with systemd version >= 230 + + sudo systemctl stop snapd.service snapd.socket + sudo systemd-socket-activate -E SNAPD_DEBUG=3 -E SNAPD_DEBUG_HTTP=3 -l /run/snapd.socket -l /run/snapd-snap.socket ./snapd + +This will stop the installed snapd and activate the new one. Once it's +printed out something like `Listening on /run/snapd.socket as 3.` you +should then + + sudo chmod 0666 /run/snapd*.socket + +so the socket has the right permissions (otherwise you need `sudo` to +connect). + +To debug interaction with the snap store, you can set `SNAP_DEBUG_HTTP`. +It is a bitfield: dump requests: 1, dump responses: 2, dump bodies: 4. + +# Quick intro to hacking on snap-confine + +Hey, welcome to the nice, low-level world of snap-confine + +## Building the code locally + +To get started from a pristine tree you want to do this: + +``` +./mkversion.sh +cd cmd/ +autoreconf -i -f +./configure --prefix=/usr --libexecdir=/usr/lib/snapd --enable-nvidia-ubuntu +``` + +This will drop makefiles and let you build stuff. You may find the `make hack` +target, available in `cmd/snap-confine` handy, it installs the locally built +version on your system and reloads the apparmor profile. + +## Submitting patches + +Please run `make fmt` before sending your patches. diff --git a/README.md b/README.md new file mode 100644 index 00000000..bc3e8a50 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +[![Build Status][travis-image]][travis-url] +[![Go Report Card][goreportcard-image]][goreportcard-url] + +# snapd + +The snapd and snap tools enable systems to work with .snap files. See +[snapcraft.io](http://snapcraft.io) for a high level overview about +snap files and the snapd application. + +## Development + +To get started with development off the snapd code itself, please check +out [HACKING.md](https://github.com/snapcore/snapd/blob/master/HACKING.md) +for in-depth details. + +## Reporting bugs + +If you have found an issue with the application, please [file a bug](https://bugs.launchpad.net/snappy/+filebug) on the [bugs list on Launchpad](https://bugs.launchpad.net/snappy/). + +## Get in touch + +We're friendly! Talk to us on [IRC](https://webchat.freenode.net/?channels=snappy) +or on [our mailing list](https://lists.snapcraft.io/mailman/listinfo/snapcraft). + +Get news and stay up to date on [Twitter](https://twitter.com/snapcraftio), +[Google+](https://plus.google.com/+SnapcraftIo) or +[Facebook](https://www.facebook.com/snapcraftio). + + + +[travis-image]: https://travis-ci.org/snapcore/snapd.svg?branch=master +[travis-url]: https://travis-ci.org/snapcore/snapd + +[goreportcard-image]: https://goreportcard.com/badge/github.com/snapcore/snapd +[goreportcard-url]: https://goreportcard.com/report/github.com/snapcore/snapd + +[coveralls-image]: https://coveralls.io/repos/snapcore/snapd/badge.svg?branch=master&service=github +[coveralls-url]: https://coveralls.io/github/snapcore/snapd?branch=master diff --git a/arch/arch.go b/arch/arch.go new file mode 100644 index 00000000..33a792eb --- /dev/null +++ b/arch/arch.go @@ -0,0 +1,85 @@ +// -*- 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 . + * + */ + +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{ + // go ubuntu + "386": "i386", + "amd64": "amd64", + "arm": "armhf", + "arm64": "arm64", + "ppc64le": "ppc64el", + "s390x": "s390x", + "ppc": "powerpc", + } + + 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 00000000..01fec0db --- /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 . + * + */ + +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("ppc64le"), 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/account.go b/asserts/account.go new file mode 100644 index 00000000..e7e6ba35 --- /dev/null +++ b/asserts/account.go @@ -0,0 +1,102 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts + +import ( + "fmt" + "time" +) + +var ( + accountValidationCertified = "certified" +) + +// Account holds an account assertion, which ties a name for an account +// to its identifier and provides the authority's confidence in the name's validity. +type Account struct { + assertionBase + certified bool + timestamp time.Time +} + +// AccountID returns the account-id of the account. +func (acc *Account) AccountID() string { + return acc.HeaderString("account-id") +} + +// Username returns the user name for the account. +func (acc *Account) Username() string { + return acc.HeaderString("username") +} + +// DisplayName returns the human-friendly name for the account. +func (acc *Account) DisplayName() string { + return acc.HeaderString("display-name") +} + +// IsCertified returns true if the authority has confidence in the account's name. +func (acc *Account) IsCertified() bool { + return acc.certified +} + +// Timestamp returns the time when the account was issued. +func (acc *Account) Timestamp() time.Time { + return acc.timestamp +} + +// Implement further consistency checks. +func (acc *Account) checkConsistency(db RODatabase, acck *AccountKey) error { + if !db.IsTrustedAccount(acc.AuthorityID()) { + return fmt.Errorf("account assertion for %q is not signed by a directly trusted authority: %s", acc.AccountID(), acc.AuthorityID()) + } + return nil +} + +// sanity +var _ consistencyChecker = (*Account)(nil) + +func assembleAccount(assert assertionBase) (Assertion, error) { + _, err := checkNotEmptyString(assert.headers, "display-name") + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "validation") + if err != nil { + return nil, err + } + certified := assert.headers["validation"] == accountValidationCertified + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + _, err = checkOptionalString(assert.headers, "username") + if err != nil { + return nil, err + } + + return &Account{ + assertionBase: assert, + certified: certified, + timestamp: timestamp, + }, nil +} diff --git a/asserts/account_key.go b/asserts/account_key.go new file mode 100644 index 00000000..a6320ff7 --- /dev/null +++ b/asserts/account_key.go @@ -0,0 +1,288 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts + +import ( + "fmt" + "regexp" + "time" +) + +var validAccountKeyName = regexp.MustCompile(`^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$`) + +// AccountKey holds an account-key assertion, asserting a public key +// belonging to the account. +type AccountKey struct { + assertionBase + since time.Time + until time.Time + pubKey PublicKey +} + +// AccountID returns the account-id of this account-key. +func (ak *AccountKey) AccountID() string { + return ak.HeaderString("account-id") +} + +// Name returns the name of the account key. +func (ak *AccountKey) Name() string { + return ak.HeaderString("name") +} + +func IsValidAccountKeyName(name string) bool { + return validAccountKeyName.MatchString(name) +} + +// Since returns the time when the account key starts being valid. +func (ak *AccountKey) Since() time.Time { + return ak.since +} + +// Until returns the time when the account key stops being valid. A zero time means the key is valid forever. +func (ak *AccountKey) Until() time.Time { + return ak.until +} + +// PublicKeyID returns the key id used for lookup of the account key. +func (ak *AccountKey) PublicKeyID() string { + return ak.pubKey.ID() +} + +// isKeyValidAt returns whether the account key is valid at 'when' time. +func (ak *AccountKey) isKeyValidAt(when time.Time) bool { + valid := when.After(ak.since) || when.Equal(ak.since) + if valid && !ak.until.IsZero() { + valid = when.Before(ak.until) + } + return valid +} + +// publicKey returns the underlying public key of the account key. +func (ak *AccountKey) publicKey() PublicKey { + return ak.pubKey +} + +func checkPublicKey(ab *assertionBase, keyIDName string) (PublicKey, error) { + pubKey, err := DecodePublicKey(ab.Body()) + if err != nil { + return nil, err + } + keyID, err := checkNotEmptyString(ab.headers, keyIDName) + if err != nil { + return nil, err + } + if keyID != pubKey.ID() { + return nil, fmt.Errorf("public key does not match provided key id") + } + return pubKey, nil +} + +// Implement further consistency checks. +func (ak *AccountKey) checkConsistency(db RODatabase, acck *AccountKey) error { + if !db.IsTrustedAccount(ak.AuthorityID()) { + return fmt.Errorf("account-key assertion for %q is not signed by a directly trusted authority: %s", ak.AccountID(), ak.AuthorityID()) + } + _, err := db.Find(AccountType, map[string]string{ + "account-id": ak.AccountID(), + }) + if err == ErrNotFound { + return fmt.Errorf("account-key assertion for %q does not have a matching account assertion", ak.AccountID()) + } + if err != nil { + return err + } + // XXX: Make this unconditional once account-key assertions are required to have a name. + if ak.Name() != "" { + // Check that we don't end up with multiple keys with + // different IDs but the same account-id and name. + // Note that this is a non-transactional check-then-add, so + // is not a hard guarantee. Backstores that can implement a + // unique constraint should do so. + assertions, err := db.FindMany(AccountKeyType, map[string]string{ + "account-id": ak.AccountID(), + "name": ak.Name(), + }) + if err != nil && err != ErrNotFound { + return err + } + for _, assertion := range assertions { + existingAccKey := assertion.(*AccountKey) + if ak.PublicKeyID() != existingAccKey.PublicKeyID() { + return fmt.Errorf("account-key assertion for %q with ID %q has the same name %q as existing ID %q", ak.AccountID(), ak.PublicKeyID(), ak.Name(), existingAccKey.PublicKeyID()) + } + } + } + return nil +} + +// sanity +var _ consistencyChecker = (*AccountKey)(nil) + +// Prerequisites returns references to this account-key's prerequisite assertions. +func (ak *AccountKey) Prerequisites() []*Ref { + return []*Ref{ + {Type: AccountType, PrimaryKey: []string{ak.AccountID()}}, + } +} + +func assembleAccountKey(assert assertionBase) (Assertion, error) { + _, err := checkNotEmptyString(assert.headers, "account-id") + if err != nil { + return nil, err + } + + // XXX: We should require name to be present after backfilling existing assertions. + _, ok := assert.headers["name"] + if ok { + _, err = checkStringMatches(assert.headers, "name", validAccountKeyName) + if err != nil { + return nil, err + } + } + + since, err := checkRFC3339Date(assert.headers, "since") + if err != nil { + return nil, err + } + + until, err := checkRFC3339DateWithDefault(assert.headers, "until", time.Time{}) + if err != nil { + return nil, err + } + if !until.IsZero() && until.Before(since) { + return nil, fmt.Errorf("'until' time cannot be before 'since' time") + } + + pubk, err := checkPublicKey(&assert, "public-key-sha3-384") + if err != nil { + return nil, err + } + + // ignore extra headers for future compatibility + return &AccountKey{ + assertionBase: assert, + since: since, + until: until, + pubKey: pubk, + }, nil +} + +// AccountKeyRequest holds an account-key-request assertion, which is a self-signed request to prove that the requester holds the private key and wishes to create an account-key assertion for it. +type AccountKeyRequest struct { + assertionBase + since time.Time + until time.Time + pubKey PublicKey +} + +// AccountID returns the account-id of this account-key-request. +func (akr *AccountKeyRequest) AccountID() string { + return akr.HeaderString("account-id") +} + +// Name returns the name of the account key. +func (akr *AccountKeyRequest) Name() string { + return akr.HeaderString("name") +} + +// Since returns the time when the requested account key starts being valid. +func (akr *AccountKeyRequest) Since() time.Time { + return akr.since +} + +// Until returns the time when the requested account key stops being valid. A zero time means the key is valid forever. +func (akr *AccountKeyRequest) Until() time.Time { + return akr.until +} + +// PublicKeyID returns the underlying public key ID of the requested account key. +func (akr *AccountKeyRequest) PublicKeyID() string { + return akr.pubKey.ID() +} + +// signKey returns the underlying public key of the requested account key. +func (akr *AccountKeyRequest) signKey() PublicKey { + return akr.pubKey +} + +// Implement further consistency checks. +func (akr *AccountKeyRequest) checkConsistency(db RODatabase, acck *AccountKey) error { + _, err := db.Find(AccountType, map[string]string{ + "account-id": akr.AccountID(), + }) + if err == ErrNotFound { + return fmt.Errorf("account-key-request assertion for %q does not have a matching account assertion", akr.AccountID()) + } + if err != nil { + return err + } + return nil +} + +// sanity +var ( + _ consistencyChecker = (*AccountKeyRequest)(nil) + _ customSigner = (*AccountKeyRequest)(nil) +) + +// Prerequisites returns references to this account-key-request's prerequisite assertions. +func (akr *AccountKeyRequest) Prerequisites() []*Ref { + return []*Ref{ + {Type: AccountType, PrimaryKey: []string{akr.AccountID()}}, + } +} + +func assembleAccountKeyRequest(assert assertionBase) (Assertion, error) { + _, err := checkNotEmptyString(assert.headers, "account-id") + if err != nil { + return nil, err + } + + _, err = checkStringMatches(assert.headers, "name", validAccountKeyName) + if err != nil { + return nil, err + } + + since, err := checkRFC3339Date(assert.headers, "since") + if err != nil { + return nil, err + } + + until, err := checkRFC3339DateWithDefault(assert.headers, "until", time.Time{}) + if err != nil { + return nil, err + } + if !until.IsZero() && until.Before(since) { + return nil, fmt.Errorf("'until' time cannot be before 'since' time") + } + + pubk, err := checkPublicKey(&assert, "public-key-sha3-384") + if err != nil { + return nil, err + } + + // ignore extra headers for future compatibility + return &AccountKeyRequest{ + assertionBase: assert, + since: since, + until: until, + pubKey: pubk, + }, nil +} diff --git a/asserts/account_key_test.go b/asserts/account_key_test.go new file mode 100644 index 00000000..d1fd153b --- /dev/null +++ b/asserts/account_key_test.go @@ -0,0 +1,809 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts_test + +import ( + "encoding/base64" + "fmt" + "path/filepath" + "strings" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +type accountKeySuite struct { + privKey asserts.PrivateKey + pubKeyBody string + keyID string + since, until time.Time + sinceLine, untilLine string +} + +var _ = Suite(&accountKeySuite{}) + +func (aks *accountKeySuite) SetUpSuite(c *C) { + cfg1 := &asserts.DatabaseConfig{} + accDb, err := asserts.OpenDatabase(cfg1) + c.Assert(err, IsNil) + aks.privKey = testPrivKey1 + err = accDb.ImportKey(aks.privKey) + c.Assert(err, IsNil) + aks.keyID = aks.privKey.PublicKey().ID() + + pubKey, err := accDb.PublicKey(aks.keyID) + c.Assert(err, IsNil) + pubKeyEncoded, err := asserts.EncodePublicKey(pubKey) + c.Assert(err, IsNil) + aks.pubKeyBody = string(pubKeyEncoded) + + aks.since, err = time.Parse(time.RFC822, "16 Nov 15 15:04 UTC") + c.Assert(err, IsNil) + aks.until = aks.since.AddDate(1, 0, 0) + aks.sinceLine = "since: " + aks.since.Format(time.RFC3339) + "\n" + aks.untilLine = "until: " + aks.until.Format(time.RFC3339) + "\n" +} + +func (aks *accountKeySuite) TestDecodeOK(c *C) { + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.AccountKeyType) + accKey := a.(*asserts.AccountKey) + c.Check(accKey.AccountID(), Equals, "acc-id1") + c.Check(accKey.Name(), Equals, "default") + c.Check(accKey.PublicKeyID(), Equals, aks.keyID) + c.Check(accKey.Since(), Equals, aks.since) +} + +func (aks *accountKeySuite) TestDecodeNoName(c *C) { + // XXX: remove this test once name is mandatory + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.AccountKeyType) + accKey := a.(*asserts.AccountKey) + c.Check(accKey.AccountID(), Equals, "acc-id1") + c.Check(accKey.Name(), Equals, "") + c.Check(accKey.PublicKeyID(), Equals, aks.keyID) + c.Check(accKey.Since(), Equals, aks.since) +} + +func (aks *accountKeySuite) TestUntil(c *C) { + + untilSinceLine := "until: " + aks.since.Format(time.RFC3339) + "\n" + + tests := []struct { + untilLine string + until time.Time + }{ + {"", time.Time{}}, // zero time default + {aks.untilLine, aks.until}, // in the future + {untilSinceLine, aks.since}, // same as since + } + + for _, test := range tests { + c.Log(test) + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + test.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "openpgp c2ln" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + accKey := a.(*asserts.AccountKey) + c.Check(accKey.Until(), Equals, test.until) + } +} + +const ( + accKeyErrPrefix = "assertion account-key: " + accKeyReqErrPrefix = "assertion account-key-request: " +) + +func (aks *accountKeySuite) TestDecodeInvalidHeaders(c *C) { + + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + + untilPast := aks.since.AddDate(-1, 0, 0) + untilPastLine := "until: " + untilPast.Format(time.RFC3339) + "\n" + + invalidHeaderTests := []struct{ original, invalid, expectedErr string }{ + {"account-id: acc-id1\n", "", `"account-id" header is mandatory`}, + {"account-id: acc-id1\n", "account-id: \n", `"account-id" header should not be empty`}, + // XXX: enable this once name is mandatory + // {"name: default\n", "", `"name" header is mandatory`}, + {"name: default\n", "name: \n", `"name" header should not be empty`}, + {"name: default\n", "name: a b\n", `"name" header contains invalid characters: "a b"`}, + {"name: default\n", "name: -default\n", `"name" header contains invalid characters: "-default"`}, + {"name: default\n", "name: foo:bar\n", `"name" header contains invalid characters: "foo:bar"`}, + {"name: default\n", "name: a--b\n", `"name" header contains invalid characters: "a--b"`}, + {"name: default\n", "name: 42\n", `"name" header contains invalid characters: "42"`}, + {"public-key-sha3-384: " + aks.keyID + "\n", "", `"public-key-sha3-384" header is mandatory`}, + {"public-key-sha3-384: " + aks.keyID + "\n", "public-key-sha3-384: \n", `"public-key-sha3-384" header should not be empty`}, + {aks.sinceLine, "", `"since" header is mandatory`}, + {aks.sinceLine, "since: \n", `"since" header should not be empty`}, + {aks.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`}, + {aks.sinceLine, "since: \n", `"since" header should not be empty`}, + {aks.untilLine, "until: \n", `"until" header is not a RFC3339 date: .*`}, + {aks.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`}, + {aks.untilLine, untilPastLine, `'until' time cannot be before 'since' time`}, + } + + for _, test := range invalidHeaderTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accKeyErrPrefix+test.expectedErr) + } +} + +func (aks *accountKeySuite) TestDecodeInvalidPublicKey(c *C) { + headers := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + raw, err := base64.StdEncoding.DecodeString(aks.pubKeyBody) + c.Assert(err, IsNil) + spurious := base64.StdEncoding.EncodeToString(append(raw, "gorp"...)) + + invalidPublicKeyTests := []struct{ body, expectedErr string }{ + {"", "cannot decode public key: no data"}, + {"==", "cannot decode public key: .*"}, + {"stuff", "cannot decode public key: .*"}, + {"AnNpZw==", "unsupported public key format version: 2"}, + {"AUJST0tFTg==", "cannot decode public key: .*"}, + {spurious, "public key has spurious trailing data"}, + } + + for _, test := range invalidPublicKeyTests { + invalid := headers + + fmt.Sprintf("body-length: %v", len(test.body)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + test.body + "\n\n" + + "AXNpZw==" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accKeyErrPrefix+test.expectedErr) + } +} + +func (aks *accountKeySuite) TestDecodeKeyIDMismatch(c *C) { + invalid := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: aa\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accKeyErrPrefix+"public key does not match provided key id") +} + +func (aks *accountKeySuite) openDB(c *C) *asserts.Database { + trustedKey := testPrivKey0 + + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + cfg := &asserts.DatabaseConfig{ + Backstore: bs, + Trusted: []asserts.Assertion{ + asserts.BootstrapAccountForTest("canonical"), + asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey()), + }, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + return db +} + +func (aks *accountKeySuite) prereqAccount(c *C, db *asserts.Database) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "display-name": "Acct1", + "account-id": "acc-id1", + "username": "acc-id1", + "validation": "unproven", + "timestamp": aks.since.Format(time.RFC3339), + } + acct1, err := asserts.AssembleAndSignInTest(asserts.AccountType, headers, nil, trustedKey) + c.Assert(err, IsNil) + + // prereq + db.Add(acct1) +} + +func (aks *accountKeySuite) TestAccountKeyCheck(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + + aks.prereqAccount(c, db) + + err = db.Check(accKey) + c.Assert(err, IsNil) +} + +func (aks *accountKeySuite) TestAccountKeyCheckNoAccount(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + + err = db.Check(accKey) + c.Assert(err, ErrorMatches, `account-key assertion for "acc-id1" does not have a matching account assertion`) +} + +func (aks *accountKeySuite) TestAccountKeyCheckUntrustedAuthority(c *C) { + trustedKey := testPrivKey0 + + db := aks.openDB(c) + storeDB := assertstest.NewSigningDB("canonical", trustedKey) + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := map[string]interface{}{ + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := otherDB.Sign(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), "") + c.Assert(err, IsNil) + + err = db.Check(accKey) + c.Assert(err, ErrorMatches, `account-key assertion for "acc-id1" is not signed by a directly trusted authority:.*`) +} + +func (aks *accountKeySuite) TestAccountKeyCheckSameNameAndNewRevision(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + aks.prereqAccount(c, db) + + err = db.Add(accKey) + c.Assert(err, IsNil) + + headers["revision"] = "1" + newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + err = db.Check(newAccKey) + c.Assert(err, IsNil) +} + +func (aks *accountKeySuite) TestAccountKeyCheckSameAccountAndDifferentName(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + aks.prereqAccount(c, db) + + err = db.Add(accKey) + c.Assert(err, IsNil) + + newPrivKey, _ := assertstest.GenerateKey(752) + err = db.ImportKey(newPrivKey) + c.Assert(err, IsNil) + newPubKey, err := db.PublicKey(newPrivKey.PublicKey().ID()) + c.Assert(err, IsNil) + newPubKeyEncoded, err := asserts.EncodePublicKey(newPubKey) + c.Assert(err, IsNil) + + headers["name"] = "another" + headers["public-key-sha3-384"] = newPubKey.ID() + newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, newPubKeyEncoded, trustedKey) + c.Assert(err, IsNil) + + err = db.Check(newAccKey) + c.Assert(err, IsNil) +} + +func (aks *accountKeySuite) TestAccountKeyCheckSameNameAndDifferentAccount(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + err = db.ImportKey(trustedKey) + c.Assert(err, IsNil) + aks.prereqAccount(c, db) + + err = db.Add(accKey) + c.Assert(err, IsNil) + + newPrivKey, _ := assertstest.GenerateKey(752) + err = db.ImportKey(newPrivKey) + c.Assert(err, IsNil) + newPubKey, err := db.PublicKey(newPrivKey.PublicKey().ID()) + c.Assert(err, IsNil) + newPubKeyEncoded, err := asserts.EncodePublicKey(newPubKey) + c.Assert(err, IsNil) + + acct2 := assertstest.NewAccount(db, "acc-id2", map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id2", + }, trustedKey.PublicKey().ID()) + db.Add(acct2) + + headers["account-id"] = "acc-id2" + headers["public-key-sha3-384"] = newPubKey.ID() + headers["revision"] = "1" + newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, newPubKeyEncoded, trustedKey) + c.Assert(err, IsNil) + + err = db.Check(newAccKey) + c.Assert(err, IsNil) +} + +func (aks *accountKeySuite) TestAccountKeyCheckNameClash(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + aks.prereqAccount(c, db) + + err = db.Add(accKey) + c.Assert(err, IsNil) + + newPrivKey, _ := assertstest.GenerateKey(752) + err = db.ImportKey(newPrivKey) + c.Assert(err, IsNil) + newPubKey, err := db.PublicKey(newPrivKey.PublicKey().ID()) + c.Assert(err, IsNil) + newPubKeyEncoded, err := asserts.EncodePublicKey(newPubKey) + c.Assert(err, IsNil) + + headers["public-key-sha3-384"] = newPubKey.ID() + headers["revision"] = "1" + newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, newPubKeyEncoded, trustedKey) + c.Assert(err, IsNil) + + err = db.Check(newAccKey) + c.Assert(err, ErrorMatches, fmt.Sprintf(`account-key assertion for "acc-id1" with ID %q has the same name "default" as existing ID %q`, newPubKey.ID(), aks.keyID)) +} + +func (aks *accountKeySuite) TestAccountKeyAddAndFind(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + + aks.prereqAccount(c, db) + + err = db.Add(accKey) + c.Assert(err, IsNil) + + found, err := db.Find(asserts.AccountKeyType, map[string]string{ + "account-id": "acc-id1", + "public-key-sha3-384": aks.keyID, + }) + c.Assert(err, IsNil) + c.Assert(found, NotNil) + c.Check(found.Body(), DeepEquals, []byte(aks.pubKeyBody)) +} + +func (aks *accountKeySuite) TestPublicKeyIsValidAt(c *C) { + // With since and until, i.e. signing account-key expires. + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + accKey := a.(*asserts.AccountKey) + + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since), Equals, true) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, -1)), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, 1)), Equals, true) + + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, -1, 0)), Equals, true) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, 1, 0)), Equals, false) + + // With no until, i.e. signing account-key never expires. + encoded = "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "openpgp c2ln" + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + accKey = a.(*asserts.AccountKey) + + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since), Equals, true) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, -1)), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, 1)), Equals, true) + + // With since == until, i.e. signing account-key has been revoked. + encoded = "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + "until: " + aks.since.Format(time.RFC3339) + "\n" + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "openpgp c2ln" + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + accKey = a.(*asserts.AccountKey) + + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, -1)), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, 1)), Equals, false) + + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, -1, 0)), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, 1, 0)), Equals, false) +} + +func (aks *accountKeySuite) TestPrerequisites(c *C) { + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + prereqs := a.Prerequisites() + c.Assert(prereqs, HasLen, 1) + c.Check(prereqs[0], DeepEquals, &asserts.Ref{ + Type: asserts.AccountType, + PrimaryKey: []string{"acc-id1"}, + }) +} + +func (aks *accountKeySuite) TestAccountKeyRequestHappy(c *C) { + akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, + map[string]interface{}{ + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + }, []byte(aks.pubKeyBody), aks.privKey) + c.Assert(err, IsNil) + + // roundtrip + a, err := asserts.Decode(asserts.Encode(akr)) + c.Assert(err, IsNil) + + akr2, ok := a.(*asserts.AccountKeyRequest) + c.Assert(ok, Equals, true) + + db := aks.openDB(c) + aks.prereqAccount(c, db) + + err = db.Check(akr2) + c.Check(err, IsNil) + + c.Check(akr2.AccountID(), Equals, "acc-id1") + c.Check(akr2.Name(), Equals, "default") + c.Check(akr2.PublicKeyID(), Equals, aks.keyID) + c.Check(akr2.Since(), Equals, aks.since) +} + +func (aks *accountKeySuite) TestAccountKeyRequestUntil(c *C) { + db := aks.openDB(c) + aks.prereqAccount(c, db) + + tests := []struct { + untilHeader string + until time.Time + }{ + {"", time.Time{}}, // zero time default + {aks.until.Format(time.RFC3339), aks.until}, // in the future + {aks.since.Format(time.RFC3339), aks.since}, // same as since + } + + for _, test := range tests { + c.Log(test) + headers := map[string]interface{}{ + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + } + if test.untilHeader != "" { + headers["until"] = test.untilHeader + } + akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, headers, []byte(aks.pubKeyBody), aks.privKey) + c.Assert(err, IsNil) + a, err := asserts.Decode(asserts.Encode(akr)) + c.Assert(err, IsNil) + akr2 := a.(*asserts.AccountKeyRequest) + c.Check(akr2.Until(), Equals, test.until) + err = db.Check(akr2) + c.Check(err, IsNil) + } +} + +func (aks *accountKeySuite) TestAccountKeyRequestAddAndFind(c *C) { + akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, + map[string]interface{}{ + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + }, []byte(aks.pubKeyBody), aks.privKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + aks.prereqAccount(c, db) + + err = db.Add(akr) + c.Assert(err, IsNil) + + found, err := db.Find(asserts.AccountKeyRequestType, map[string]string{ + "account-id": "acc-id1", + "public-key-sha3-384": aks.keyID, + }) + c.Assert(err, IsNil) + c.Assert(found, NotNil) + c.Check(found.Body(), DeepEquals, []byte(aks.pubKeyBody)) +} + +func (aks *accountKeySuite) TestAccountKeyRequestDecodeInvalid(c *C) { + encoded := "type: account-key-request\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: " + aks.privKey.PublicKey().ID() + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + + untilPast := aks.since.AddDate(-1, 0, 0) + untilPastLine := "until: " + untilPast.Format(time.RFC3339) + "\n" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"account-id: acc-id1\n", "", `"account-id" header is mandatory`}, + {"account-id: acc-id1\n", "account-id: \n", `"account-id" header should not be empty`}, + {"name: default\n", "", `"name" header is mandatory`}, + {"name: default\n", "name: \n", `"name" header should not be empty`}, + {"name: default\n", "name: a b\n", `"name" header contains invalid characters: "a b"`}, + {"name: default\n", "name: -default\n", `"name" header contains invalid characters: "-default"`}, + {"name: default\n", "name: foo:bar\n", `"name" header contains invalid characters: "foo:bar"`}, + {"public-key-sha3-384: " + aks.keyID + "\n", "", `"public-key-sha3-384" header is mandatory`}, + {"public-key-sha3-384: " + aks.keyID + "\n", "public-key-sha3-384: \n", `"public-key-sha3-384" header should not be empty`}, + {aks.sinceLine, "", `"since" header is mandatory`}, + {aks.sinceLine, "since: \n", `"since" header should not be empty`}, + {aks.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`}, + {aks.sinceLine, "since: \n", `"since" header should not be empty`}, + {aks.untilLine, "until: \n", `"until" header is not a RFC3339 date: .*`}, + {aks.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`}, + {aks.untilLine, untilPastLine, `'until' time cannot be before 'since' time`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accKeyReqErrPrefix+test.expectedErr) + } +} + +func (aks *accountKeySuite) TestAccountKeyRequestDecodeInvalidPublicKey(c *C) { + headers := "type: account-key-request\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + raw, err := base64.StdEncoding.DecodeString(aks.pubKeyBody) + c.Assert(err, IsNil) + spurious := base64.StdEncoding.EncodeToString(append(raw, "gorp"...)) + + invalidPublicKeyTests := []struct{ body, expectedErr string }{ + {"", "cannot decode public key: no data"}, + {"==", "cannot decode public key: .*"}, + {"stuff", "cannot decode public key: .*"}, + {"AnNpZw==", "unsupported public key format version: 2"}, + {"AUJST0tFTg==", "cannot decode public key: .*"}, + {spurious, "public key has spurious trailing data"}, + } + + for _, test := range invalidPublicKeyTests { + invalid := headers + + fmt.Sprintf("body-length: %v", len(test.body)) + "\n" + + "sign-key-sha3-384: " + aks.privKey.PublicKey().ID() + "\n\n" + + test.body + "\n\n" + + "AXNpZw==" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accKeyReqErrPrefix+test.expectedErr) + } +} + +func (aks *accountKeySuite) TestAccountKeyRequestDecodeKeyIDMismatch(c *C) { + invalid := "type: account-key-request\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: aa\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: " + aks.privKey.PublicKey().ID() + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, "assertion account-key-request: public key does not match provided key id") +} + +func (aks *accountKeySuite) TestAccountKeyRequestNoAccount(c *C) { + headers := map[string]interface{}{ + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + } + akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, headers, []byte(aks.pubKeyBody), aks.privKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + + err = db.Check(akr) + c.Assert(err, ErrorMatches, `account-key-request assertion for "acc-id1" does not have a matching account assertion`) +} diff --git a/asserts/account_test.go b/asserts/account_test.go new file mode 100644 index 00000000..e3d4b3b4 --- /dev/null +++ b/asserts/account_test.go @@ -0,0 +1,167 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts_test + +import ( + "fmt" + "strings" + "time" + + "github.com/snapcore/snapd/asserts" + . "gopkg.in/check.v1" +) + +var ( + _ = Suite(&accountSuite{}) +) + +type accountSuite struct { + ts time.Time + tsLine string +} + +func (s *accountSuite) SetUpSuite(c *C) { + s.ts = time.Now().Truncate(time.Second).UTC() + s.tsLine = "timestamp: " + s.ts.Format(time.RFC3339) + "\n" +} + +const accountExample = "type: account\n" + + "authority-id: canonical\n" + + "account-id: abc-123\n" + + "display-name: Nice User\n" + + "username: nice\n" + + "validation: certified\n" + + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + +func (s *accountSuite) TestDecodeOK(c *C) { + encoded := strings.Replace(accountExample, "TSLINE", s.tsLine, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.AccountType) + account := a.(*asserts.Account) + c.Check(account.AuthorityID(), Equals, "canonical") + c.Check(account.Timestamp(), Equals, s.ts) + c.Check(account.AccountID(), Equals, "abc-123") + c.Check(account.DisplayName(), Equals, "Nice User") + c.Check(account.Username(), Equals, "nice") + c.Check(account.IsCertified(), Equals, true) +} + +func (s *accountSuite) TestOptional(c *C) { + encoded := strings.Replace(accountExample, "TSLINE", s.tsLine, 1) + + tests := []struct{ original, replacement string }{ + {"username: nice\n", ""}, + {"username: nice\n", "username: \n"}, + } + + for _, test := range tests { + valid := strings.Replace(encoded, test.original, test.replacement, 1) + _, err := asserts.Decode([]byte(valid)) + c.Check(err, IsNil) + } +} + +func (s *accountSuite) TestIsCertified(c *C) { + tests := []struct { + value string + isCertified bool + }{ + {"certified", true}, + {"unproven", false}, + {"nonsense", false}, + } + + template := strings.Replace(accountExample, "TSLINE", s.tsLine, 1) + for _, test := range tests { + encoded := strings.Replace( + template, + "validation: certified\n", + fmt.Sprintf("validation: %s\n", test.value), + 1, + ) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + account := assert.(*asserts.Account) + c.Check(account.IsCertified(), Equals, test.isCertified) + } +} + +const ( + accountErrPrefix = "assertion account: " +) + +func (s *accountSuite) TestDecodeInvalid(c *C) { + encoded := strings.Replace(accountExample, "TSLINE", s.tsLine, 1) + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"account-id: abc-123\n", "", `"account-id" header is mandatory`}, + {"account-id: abc-123\n", "account-id: \n", `"account-id" header should not be empty`}, + {"display-name: Nice User\n", "", `"display-name" header is mandatory`}, + {"display-name: Nice User\n", "display-name: \n", `"display-name" header should not be empty`}, + {"username: nice\n", "username:\n - foo\n - bar\n", `"username" header must be a string`}, + {"validation: certified\n", "", `"validation" header is mandatory`}, + {"validation: certified\n", "validation: \n", `"validation" header should not be empty`}, + {s.tsLine, "", `"timestamp" header is mandatory`}, + {s.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {s.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accountErrPrefix+test.expectedErr) + } +} + +func (s *accountSuite) TestCheckInconsistentTimestamp(c *C) { + ex, err := asserts.Decode([]byte(strings.Replace(accountExample, "TSLINE", s.tsLine, 1))) + c.Assert(err, IsNil) + + storeDB, db := makeStoreAndCheckDB(c) + + headers := ex.Headers() + headers["timestamp"] = "2011-01-01T14:00:00Z" + account, err := storeDB.Sign(asserts.AccountType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(account) + c.Assert(err, ErrorMatches, "account assertion timestamp outside of signing key validity") +} + +func (s *accountSuite) TestCheckUntrustedAuthority(c *C) { + ex, err := asserts.Decode([]byte(strings.Replace(accountExample, "TSLINE", s.tsLine, 1))) + c.Assert(err, IsNil) + + storeDB, db := makeStoreAndCheckDB(c) + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := ex.Headers() + headers["timestamp"] = time.Now().Format(time.RFC3339) + account, err := otherDB.Sign(asserts.AccountType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(account) + c.Assert(err, ErrorMatches, `account assertion for "abc-123" is not signed by a directly trusted authority:.*`) +} diff --git a/asserts/asserts.go b/asserts/asserts.go new file mode 100644 index 00000000..981dcab0 --- /dev/null +++ b/asserts/asserts.go @@ -0,0 +1,877 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts + +import ( + "bufio" + "bytes" + "crypto" + "fmt" + "io" + "sort" + "strconv" + "strings" + "unicode/utf8" +) + +type typeFlags int + +const ( + noAuthority typeFlags = iota + 1 +) + +// AssertionType describes a known assertion type with its name and metadata. +type AssertionType struct { + // Name of the type. + Name string + // PrimaryKey holds the names of the headers that constitute the + // unique primary key for this assertion type. + PrimaryKey []string + + assembler func(assert assertionBase) (Assertion, error) + flags typeFlags +} + +// MaxSupportedFormat returns the maximum supported format iteration for the type. +func (at *AssertionType) MaxSupportedFormat() int { + return maxSupportedFormat[at.Name] +} + +// Understood assertion types. +var ( + AccountType = &AssertionType{"account", []string{"account-id"}, assembleAccount, 0} + AccountKeyType = &AssertionType{"account-key", []string{"public-key-sha3-384"}, assembleAccountKey, 0} + ModelType = &AssertionType{"model", []string{"series", "brand-id", "model"}, assembleModel, 0} + SerialType = &AssertionType{"serial", []string{"brand-id", "model", "serial"}, assembleSerial, 0} + BaseDeclarationType = &AssertionType{"base-declaration", []string{"series"}, assembleBaseDeclaration, 0} + SnapDeclarationType = &AssertionType{"snap-declaration", []string{"series", "snap-id"}, assembleSnapDeclaration, 0} + SnapBuildType = &AssertionType{"snap-build", []string{"snap-sha3-384"}, assembleSnapBuild, 0} + SnapRevisionType = &AssertionType{"snap-revision", []string{"snap-sha3-384"}, assembleSnapRevision, 0} + SystemUserType = &AssertionType{"system-user", []string{"brand-id", "email"}, assembleSystemUser, 0} + ValidationType = &AssertionType{"validation", []string{"series", "snap-id", "approved-snap-id", "approved-snap-revision"}, assembleValidation, 0} + +// ... +) + +// Assertion types without a definite authority set (on the wire and/or self-signed). +var ( + DeviceSessionRequestType = &AssertionType{"device-session-request", []string{"brand-id", "model", "serial"}, assembleDeviceSessionRequest, noAuthority} + SerialRequestType = &AssertionType{"serial-request", nil, assembleSerialRequest, noAuthority} + AccountKeyRequestType = &AssertionType{"account-key-request", []string{"public-key-sha3-384"}, assembleAccountKeyRequest, noAuthority} +) + +var typeRegistry = map[string]*AssertionType{ + AccountType.Name: AccountType, + AccountKeyType.Name: AccountKeyType, + ModelType.Name: ModelType, + SerialType.Name: SerialType, + BaseDeclarationType.Name: BaseDeclarationType, + SnapDeclarationType.Name: SnapDeclarationType, + SnapBuildType.Name: SnapBuildType, + SnapRevisionType.Name: SnapRevisionType, + SystemUserType.Name: SystemUserType, + ValidationType.Name: ValidationType, + // no authority + DeviceSessionRequestType.Name: DeviceSessionRequestType, + SerialRequestType.Name: SerialRequestType, + AccountKeyRequestType.Name: AccountKeyRequestType, +} + +var maxSupportedFormat = map[string]int{} + +func init() { + // register maxSupportedFormats while breaking initialisation loop + maxSupportedFormat[SnapDeclarationType.Name] = 1 +} + +func MockMaxSupportedFormat(assertType *AssertionType, maxFormat int) (restore func()) { + prev := maxSupportedFormat[assertType.Name] + maxSupportedFormat[assertType.Name] = maxFormat + return func() { + maxSupportedFormat[assertType.Name] = prev + } +} + +// Type returns the AssertionType with name or nil +func Type(name string) *AssertionType { + return typeRegistry[name] +} + +// Ref expresses a reference to an assertion. +type Ref struct { + Type *AssertionType + PrimaryKey []string +} + +func (ref *Ref) String() string { + pkStr := "-" + n := len(ref.Type.PrimaryKey) + if n != len(ref.PrimaryKey) { + pkStr = "???" + } else if n > 0 { + pkStr = ref.PrimaryKey[n-1] + if n > 1 { + sfx := []string{pkStr + ";"} + for i, k := range ref.Type.PrimaryKey[:n-1] { + sfx = append(sfx, fmt.Sprintf("%s:%s", k, ref.PrimaryKey[i])) + } + pkStr = strings.Join(sfx, " ") + } + } + return fmt.Sprintf("%s (%s)", ref.Type.Name, pkStr) +} + +// Unique returns a unique string representing the reference that can be used as a key in maps. +func (ref *Ref) Unique() string { + return fmt.Sprintf("%s/%s", ref.Type.Name, strings.Join(ref.PrimaryKey, "/")) +} + +// Resolve resolves the reference using the given find function. +func (ref *Ref) Resolve(find func(assertType *AssertionType, headers map[string]string) (Assertion, error)) (Assertion, error) { + if len(ref.PrimaryKey) != len(ref.Type.PrimaryKey) { + return nil, fmt.Errorf("%q assertion reference primary key has the wrong length (expected %v): %v", ref.Type.Name, ref.Type.PrimaryKey, ref.PrimaryKey) + } + headers := make(map[string]string, len(ref.PrimaryKey)) + for i, name := range ref.Type.PrimaryKey { + headers[name] = ref.PrimaryKey[i] + } + return find(ref.Type, headers) +} + +// Assertion represents an assertion through its general elements. +type Assertion interface { + // Type returns the type of this assertion + Type() *AssertionType + // Format returns the format iteration of this assertion + Format() int + // SupportedFormat returns whether the assertion uses a supported + // format iteration. If false the assertion might have been only + // partially parsed. + SupportedFormat() bool + // Revision returns the revision of this assertion + Revision() int + // AuthorityID returns the authority that signed this assertion + AuthorityID() string + + // Header retrieves the header with name + Header(name string) interface{} + + // Headers returns the complete headers + Headers() map[string]interface{} + + // HeaderString retrieves the string value of header with name or "" + HeaderString(name string) string + + // Body returns the body of this assertion + Body() []byte + + // Signature returns the signed content and its unprocessed signature + Signature() (content, signature []byte) + + // SignKeyID returns the key id for the key that signed this assertion. + SignKeyID() string + + // Prerequisites returns references to the prerequisite assertions for the validity of this one. + Prerequisites() []*Ref + + // Ref returns a reference representing this assertion. + Ref() *Ref +} + +// customSigner represents an assertion with special arrangements for its signing key (e.g. self-signed), rather than the usual case where an assertion is signed by its authority. +type customSigner interface { + // signKey returns the public key material for the key that signed this assertion. See also SignKeyID. + signKey() PublicKey +} + +// MediaType is the media type for encoded assertions on the wire. +const MediaType = "application/x.ubuntu.assertion" + +// assertionBase is the concrete base to hold representation data for actual assertions. +type assertionBase struct { + headers map[string]interface{} + body []byte + // parsed format iteration + format int + // parsed revision + revision int + // preserved content + content []byte + // unprocessed signature + signature []byte +} + +// HeaderString retrieves the string value of header with name or "" +func (ab *assertionBase) HeaderString(name string) string { + s, _ := ab.headers[name].(string) + return s +} + +// Type returns the assertion type. +func (ab *assertionBase) Type() *AssertionType { + return Type(ab.HeaderString("type")) +} + +// Format returns the assertion format iteration. +func (ab *assertionBase) Format() int { + return ab.format +} + +// SupportedFormat returns whether the assertion uses a supported +// format iteration. If false the assertion might have been only +// partially parsed. +func (ab *assertionBase) SupportedFormat() bool { + return ab.format <= maxSupportedFormat[ab.HeaderString("type")] +} + +// Revision returns the assertion revision. +func (ab *assertionBase) Revision() int { + return ab.revision +} + +// AuthorityID returns the authority-id a.k.a the signer id of the assertion. +func (ab *assertionBase) AuthorityID() string { + return ab.HeaderString("authority-id") +} + +// Header returns the value of an header by name. +func (ab *assertionBase) Header(name string) interface{} { + v := ab.headers[name] + if v == nil { + return nil + } + return copyHeader(v) +} + +// Headers returns the complete headers. +func (ab *assertionBase) Headers() map[string]interface{} { + return copyHeaders(ab.headers) +} + +// Body returns the body of the assertion. +func (ab *assertionBase) Body() []byte { + return ab.body +} + +// Signature returns the signed content and its unprocessed signature. +func (ab *assertionBase) Signature() (content, signature []byte) { + return ab.content, ab.signature +} + +// SignKeyID returns the key id for the key that signed this assertion. +func (ab *assertionBase) SignKeyID() string { + return ab.HeaderString("sign-key-sha3-384") +} + +// Prerequisites returns references to the prerequisite assertions for the validity of this one. +func (ab *assertionBase) Prerequisites() []*Ref { + return nil +} + +// Ref returns a reference representing this assertion. +func (ab *assertionBase) Ref() *Ref { + assertType := ab.Type() + primKey := make([]string, len(assertType.PrimaryKey)) + for i, name := range assertType.PrimaryKey { + primKey[i] = ab.HeaderString(name) + } + return &Ref{ + Type: assertType, + PrimaryKey: primKey, + } +} + +// sanity check +var _ Assertion = (*assertionBase)(nil) + +// Decode parses a serialized assertion. +// +// The expected serialisation format looks like: +// +// HEADER ("\n\n" BODY?)? "\n\n" SIGNATURE +// +// where: +// +// HEADER is a set of header entries separated by "\n" +// BODY can be arbitrary, +// SIGNATURE is the signature +// +// A header entry for a single line value (no '\n' in it) looks like: +// +// NAME ": " SIMPLEVALUE +// +// The format supports multiline text values (with '\n's in them) and +// lists possibly nested with string scalars in them. +// +// For those a header entry looks like: +// +// NAME ":\n" MULTI(baseindent) +// +// where MULTI can be +// +// * (baseindent + 4)-space indented value (multiline text) +// * entries of a list each of the form: +// +// " "*baseindent " -" ( " " SIMPLEVALUE | "\n" MULTI ) +// +// baseindent starts at 0 and then grows with nesting matching the +// previous level introduction (the " "*baseindent " -" bit) +// length minus 1. +// +// In general the following headers are mandatory: +// +// type +// authority-id (except for on the wire/self-signed assertions like serial-request) +// +// Further for a given assertion type all the primary key headers +// must be non empty and must not contain '/'. +// +// The following headers expect string representing integer values and +// if omitted otherwise are assumed to be 0: +// +// revision (a positive int) +// body-length (expected to be equal to the length of BODY) +// +// Times are expected to be in the RFC3339 format: "2006-01-02T15:04:05Z07:00". +func Decode(serializedAssertion []byte) (Assertion, error) { + // copy to get an independent backstorage that can't be mutated later + assertionSnapshot := make([]byte, len(serializedAssertion)) + copy(assertionSnapshot, serializedAssertion) + contentSignatureSplit := bytes.LastIndex(assertionSnapshot, nlnl) + if contentSignatureSplit == -1 { + return nil, fmt.Errorf("assertion content/signature separator not found") + } + content := assertionSnapshot[:contentSignatureSplit] + signature := assertionSnapshot[contentSignatureSplit+2:] + + headersBodySplit := bytes.Index(content, nlnl) + var body, head []byte + if headersBodySplit == -1 { + head = content + } else { + body = content[headersBodySplit+2:] + if len(body) == 0 { + body = nil + } + head = content[:headersBodySplit] + } + + headers, err := parseHeaders(head) + if err != nil { + return nil, fmt.Errorf("parsing assertion headers: %v", err) + } + + return assemble(headers, body, content, signature) +} + +// Maximum assertion component sizes. +const ( + MaxBodySize = 2 * 1024 * 1024 + MaxHeadersSize = 128 * 1024 + MaxSignatureSize = 128 * 1024 +) + +// Decoder parses a stream of assertions bundled by separating them with double newlines. +type Decoder struct { + rd io.Reader + initialBufSize int + b *bufio.Reader + err error + maxHeadersSize int + maxBodySize int + maxSigSize int +} + +// initBuffer finishes a Decoder initialization by setting up the bufio.Reader, +// it returns the *Decoder for convenience of notation. +func (d *Decoder) initBuffer() *Decoder { + d.b = bufio.NewReaderSize(d.rd, d.initialBufSize) + return d +} + +const defaultDecoderButSize = 4096 + +// NewDecoder returns a Decoder to parse the stream of assertions from the reader. +func NewDecoder(r io.Reader) *Decoder { + return (&Decoder{ + rd: r, + initialBufSize: defaultDecoderButSize, + maxHeadersSize: MaxHeadersSize, + maxBodySize: MaxBodySize, + maxSigSize: MaxSignatureSize, + }).initBuffer() +} + +func (d *Decoder) peek(size int) ([]byte, error) { + buf, err := d.b.Peek(size) + if err == bufio.ErrBufferFull { + rebuf, reerr := d.b.Peek(d.b.Buffered()) + if reerr != nil { + panic(reerr) + } + mr := io.MultiReader(bytes.NewBuffer(rebuf), d.rd) + d.b = bufio.NewReaderSize(mr, (size/d.initialBufSize+1)*d.initialBufSize) + buf, err = d.b.Peek(size) + } + if err != nil && d.err == nil { + d.err = err + } + return buf, d.err +} + +// NB: readExact and readUntil use peek underneath and their returned +// buffers are valid only until the next reading call + +func (d *Decoder) readExact(size int) ([]byte, error) { + buf, err := d.peek(size) + d.b.Discard(len(buf)) + if len(buf) == size { + return buf, nil + } + if err == io.EOF { + return buf, io.ErrUnexpectedEOF + } + return buf, err +} + +func (d *Decoder) readUntil(delim []byte, maxSize int) ([]byte, error) { + last := 0 + size := d.initialBufSize + for { + buf, err := d.peek(size) + if i := bytes.Index(buf[last:], delim); i >= 0 { + d.b.Discard(last + i + len(delim)) + return buf[:last+i+len(delim)], nil + } + // report errors only once we have consumed what is buffered + if err != nil && len(buf) == d.b.Buffered() { + d.b.Discard(len(buf)) + return buf, err + } + last = size - len(delim) + 1 + size *= 2 + if size > maxSize { + return nil, fmt.Errorf("maximum size exceeded while looking for delimiter %q", delim) + } + } +} + +// Decode parses the next assertion from the stream. +// It returns the error io.EOF at the end of a well-formed stream. +func (d *Decoder) Decode() (Assertion, error) { + // read the headers and the nlnl separator after them + headAndSep, err := d.readUntil(nlnl, d.maxHeadersSize) + if err != nil { + if err == io.EOF { + if len(headAndSep) != 0 { + return nil, io.ErrUnexpectedEOF + } + return nil, io.EOF + } + return nil, fmt.Errorf("error reading assertion headers: %v", err) + } + + headLen := len(headAndSep) - len(nlnl) + headers, err := parseHeaders(headAndSep[:headLen]) + if err != nil { + return nil, fmt.Errorf("parsing assertion headers: %v", err) + } + + length, err := checkIntWithDefault(headers, "body-length", 0) + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + if length > d.maxBodySize { + return nil, fmt.Errorf("assertion body length %d exceeds maximum body size", length) + } + + // save the headers before we try to read more, and setup to capture + // the whole content in a buffer + contentBuf := bytes.NewBuffer(make([]byte, 0, len(headAndSep)+length)) + contentBuf.Write(headAndSep) + + if length > 0 { + // read the body if length != 0 + body, err := d.readExact(length) + if err != nil { + return nil, err + } + contentBuf.Write(body) + } + + // try to read the end of body a.k.a content/signature separator + endOfBody, err := d.readUntil(nlnl, d.maxSigSize) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("error reading assertion trailer: %v", err) + } + + var sig []byte + if bytes.Equal(endOfBody, nlnl) { + // we got the nlnl content/signature separator, read the signature now and the assertion/assertion nlnl separation + sig, err = d.readUntil(nlnl, d.maxSigSize) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("error reading assertion signature: %v", err) + } + } else { + // we got the signature directly which is a ok format only if body length == 0 + if length > 0 { + return nil, fmt.Errorf("missing content/signature separator") + } + sig = endOfBody + contentBuf.Truncate(headLen) + } + + // normalize sig ending newlines + if bytes.HasSuffix(sig, nlnl) { + sig = sig[:len(sig)-1] + } + + finalContent := contentBuf.Bytes() + var finalBody []byte + if length > 0 { + finalBody = finalContent[headLen+len(nlnl):] + } + + finalSig := make([]byte, len(sig)) + copy(finalSig, sig) + + return assemble(headers, finalBody, finalContent, finalSig) +} + +func checkIteration(headers map[string]interface{}, name string) (int, error) { + iternum, err := checkIntWithDefault(headers, name, 0) + if err != nil { + return -1, err + } + if iternum < 0 { + return -1, fmt.Errorf("%s should be positive: %v", name, iternum) + } + return iternum, nil +} + +func checkFormat(headers map[string]interface{}) (int, error) { + return checkIteration(headers, "format") +} + +func checkRevision(headers map[string]interface{}) (int, error) { + return checkIteration(headers, "revision") +} + +// Assemble assembles an assertion from its components. +func Assemble(headers map[string]interface{}, body, content, signature []byte) (Assertion, error) { + err := checkHeaders(headers) + if err != nil { + return nil, err + } + return assemble(headers, body, content, signature) +} + +// assemble is the internal variant of Assemble, assumes headers are already checked for supported types +func assemble(headers map[string]interface{}, body, content, signature []byte) (Assertion, error) { + length, err := checkIntWithDefault(headers, "body-length", 0) + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + if length != len(body) { + return nil, fmt.Errorf("assertion body length and declared body-length don't match: %v != %v", len(body), length) + } + + if !utf8.Valid(body) { + return nil, fmt.Errorf("body is not utf8") + } + + if _, err := checkDigest(headers, "sign-key-sha3-384", crypto.SHA3_384); err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + + typ, err := checkNotEmptyString(headers, "type") + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + assertType := Type(typ) + if assertType == nil { + return nil, fmt.Errorf("unknown assertion type: %q", typ) + } + + if assertType.flags&noAuthority == 0 { + if _, err := checkNotEmptyString(headers, "authority-id"); err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + } else { + _, ok := headers["authority-id"] + if ok { + return nil, fmt.Errorf("%q assertion cannot have authority-id set", assertType.Name) + } + } + + formatnum, err := checkFormat(headers) + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + + for _, primKey := range assertType.PrimaryKey { + if _, err := checkPrimaryKey(headers, primKey); err != nil { + return nil, fmt.Errorf("assertion %s: %v", assertType.Name, err) + } + } + + revision, err := checkRevision(headers) + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + + if len(signature) == 0 { + return nil, fmt.Errorf("empty assertion signature") + } + + assert, err := assertType.assembler(assertionBase{ + headers: headers, + body: body, + format: formatnum, + revision: revision, + content: content, + signature: signature, + }) + if err != nil { + return nil, fmt.Errorf("assertion %s: %v", assertType.Name, err) + } + return assert, nil +} + +func writeHeader(buf *bytes.Buffer, headers map[string]interface{}, name string) { + appendEntry(buf, fmt.Sprintf("%s:", name), headers[name], 0) +} + +func assembleAndSign(assertType *AssertionType, headers map[string]interface{}, body []byte, privKey PrivateKey) (Assertion, error) { + err := checkAssertType(assertType) + if err != nil { + return nil, err + } + + withAuthority := assertType.flags&noAuthority == 0 + + err = checkHeaders(headers) + if err != nil { + return nil, err + } + + // there's no hint at all that we will need non-textual bodies, + // make sure we actually enforce that + if !utf8.Valid(body) { + return nil, fmt.Errorf("assertion body is not utf8") + } + + finalHeaders := copyHeaders(headers) + bodyLength := len(body) + finalBody := make([]byte, bodyLength) + copy(finalBody, body) + finalHeaders["type"] = assertType.Name + finalHeaders["body-length"] = strconv.Itoa(bodyLength) + finalHeaders["sign-key-sha3-384"] = privKey.PublicKey().ID() + + if withAuthority { + if _, err := checkNotEmptyString(finalHeaders, "authority-id"); err != nil { + return nil, err + } + } else { + _, ok := finalHeaders["authority-id"] + if ok { + return nil, fmt.Errorf("%q assertion cannot have authority-id set", assertType.Name) + } + } + + formatnum, err := checkFormat(finalHeaders) + if err != nil { + return nil, err + } + + if formatnum > assertType.MaxSupportedFormat() { + return nil, fmt.Errorf("cannot sign %q assertion with format %d higher than max supported format %d", assertType.Name, formatnum, assertType.MaxSupportedFormat()) + } + + revision, err := checkRevision(finalHeaders) + if err != nil { + return nil, err + } + + buf := bytes.NewBufferString("type: ") + buf.WriteString(assertType.Name) + + if formatnum > 0 { + writeHeader(buf, finalHeaders, "format") + } else { + delete(finalHeaders, "format") + } + + if withAuthority { + writeHeader(buf, finalHeaders, "authority-id") + } + + if revision > 0 { + writeHeader(buf, finalHeaders, "revision") + } else { + delete(finalHeaders, "revision") + } + written := map[string]bool{ + "type": true, + "format": true, + "authority-id": true, + "revision": true, + "body-length": true, + "sign-key-sha3-384": true, + } + for _, primKey := range assertType.PrimaryKey { + if _, err := checkPrimaryKey(finalHeaders, primKey); err != nil { + return nil, err + } + writeHeader(buf, finalHeaders, primKey) + written[primKey] = true + } + + // emit other headers in lexicographic order + otherKeys := make([]string, 0, len(finalHeaders)) + for name := range finalHeaders { + if !written[name] { + otherKeys = append(otherKeys, name) + } + } + sort.Strings(otherKeys) + for _, k := range otherKeys { + writeHeader(buf, finalHeaders, k) + } + + // body-length and body + if bodyLength > 0 { + writeHeader(buf, finalHeaders, "body-length") + } else { + delete(finalHeaders, "body-length") + } + + // signing key reference + writeHeader(buf, finalHeaders, "sign-key-sha3-384") + + if bodyLength > 0 { + buf.Grow(bodyLength + 2) + buf.Write(nlnl) + buf.Write(finalBody) + } else { + finalBody = nil + } + content := buf.Bytes() + + signature, err := signContent(content, privKey) + if err != nil { + return nil, fmt.Errorf("cannot sign assertion: %v", err) + } + // be 'cat' friendly, add a ignored newline to the signature which is the last part of the encoded assertion + signature = append(signature, '\n') + + assert, err := assertType.assembler(assertionBase{ + headers: finalHeaders, + body: finalBody, + format: formatnum, + revision: revision, + content: content, + signature: signature, + }) + if err != nil { + return nil, fmt.Errorf("cannot assemble assertion %s: %v", assertType.Name, err) + } + return assert, nil +} + +// SignWithoutAuthority assembles an assertion without a set authority with the provided information and signs it with the given private key. +func SignWithoutAuthority(assertType *AssertionType, headers map[string]interface{}, body []byte, privKey PrivateKey) (Assertion, error) { + if assertType.flags&noAuthority == 0 { + return nil, fmt.Errorf("cannot sign assertions needing a definite authority with SignWithoutAuthority") + } + return assembleAndSign(assertType, headers, body, privKey) +} + +// 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.Write(nlnl) + buf.Write(signature) + return buf.Bytes() +} + +// Encoder emits a stream of assertions bundled by separating them with double newlines. +type Encoder struct { + wr io.Writer + nextSep []byte +} + +// NewEncoder returns a Encoder to emit a stream of assertions to a writer. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{wr: w} +} + +// append emits an already encoded assertion into the stream with a proper required separator. +func (enc *Encoder) append(encoded []byte) error { + sz := len(encoded) + if sz == 0 { + return fmt.Errorf("internal error: encoded assertion cannot be empty") + } + + _, err := enc.wr.Write(enc.nextSep) + if err != nil { + return err + } + + _, err = enc.wr.Write(encoded) + if err != nil { + return err + } + + if encoded[sz-1] != '\n' { + _, err = enc.wr.Write(nl) + if err != nil { + return err + } + } + enc.nextSep = nl + + return nil +} + +// Encode emits the assertion into the stream with the required separator. +// Errors here are always about writing given that Encode() itself cannot error. +func (enc *Encoder) Encode(assert Assertion) error { + encoded := Encode(assert) + return enc.append(encoded) +} + +// SignatureCheck checks the signature of the assertion against the given public key. Useful for assertions with no authority. +func SignatureCheck(assert Assertion, pubKey PublicKey) error { + content, encodedSig := assert.Signature() + sig, err := decodeSignature(encodedSig) + if err != nil { + return err + } + err = pubKey.verify(content, sig) + if err != nil { + return fmt.Errorf("failed signature verification: %v", err) + } + return nil +} diff --git a/asserts/asserts_test.go b/asserts/asserts_test.go new file mode 100644 index 00000000..a6b3314b --- /dev/null +++ b/asserts/asserts_test.go @@ -0,0 +1,779 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts_test + +import ( + "bytes" + "io" + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type assertsSuite struct{} + +var _ = Suite(&assertsSuite{}) + +func (as *assertsSuite) TestType(c *C) { + c.Check(asserts.Type("test-only"), Equals, asserts.TestOnlyType) +} + +func (as *assertsSuite) TestUnknown(c *C) { + c.Check(asserts.Type(""), IsNil) + c.Check(asserts.Type("unknown"), IsNil) +} + +func (as *assertsSuite) TestTypeMaxSupportedFormat(c *C) { + c.Check(asserts.Type("test-only").MaxSupportedFormat(), Equals, 1) +} + +func (as *assertsSuite) TestRef(c *C) { + ref := &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc", "xyz"}, + } + c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz") +} + +func (as *assertsSuite) TestRefString(c *C) { + ref := &asserts.Ref{ + Type: asserts.AccountType, + PrimaryKey: []string{"canonical"}, + } + + c.Check(ref.String(), Equals, "account (canonical)") + + ref = &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{"18", "SNAPID"}, + } + + c.Check(ref.String(), Equals, "snap-declaration (SNAPID; series:18)") + + ref = &asserts.Ref{ + Type: asserts.ModelType, + PrimaryKey: []string{"18", "BRAND", "baz-3000"}, + } + + c.Check(ref.String(), Equals, "model (baz-3000; series:18 brand-id:BRAND)") + + // broken primary key + ref = &asserts.Ref{ + Type: asserts.ModelType, + PrimaryKey: []string{"18"}, + } + c.Check(ref.String(), Equals, "model (???)") + + ref = &asserts.Ref{ + Type: asserts.TestOnlyNoAuthorityType, + } + c.Check(ref.String(), Equals, "test-only-no-authority (-)") +} + +func (as *assertsSuite) TestRefResolveError(c *C) { + ref := &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc"}, + } + _, err := ref.Resolve(nil) + c.Check(err, ErrorMatches, `"test-only-2" assertion reference primary key has the wrong length \(expected \[pk1 pk2\]\): \[abc\]`) +} + +const exKeyID = "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + +const exampleEmptyBodyAllDefaults = "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: abc\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + +func (as *assertsSuite) TestDecodeEmptyBodyAllDefaults(c *C) { + a, err := asserts.Decode([]byte(exampleEmptyBodyAllDefaults)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + _, ok := a.(*asserts.TestOnly) + c.Check(ok, Equals, true) + c.Check(a.Revision(), Equals, 0) + c.Check(a.Format(), Equals, 0) + c.Check(a.Body(), IsNil) + c.Check(a.Header("header1"), IsNil) + c.Check(a.HeaderString("header1"), Equals, "") + c.Check(a.AuthorityID(), Equals, "auth-id1") + c.Check(a.SignKeyID(), Equals, exKeyID) +} + +const exampleEmptyBody2NlNl = "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: xyz\n" + + "revision: 0\n" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "\n\n" + + "AXNpZw==\n" + +func (as *assertsSuite) TestDecodeEmptyBodyNormalize2NlNl(c *C) { + a, err := asserts.Decode([]byte(exampleEmptyBody2NlNl)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + c.Check(a.Revision(), Equals, 0) + c.Check(a.Format(), Equals, 0) + c.Check(a.Body(), IsNil) +} + +const exampleBodyAndExtraHeaders = "type: test-only\n" + + "format: 1\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==\n" + +func (as *assertsSuite) TestDecodeWithABodyAndExtraHeaders(c *C) { + a, err := asserts.Decode([]byte(exampleBodyAndExtraHeaders)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + c.Check(a.AuthorityID(), Equals, "auth-id2") + c.Check(a.SignKeyID(), Equals, exKeyID) + c.Check(a.Header("primary-key"), Equals, "abc") + c.Check(a.Revision(), Equals, 5) + c.Check(a.Format(), Equals, 1) + c.Check(a.SupportedFormat(), Equals, true) + c.Check(a.Header("header1"), Equals, "value1") + c.Check(a.Header("header2"), Equals, "value2") + c.Check(a.Body(), DeepEquals, []byte("THE-BODY")) + +} + +const exampleUnsupportedFormat = "type: test-only\n" + + "format: 77\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "AXNpZw==\n" + +func (as *assertsSuite) TestDecodeUnsupportedFormat(c *C) { + a, err := asserts.Decode([]byte(exampleUnsupportedFormat)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + c.Check(a.AuthorityID(), Equals, "auth-id2") + c.Check(a.SignKeyID(), Equals, exKeyID) + c.Check(a.Header("primary-key"), Equals, "abc") + c.Check(a.Revision(), Equals, 5) + c.Check(a.Format(), Equals, 77) + c.Check(a.SupportedFormat(), Equals, false) +} + +func (as *assertsSuite) TestDecodeGetSignatureBits(c *C) { + content := "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: xyz\n" + + "revision: 5\n" + + "header1: value1\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + encoded := content + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + c.Check(a.AuthorityID(), Equals, "auth-id1") + c.Check(a.SignKeyID(), Equals, exKeyID) + cont, signature := a.Signature() + c.Check(signature, DeepEquals, []byte("AXNpZw==")) + c.Check(cont, DeepEquals, []byte(content)) +} + +func (as *assertsSuite) TestDecodeNoSignatureSplit(c *C) { + for _, encoded := range []string{"", "foo"} { + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, ErrorMatches, "assertion content/signature separator not found") + } +} + +func (as *assertsSuite) TestDecodeHeaderParsingErrors(c *C) { + headerParsingErrorsTests := []struct{ encoded, expectedErr string }{ + {string([]byte{255, '\n', '\n'}), "header is not utf8"}, + {"foo: a\nbar\n\n", `header entry missing ':' separator: "bar"`}, + {"TYPE: foo\n\n", `invalid header name: "TYPE"`}, + {"foo: a\nbar:>\n\n", `header entry should have a space or newline \(for multiline\) before value: "bar:>"`}, + {"foo: a\nbar:\n\n", `expected 4 chars nesting prefix after multiline introduction "bar:": EOF`}, + {"foo: a\nbar:\nbaz: x\n\n", `expected 4 chars nesting prefix after multiline introduction "bar:": "baz: x"`}, + {"foo: a:\nbar: b\nfoo: x\n\n", `repeated header: "foo"`}, + } + + for _, test := range headerParsingErrorsTests { + _, err := asserts.Decode([]byte(test.encoded)) + c.Check(err, ErrorMatches, "parsing assertion headers: "+test.expectedErr) + } +} + +func (as *assertsSuite) TestDecodeInvalid(c *C) { + keyIDHdr := "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n" + encoded := "type: test-only\n" + + "format: 0\n" + + "authority-id: auth-id\n" + + "primary-key: abc\n" + + "revision: 0\n" + + "body-length: 5\n" + + keyIDHdr + + "\n" + + "abcde" + + "\n\n" + + "AXNpZw==" + + invalidAssertTests := []struct{ original, invalid, expectedErr string }{ + {"body-length: 5", "body-length: z", `assertion: "body-length" header is not an integer: z`}, + {"body-length: 5", "body-length: 3", "assertion body length and declared body-length don't match: 5 != 3"}, + {"authority-id: auth-id\n", "", `assertion: "authority-id" header is mandatory`}, + {"authority-id: auth-id\n", "authority-id: \n", `assertion: "authority-id" header should not be empty`}, + {keyIDHdr, "", `assertion: "sign-key-sha3-384" header is mandatory`}, + {keyIDHdr, "sign-key-sha3-384: \n", `assertion: "sign-key-sha3-384" header should not be empty`}, + {keyIDHdr, "sign-key-sha3-384: $\n", `assertion: "sign-key-sha3-384" header cannot be decoded: .*`}, + {keyIDHdr, "sign-key-sha3-384: eHl6\n", `assertion: "sign-key-sha3-384" header does not have the expected bit length: 24`}, + {"AXNpZw==", "", "empty assertion signature"}, + {"type: test-only\n", "", `assertion: "type" header is mandatory`}, + {"type: test-only\n", "type: unknown\n", `unknown assertion type: "unknown"`}, + {"revision: 0\n", "revision: Z\n", `assertion: "revision" header is not an integer: Z`}, + {"revision: 0\n", "revision:\n - 1\n", `assertion: "revision" header is not an integer: \[1\]`}, + {"revision: 0\n", "revision: -10\n", "assertion: revision should be positive: -10"}, + {"format: 0\n", "format: Z\n", `assertion: "format" header is not an integer: Z`}, + {"format: 0\n", "format: -10\n", "assertion: format should be positive: -10"}, + {"primary-key: abc\n", "", `assertion test-only: "primary-key" header is mandatory`}, + {"primary-key: abc\n", "primary-key:\n - abc\n", `assertion test-only: "primary-key" header must be a string`}, + {"primary-key: abc\n", "primary-key: a/c\n", `assertion test-only: "primary-key" primary key header cannot contain '/'`}, + {"abcde", "ab\xffde", "body is not utf8"}, + } + + for _, test := range invalidAssertTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, test.expectedErr) + } +} + +func (as *assertsSuite) TestDecodeNoAuthorityInvalid(c *C) { + invalid := "type: test-only-no-authority\n" + + "authority-id: auth-id1\n" + + "hdr: FOO\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "openpgp c2ln" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, `"test-only-no-authority" assertion cannot have authority-id set`) +} + +func checkContent(c *C, a asserts.Assertion, encoded string) { + expected, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + expectedCont, _ := expected.Signature() + + cont, _ := a.Signature() + c.Check(cont, DeepEquals, expectedCont) +} + +func (as *assertsSuite) TestEncoderDecoderHappy(c *C) { + stream := new(bytes.Buffer) + enc := asserts.NewEncoder(stream) + asserts.EncoderAppend(enc, []byte(exampleEmptyBody2NlNl)) + asserts.EncoderAppend(enc, []byte(exampleBodyAndExtraHeaders)) + asserts.EncoderAppend(enc, []byte(exampleEmptyBodyAllDefaults)) + + decoder := asserts.NewDecoder(stream) + a, err := decoder.Decode() + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + _, ok := a.(*asserts.TestOnly) + c.Check(ok, Equals, true) + checkContent(c, a, exampleEmptyBody2NlNl) + + a, err = decoder.Decode() + c.Assert(err, IsNil) + checkContent(c, a, exampleBodyAndExtraHeaders) + + a, err = decoder.Decode() + c.Assert(err, IsNil) + checkContent(c, a, exampleEmptyBodyAllDefaults) + + a, err = decoder.Decode() + c.Assert(err, Equals, io.EOF) +} + +func (as *assertsSuite) TestDecodeEmptyStream(c *C) { + stream := new(bytes.Buffer) + decoder := asserts.NewDecoder(stream) + _, err := decoder.Decode() + c.Check(err, Equals, io.EOF) +} + +func (as *assertsSuite) TestDecoderHappyWithSeparatorsVariations(c *C) { + streams := []string{ + exampleBodyAndExtraHeaders, + exampleEmptyBody2NlNl, + exampleEmptyBodyAllDefaults, + } + + for _, streamData := range streams { + stream := bytes.NewBufferString(streamData) + decoder := asserts.NewDecoderStressed(stream, 16, 1024, 1024, 1024) + a, err := decoder.Decode() + c.Assert(err, IsNil, Commentf("stream: %q", streamData)) + + checkContent(c, a, streamData) + + a, err = decoder.Decode() + c.Check(a, IsNil) + c.Check(err, Equals, io.EOF, Commentf("stream: %q", streamData)) + } +} + +func (as *assertsSuite) TestDecoderHappyWithTrailerDoubleNewlines(c *C) { + streams := []string{ + exampleBodyAndExtraHeaders, + exampleEmptyBody2NlNl, + exampleEmptyBodyAllDefaults, + } + + for _, streamData := range streams { + stream := bytes.NewBufferString(streamData) + if strings.HasSuffix(streamData, "\n") { + stream.WriteString("\n") + } else { + stream.WriteString("\n\n") + } + + decoder := asserts.NewDecoderStressed(stream, 16, 1024, 1024, 1024) + a, err := decoder.Decode() + c.Assert(err, IsNil, Commentf("stream: %q", streamData)) + + checkContent(c, a, streamData) + + a, err = decoder.Decode() + c.Check(a, IsNil) + c.Check(err, Equals, io.EOF, Commentf("stream: %q", streamData)) + } +} + +func (as *assertsSuite) TestDecoderUnexpectedEOF(c *C) { + streamData := exampleBodyAndExtraHeaders + "\n" + exampleEmptyBodyAllDefaults + fstHeadEnd := strings.Index(exampleBodyAndExtraHeaders, "\n\n") + sndHeadEnd := len(exampleBodyAndExtraHeaders) + 1 + strings.Index(exampleEmptyBodyAllDefaults, "\n\n") + + for _, brk := range []int{1, fstHeadEnd / 2, fstHeadEnd, fstHeadEnd + 1, fstHeadEnd + 2, fstHeadEnd + 6} { + stream := bytes.NewBufferString(streamData[:brk]) + decoder := asserts.NewDecoderStressed(stream, 16, 1024, 1024, 1024) + _, err := decoder.Decode() + c.Check(err, Equals, io.ErrUnexpectedEOF, Commentf("brk: %d", brk)) + } + + for _, brk := range []int{sndHeadEnd, sndHeadEnd + 1} { + stream := bytes.NewBufferString(streamData[:brk]) + decoder := asserts.NewDecoder(stream) + _, err := decoder.Decode() + c.Assert(err, IsNil) + + _, err = decoder.Decode() + c.Check(err, Equals, io.ErrUnexpectedEOF, Commentf("brk: %d", brk)) + } +} + +func (as *assertsSuite) TestDecoderBrokenBodySeparation(c *C) { + streamData := strings.Replace(exampleBodyAndExtraHeaders, "THE-BODY\n\n", "THE-BODY", 1) + decoder := asserts.NewDecoder(bytes.NewBufferString(streamData)) + _, err := decoder.Decode() + c.Assert(err, ErrorMatches, "missing content/signature separator") + + streamData = strings.Replace(exampleBodyAndExtraHeaders, "THE-BODY\n\n", "THE-BODY\n", 1) + decoder = asserts.NewDecoder(bytes.NewBufferString(streamData)) + _, err = decoder.Decode() + c.Assert(err, ErrorMatches, "missing content/signature separator") +} + +func (as *assertsSuite) TestDecoderHeadTooBig(c *C) { + decoder := asserts.NewDecoderStressed(bytes.NewBufferString(exampleBodyAndExtraHeaders), 4, 4, 1024, 1024) + _, err := decoder.Decode() + c.Assert(err, ErrorMatches, `error reading assertion headers: maximum size exceeded while looking for delimiter "\\n\\n"`) +} + +func (as *assertsSuite) TestDecoderBodyTooBig(c *C) { + decoder := asserts.NewDecoderStressed(bytes.NewBufferString(exampleBodyAndExtraHeaders), 1024, 1024, 5, 1024) + _, err := decoder.Decode() + c.Assert(err, ErrorMatches, "assertion body length 8 exceeds maximum body size") +} + +func (as *assertsSuite) TestDecoderSignatureTooBig(c *C) { + decoder := asserts.NewDecoderStressed(bytes.NewBufferString(exampleBodyAndExtraHeaders), 4, 1024, 1024, 7) + _, err := decoder.Decode() + c.Assert(err, ErrorMatches, `error reading assertion signature: maximum size exceeded while looking for delimiter "\\n\\n"`) +} + +func (as *assertsSuite) TestEncode(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: xyz\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + encodeRes := asserts.Encode(a) + c.Check(encodeRes, DeepEquals, encoded) +} + +func (as *assertsSuite) TestEncoderOK(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: xyzyz\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a0, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + cont0, _ := a0.Signature() + + stream := new(bytes.Buffer) + enc := asserts.NewEncoder(stream) + enc.Encode(a0) + + c.Check(bytes.HasSuffix(stream.Bytes(), []byte{'\n'}), Equals, true) + + dec := asserts.NewDecoder(stream) + a1, err := dec.Decode() + c.Assert(err, IsNil) + + cont1, _ := a1.Signature() + c.Check(cont1, DeepEquals, cont0) +} + +func (as *assertsSuite) TestEncoderSingleDecodeOK(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a0, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + cont0, _ := a0.Signature() + + stream := new(bytes.Buffer) + enc := asserts.NewEncoder(stream) + enc.Encode(a0) + + a1, err := asserts.Decode(stream.Bytes()) + c.Assert(err, IsNil) + + cont1, _ := a1.Signature() + c.Check(cont1, DeepEquals, cont0) +} + +func (as *assertsSuite) TestSignFormatSanityEmptyBody(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + _, err = asserts.Decode(asserts.Encode(a)) + c.Check(err, IsNil) +} + +func (as *assertsSuite) TestSignFormatSanityNonEmptyBody(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + body := []byte("THE-BODY") + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, body, testPrivKey1) + c.Assert(err, IsNil) + c.Check(a.Body(), DeepEquals, body) + + decoded, err := asserts.Decode(asserts.Encode(a)) + c.Assert(err, IsNil) + c.Check(decoded.Body(), DeepEquals, body) +} + +func (as *assertsSuite) TestSignFormatSanitySupportMultilineHeaderValues(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + + multilineVals := []string{ + "a\n", + "\na", + "a\n\b\nc", + "a\n\b\nc\n", + "\na\n", + "\n\na\n\nb\n\nc", + } + + for _, multilineVal := range multilineVals { + headers["multiline"] = multilineVal + if len(multilineVal)%2 == 1 { + headers["odd"] = "true" + } + + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + decoded, err := asserts.Decode(asserts.Encode(a)) + c.Assert(err, IsNil) + + c.Check(decoded.Header("multiline"), Equals, multilineVal) + } +} + +func (as *assertsSuite) TestSignFormatAndRevision(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + "format": "1", + "revision": "11", + } + + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + c.Check(a.Revision(), Equals, 11) + c.Check(a.Format(), Equals, 1) + c.Check(a.SupportedFormat(), Equals, true) + + a1, err := asserts.Decode(asserts.Encode(a)) + c.Assert(err, IsNil) + + c.Check(a1.Revision(), Equals, 11) + c.Check(a1.Format(), Equals, 1) + c.Check(a1.SupportedFormat(), Equals, true) +} + +func (as *assertsSuite) TestSignBodyIsUTF8Text(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + _, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, []byte{'\xff'}, testPrivKey1) + c.Assert(err, ErrorMatches, "assertion body is not utf8") +} + +func (as *assertsSuite) TestHeaders(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + + hs := a.Headers() + c.Check(hs, DeepEquals, map[string]interface{}{ + "type": "test-only", + "authority-id": "auth-id2", + "primary-key": "abc", + "revision": "5", + "header1": "value1", + "header2": "value2", + "body-length": "8", + "sign-key-sha3-384": exKeyID, + }) +} + +func (as *assertsSuite) TestHeadersReturnsCopy(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: xyz\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + + hs := a.Headers() + // casual later result mutation doesn't trip us + delete(hs, "primary-key") + c.Check(a.Header("primary-key"), Equals, "xyz") +} + +func (as *assertsSuite) TestAssembleRoundtrip(c *C) { + encoded := []byte("type: test-only\n" + + "format: 1\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + + cont, sig := a.Signature() + reassembled, err := asserts.Assemble(a.Headers(), a.Body(), cont, sig) + c.Assert(err, IsNil) + + c.Check(reassembled.Headers(), DeepEquals, a.Headers()) + c.Check(reassembled.Body(), DeepEquals, a.Body()) + + reassembledEncoded := asserts.Encode(reassembled) + c.Check(reassembledEncoded, DeepEquals, encoded) +} + +func (as *assertsSuite) TestSignKeyID(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + keyID := a.SignKeyID() + c.Check(keyID, Equals, testPrivKey1.PublicKey().ID()) +} + +func (as *assertsSuite) TestSelfRef(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + a1, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + c.Check(a1.Ref(), DeepEquals, &asserts.Ref{ + Type: asserts.TestOnlyType, + PrimaryKey: []string{"0"}, + }) + + headers = map[string]interface{}{ + "authority-id": "auth-id1", + "pk1": "a", + "pk2": "b", + } + a2, err := asserts.AssembleAndSignInTest(asserts.TestOnly2Type, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + c.Check(a2.Ref(), DeepEquals, &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"a", "b"}, + }) +} + +func (as *assertsSuite) TestAssembleHeadersCheck(c *C) { + cont := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5") + headers := map[string]interface{}{ + "type": "test-only", + "authority-id": "auth-id2", + "primary-key": "abc", + "revision": 5, // must be a string actually! + } + + _, err := asserts.Assemble(headers, nil, cont, nil) + c.Check(err, ErrorMatches, `header "revision": header values must be strings or nested lists or maps with strings as the only scalars: 5`) +} + +func (as *assertsSuite) TestSignWithoutAuthorityMisuse(c *C) { + _, err := asserts.SignWithoutAuthority(asserts.TestOnlyType, nil, nil, testPrivKey1) + c.Check(err, ErrorMatches, `cannot sign assertions needing a definite authority with SignWithoutAuthority`) + + _, err = asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityType, + map[string]interface{}{ + "authority-id": "auth-id1", + "hdr": "FOO", + }, nil, testPrivKey1) + c.Check(err, ErrorMatches, `"test-only-no-authority" assertion cannot have authority-id set`) +} + +func (ss *serialSuite) TestSignatureCheckError(c *C) { + sreq, err := asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityType, + map[string]interface{}{ + "hdr": "FOO", + }, nil, testPrivKey1) + c.Assert(err, IsNil) + + err = asserts.SignatureCheck(sreq, testPrivKey2.PublicKey()) + c.Check(err, ErrorMatches, `failed signature verification:.*`) +} + +func (as *assertsSuite) TestWithAuthority(c *C) { + withAuthority := []string{ + "account", + "account-key", + "base-declaration", + "snap-declaration", + "snap-build", + "snap-revision", + "model", + "serial", + "system-user", + "validation", + } + c.Check(withAuthority, HasLen, asserts.NumAssertionType-3) // excluding device-session-request, serial-request, account-key-request + for _, name := range withAuthority { + typ := asserts.Type(name) + _, err := asserts.AssembleAndSignInTest(typ, nil, nil, testPrivKey1) + c.Check(err, ErrorMatches, `"authority-id" header is mandatory`) + } +} diff --git a/asserts/assertstest/assertstest.go b/asserts/assertstest/assertstest.go new file mode 100644 index 00000000..3ce89e4a --- /dev/null +++ b/asserts/assertstest/assertstest.go @@ -0,0 +1,355 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +// Package assertstest provides helpers for testing code that involves assertions. +package assertstest + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "fmt" + "io" + "os/exec" + "strings" + "time" + + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/packet" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/strutil" +) + +// GenerateKey generates a private/public key pair of the given bits. It panics on error. +func GenerateKey(bits int) (asserts.PrivateKey, *rsa.PrivateKey) { + priv, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + panic(fmt.Errorf("failed to create private key: %v", err)) + } + return asserts.RSAPrivateKey(priv), priv +} + +// ReadPrivKey reads a PGP private key (either armored or simply base64 encoded). It panics on error. +func ReadPrivKey(pk string) (asserts.PrivateKey, *rsa.PrivateKey) { + rd := bytes.NewReader([]byte(pk)) + blk, err := armor.Decode(rd) + var body io.Reader + if err == nil { + body = blk.Body + } else { + rd.Seek(0, 0) + // try unarmored + body = base64.NewDecoder(base64.StdEncoding, rd) + } + pkt, err := packet.Read(body) + if err != nil { + panic(err) + } + + pkPkt := pkt.(*packet.PrivateKey) + rsaPrivKey, ok := pkPkt.PrivateKey.(*rsa.PrivateKey) + if !ok { + panic("not a RSA key") + } + + return asserts.RSAPrivateKey(rsaPrivKey), rsaPrivKey +} + +// A sample developer key. +// See systestkeys for a prebuilt set of trusted keys and assertions. +const ( + DevKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQcYBFaFwYABEAC0kYiC4rsWFLJHEv/qO93LTMCAYKMLXFU0XN4XvqnkbwFc0QQd +lQcr7PwavYmKdWum+EmGWV/k5vZ0gwfZhBsL2MTWSNvO+5q5AYOqTq01CbSLcoN4 +cJI+BU348Vc/AoiIuuHro+gALs59HWsVSAKq7SNyHQfo257TKe8Q+Jjh095eruYJ +2kOvlAgAzjUv7eGDQ53O87wcwgZlCl0XqM/t+SRUxE5i8dQ4nySSekoTsWJo02kf +uMrWo3E5iEt6KKhfQtit2ZO91NYetIplzzZmaUOOkpziFTFW1NcwDKzDsLMh1EQ+ +ib+8mSWcou9m35aTkAQXlXlgqe5Pelj5+NUxnnoa1MR478Sv+guT+fbFQrl8PkMD +Jb/3PTKDPBNtjki5ZfIN9id4vidfBY4SCDftnj7yZMf5+1PPZ2XXHUoiUhHbGjST +F/23wr6OWvXe/AXX5BF4wJJTJxSxnYR6nleGMj4sbsbVsxIaqh1lMg5cuQjLr7eI +nxn994geUnQQsEPIVuVjLThJ/0sjXjy8kyxh6eieShZ6NZ0yLyIJRN5pnJ0ckRQF +T9Fs0UuMJZro0hR71t9mAuI45mSmznj78AvTvyuL+0aOj/lQa97NKbCsShYnKqsm +3Yzr03ahUMslwd6jZtRg+0ULYp9vaN7nwmsn6WWJ92CsCzFucdeJfJWKZQARAQAB +AA/9GSda3mzKRhms+hSt/MnJLFxtRpTvsZHztp8nOySO0ykZhf4B9kL/5EEXn3v+ +0IBp9jEJQQNrRd5cv79PFSB/igdw6C7vG+bV12bcGhnqrARFl9Vkdh8saCJiCcdI +8ZifP3jVJvfGxlu+3RP/ik/lOz1cnjVoGCqb9euWB4Wx+meCxyrTFdVHb4qOENqo +8xvOufPt5Fn0vwbSUDoA3N5h1NNLmdlc2BC7EQYuWI9biWHBBTxKHSanbv4GtE6F +wScvyVFtEM7J83xWNaHN07/pYpvQUuienSn5nRB6R5HEcWBIm/JPbWzP/mxRHoBe +HDUSa0z5HPXwGiSh84VmJrBgtlQosxk3jOHjynlU194S2cVLcSrFSf4hp6WZVAa1 +Nlkv6v62eU3nDxabkF92Lcv40s1cBqYCvhOtMzgoXL0TuaVJIdUwbJRHoBi8Bh5f +bNYJqyMqJNHcT9ylAWw130ljPTtqzbTMRtitxnJPbf60hpsJ4jcp2bJP9pg9XyuR +ZyIKtLfGQfxvFLsXzNssnVv7ZenK5AgUFTMvmyKCQQeYluheKc0KtRKSYE3iaVAs +Efw5Pd0GD82UGef9WahtnemodTlD3nkzlD50XBsd8xdNBQ7N2TFsP5Ldvfp1Wf2F +qg+rTaS0OID9vDQuekOcDI8lA9E4FYlIkJ6AqIb7hD5hlBMIAMRVXLlPLgzmrY5k +pIPMbgyN0wm3f4qAWIaMtg79x9gTylsGF7lkqNLqFDFYfoUHb+iXINYc51kHV7Ka +JifHhdy8TaBTBrIrsFLJpv06lRex/fdyvswev3W1g3wRJ86eTCqwr1DjB+q2kYX8 +u1qDPFRzK4WF+wOF/FwCBLDpESmHSapXuzL5i6pJfOCFIJqT/Q/yp9tyTcxs82tu +kSlNKoXrZi4xHsDpPBuNjMl3eIz3ogesUG60MMa6xovVGV3ICJcwYwycvvQcjuxS +XtJlHK+/G3kB87BXzNCMyUGfDNy7mcTrXAXoUH8nCu4ipyaT/jEyvi95w/7RJcFU +qs6taH8IAOtxqnBZGDQuYPF0ZmZQ7e1/FXq/LBQryYZgNtyLUdR7ycXGCTXlEIGw +X3r7Qf4+a3MlriP5thKxci+npcIj4e31aS6cpO2IcGJzmNOHzLCl3b4XmO/APBSA +FZpQE3k+lg45tn/vgcPMKKLAAv6TbpVVgLrFXGtX3Gtkd66fPPOcINXi6+MqXfp5 +rl8OJIq5O5ygbbglwcqmeI29RLZ58b0ktCa5ZZNzeSV+T5jHwRNnWm0EJgjx8Lwn +LEWFS/vQjGwaoRJi06jpmM+66sefyTQ3qvyzQLBqlenZf16GGz28cOSjNJ9FDth1 +iKnyk7d8nqhmbSHeW08QUwTF6NGp+xsIAJDa3ouxSjTEB1P7z1VLJp6nSglBQ74n +XAprk2WpggLNrWwyFJsgFh07LxShb/O3t1TanU+Ld/ryyWHztTxag2auAHuVQ4+S +EkjKqkUaSOQML9a9AvZ2rQr84f5ohc/vCOQhpNVLSyw55EW6WhnntNWVwgZxMiAj +oREMJMrBb6LL9b7kHtfYqLNfe3fkUx+tuTsm96Wi1cdkh0qyut0+J+eieZVn7kiM +UP5IZuz9TSjDOrA5qu5NGlbXNaN0cdJ2UUSNekQiysqDpdf00wIwr1XqH+KLUjZv +pO5Mub6NdnVXJRZunpbNXbuxj49NXnZEEi71WQm9KLR8KQ1oQ+RlnHx/XLQHICh0 +ZXN0KYkCOAQTAQIAIgUCVoXBgAIbLwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA +CgkQSkI9KKrqS0/YEhAAgJALHrx4kFRcgDJE+khK/CdoaLvi0N40eAE+RzQgcxhh +S4Aeks8n1cL6oAwDfCL+ohyWvPzF2DzsBkEIC3l+JS2tn0JJ+qexY+qhdGkEze/o +SIvH9sfR5LJuKb3OAt2mQlY+sxjlkzU9rTGKsVZxgApNM4665dlagF9tipMQTHnd +eFZRlvNTWKkweW0jbJCpRKlQnjEZ6S/wlPBgH69Ek3bnDcgp6eaAU92Ke9Fa2wMV +LBMaXpUIvddKFjoGtvShDOpcQRE99Z8tK4YSAOg+zbSUeD7HGH00EQERItoJsAv1 +7Du8+jcKSeOhz7PPxOA7mEnYNdoMcrg/2AP+FVI6zGYcKN7Hq3C6Z+bQ4X1VkKmv +NCFomU2AyPVxpJRYw7/EkoRWp/iq6sEb7bsmhmDEiz1MiroAV+efmWyUjxueSzrW +24OxHTWi2GuHBF+FKUD3UxfaWMjH+tuWYPIHzYsT+TfsN0vAEFyhRi8Ncelu1RV4 +x2O3wmjxoaX/2FmyuU5WhcVkcpRFgceyf1/86NP9gT5MKbWtJC85YYpxibnvPdGd ++sqtEEqgX3dSsHT+rkBk7kf3ghDwsLtnliFPOeAaIHGZl754EpK+qPUTnYZK022H +2crhYlApO9+06kBeybSO6joMUR007883I9GELYhzmuEjpVGquJQ3+S5QtW1to0w= +=5Myf +-----END PGP PRIVATE KEY BLOCK----- +` + + DevKeyID = "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu" + + DevKeyPGPFingerprint = "966e70f4b9f257a2772f8f354a423d28aaea4b4f" +) + +// GPGImportKey imports the given PGP armored key into the GnuPG setup at homedir. It panics on error. +func GPGImportKey(homedir, armoredKey string) { + path, err := exec.LookPath("gpg1") + if err != nil { + path, err = exec.LookPath("gpg") + } + if err != nil { + panic(err) + } + gpg := exec.Command(path, "--homedir", homedir, "-q", "--batch", "--import", "--armor") + gpg.Stdin = bytes.NewBufferString(armoredKey) + out, err := gpg.CombinedOutput() + if err != nil { + panic(fmt.Errorf("cannot import test key into GPG setup at %q: %v (%q)", homedir, err, out)) + } +} + +// A SignerDB can sign assertions using its key pairs. +type SignerDB interface { + Sign(assertType *asserts.AssertionType, headers map[string]interface{}, body []byte, keyID string) (asserts.Assertion, error) +} + +// NewAccount creates an account assertion for username, it fills in values for other missing headers as needed. It panics on error. +func NewAccount(db SignerDB, username string, otherHeaders map[string]interface{}, keyID string) *asserts.Account { + if otherHeaders == nil { + otherHeaders = make(map[string]interface{}) + } + otherHeaders["username"] = username + if otherHeaders["account-id"] == nil { + otherHeaders["account-id"] = strutil.MakeRandomString(32) + } + if otherHeaders["display-name"] == nil { + otherHeaders["display-name"] = strings.ToTitle(username[:1]) + username[1:] + } + if otherHeaders["validation"] == nil { + otherHeaders["validation"] = "unproven" + } + if otherHeaders["timestamp"] == nil { + otherHeaders["timestamp"] = time.Now().Format(time.RFC3339) + } + a, err := db.Sign(asserts.AccountType, otherHeaders, nil, keyID) + if err != nil { + panic(err) + } + return a.(*asserts.Account) +} + +// NewAccountKey creates an account-key assertion for the account, it fills in values for missing headers as needed. In panics on error. +func NewAccountKey(db SignerDB, acct *asserts.Account, otherHeaders map[string]interface{}, pubKey asserts.PublicKey, keyID string) *asserts.AccountKey { + if otherHeaders == nil { + otherHeaders = make(map[string]interface{}) + } + otherHeaders["account-id"] = acct.AccountID() + otherHeaders["public-key-sha3-384"] = pubKey.ID() + if otherHeaders["name"] == nil { + otherHeaders["name"] = "default" + } + if otherHeaders["since"] == nil { + otherHeaders["since"] = time.Now().Format(time.RFC3339) + } + encodedPubKey, err := asserts.EncodePublicKey(pubKey) + if err != nil { + panic(err) + } + a, err := db.Sign(asserts.AccountKeyType, otherHeaders, encodedPubKey, keyID) + if err != nil { + panic(err) + } + return a.(*asserts.AccountKey) +} + +// SigningDB embeds a signing assertion database with a default private key and assigned authority id. +// Sign will use the assigned authority id. +// "" can be passed for keyID to Sign and PublicKey to use the default key. +type SigningDB struct { + AuthorityID string + KeyID string + + *asserts.Database +} + +// NewSigningDB creates a test signing assertion db with the given defaults. It panics on error. +func NewSigningDB(authorityID string, privKey asserts.PrivateKey) *SigningDB { + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{}) + if err != nil { + panic(err) + } + err = db.ImportKey(privKey) + if err != nil { + panic(err) + } + return &SigningDB{ + AuthorityID: authorityID, + KeyID: privKey.PublicKey().ID(), + Database: db, + } +} + +func (db *SigningDB) Sign(assertType *asserts.AssertionType, headers map[string]interface{}, body []byte, keyID string) (asserts.Assertion, error) { + headers["authority-id"] = db.AuthorityID + if keyID == "" { + keyID = db.KeyID + } + return db.Database.Sign(assertType, headers, body, keyID) +} + +func (db *SigningDB) PublicKey(keyID string) (asserts.PublicKey, error) { + if keyID == "" { + keyID = db.KeyID + } + return db.Database.PublicKey(keyID) +} + +// StoreStack realises a store-like set of founding trusted assertions and signing setup. +type StoreStack struct { + // Trusted authority assertions. + TrustedAccount *asserts.Account + TrustedKey *asserts.AccountKey + Trusted []asserts.Assertion + + // Signing assertion db that signs with the root private key. + RootSigning *SigningDB + + // The store-like signing functionality that signs with a store key, setup to also store assertions if desired. It stores a default account-key for the store private key, see also the StoreStack.Key method. + *SigningDB +} + +// NewStoreStack creates a new store assertion stack. It panics on error. +func NewStoreStack(authorityID string, rootPrivKey, storePrivKey asserts.PrivateKey) *StoreStack { + rootSigning := NewSigningDB(authorityID, rootPrivKey) + ts := time.Now().Format(time.RFC3339) + trustedAcct := NewAccount(rootSigning, authorityID, map[string]interface{}{ + "account-id": authorityID, + "validation": "certified", + "timestamp": ts, + }, "") + trustedKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{ + "name": "root", + "since": ts, + }, rootPrivKey.PublicKey(), "") + trusted := []asserts.Assertion{trustedAcct, trustedKey} + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: trusted, + }) + if err != nil { + panic(err) + } + err = db.ImportKey(storePrivKey) + if err != nil { + panic(err) + } + storeKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{ + "name": "store", + }, storePrivKey.PublicKey(), "") + err = db.Add(storeKey) + if err != nil { + panic(err) + } + + return &StoreStack{ + TrustedAccount: trustedAcct, + TrustedKey: trustedKey, + Trusted: trusted, + + RootSigning: rootSigning, + + SigningDB: &SigningDB{ + AuthorityID: authorityID, + KeyID: storeKey.PublicKeyID(), + Database: db, + }, + } +} + +// StoreAccountKey retrieves one of the account-key assertions for the signing keys of the simulated store signing database. +// "" for keyID means the default one. It panics on error. +func (ss *StoreStack) StoreAccountKey(keyID string) *asserts.AccountKey { + if keyID == "" { + keyID = ss.KeyID + } + key, err := ss.Find(asserts.AccountKeyType, map[string]string{ + "account-id": ss.AuthorityID, + "public-key-sha3-384": keyID, + }) + if err == asserts.ErrNotFound { + return nil + } + if err != nil { + panic(err) + } + return key.(*asserts.AccountKey) +} + +// MockBuiltinBaseDeclaration mocks the builtin base-declaration exposed by asserts.BuiltinBaseDeclaration. +func MockBuiltinBaseDeclaration(headers []byte) (restore func()) { + var prevHeaders []byte + decl := asserts.BuiltinBaseDeclaration() + if decl != nil { + prevHeaders, _ = decl.Signature() + } + + err := asserts.InitBuiltinBaseDeclaration(headers) + if err != nil { + panic(err) + } + + return func() { + err := asserts.InitBuiltinBaseDeclaration(prevHeaders) + if err != nil { + panic(err) + } + } +} diff --git a/asserts/assertstest/assertstest_test.go b/asserts/assertstest/assertstest_test.go new file mode 100644 index 00000000..3c1b0c9b --- /dev/null +++ b/asserts/assertstest/assertstest_test.go @@ -0,0 +1,122 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package assertstest_test + +import ( + "encoding/hex" + "testing" + "time" + + "golang.org/x/crypto/openpgp/packet" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +func TestAssertsTest(t *testing.T) { TestingT(t) } + +type helperSuite struct{} + +var _ = Suite(&helperSuite{}) + +func (s *helperSuite) TestReadPrivKeyArmored(c *C) { + pk, rsaPrivKey := assertstest.ReadPrivKey(assertstest.DevKey) + c.Check(pk, NotNil) + c.Check(rsaPrivKey, NotNil) + c.Check(pk.PublicKey().ID(), Equals, assertstest.DevKeyID) + pkt := packet.NewRSAPrivateKey(time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC), rsaPrivKey) + c.Check(hex.EncodeToString(pkt.Fingerprint[:]), Equals, assertstest.DevKeyPGPFingerprint) +} + +const ( + base64PrivKey = ` +xcLYBFaU5cgBCAC/2wUYK7YzvL6f0ZxBfptFVfNmI7G9J9Eszdoq1NZZXaV+aYeC7eNU +1sKdO6wIRcw3lvybtq5W1n4D/jJAb2qXbB6BukuCGVXCLMEUdvheaVVcIZ/LwdbxmgMJsDFoHsDC +RzjkUVTU2b8sK6MwANIsSS5r8Lwm7FazD1qq50UdebsIx8dkjFR5VwrCYgOu1MO2Bqka7UU9as2q +4ZsFzpcS/so41kd4IPFEmNMlejhSjgCaixehpLeXypQVHLluV+oSPMV7GtE7Z6HO4V5cT2c9RdXg +l4jSKY91rHInkmSizF03laL3T/I6oj0FdZG9GB6QzqRCBTzK05cnVP1k7WFJABEBAAEAB/9spiIa +cBa88fSaGWB+Dq7r8yLmAuzTDEt/LgyRGPtSnJ/uGOEvGn0VPJH17ScdgDmIea8Ql8HfV5UBueDH +cNFSc15LZS8BvEs+rY2ig0VgYhJ/HGOcRmftZqS1xdwU9OWAoEjts8lwyOdkoknGE5Dyl3b8ldZX +zJvEx7s28cXITH4UwGEAMHEXrAMCjkcKPVbM7vW81uOWn0U1jMzmfmqrcLkSfvaCnep6+4QphKPy +B4DxJAI34EvJAru4iL5bWWvMeXkBZgmBy4g2SlYbk09cfTmhzw6di5GZtg+77yGACltPBA8MSbzF +v30apQ5iuI/hVin7U2/QtQHP4d0zUDbpBADusynnaFcDnPEUm4RdvNpujaBC/HfIpOstiS36RZy8 +lZeVtffa/+DqzodZD9YF7zEVWeUiC5Os4THirYOZ04dM5yqR/GlKXMHGHaT+mnhD8g1hORx/LrMO +k5wUpD1NmloSjP/0pJRccuXq7O1QQfls1Hq1vOSh3cZ/aIvTONJ/YwQAzcK0/2SrnaUc3oCxMEuI +2FX0LsYDQiXzMK/x/lfZ/ywxt5J/q6CuaG3xXgSHlsk0M8Uo4acZqpCIFA9mwCPxKbrIOGnwJsI/ ++sZBkngtZMSS88Vl32gnzpVWLGpbW2F7hnWrj1YigTcFUdi6TFNa7zHPASzCKxKKiz9YxEWWymME +AIbURnQJJOSfYgFyloQuA2QWyAK5Zu7qPworBoRo+PZPVb5yQmSUQ21VqNfzqIJz1EgiDZ0NyGid +uXAjn58O9tAq7IN5pTeHoTacZ75cI82kQkUxEnfiKjBO/AU30Y3COsIXhtbIXbtcitHSicp4lnpU +NejDkxUnC2wIvJzHWo1FQ18= +` +) + +func (s *helperSuite) TestReadPrivKeyUnarmored(c *C) { + pk, rsaPrivKey := assertstest.ReadPrivKey(base64PrivKey) + c.Check(pk, NotNil) + c.Check(rsaPrivKey, NotNil) +} + +func (s *helperSuite) TestStoreStack(c *C) { + rootPrivKey, _ := assertstest.GenerateKey(1024) + storePrivKey, _ := assertstest.GenerateKey(752) + + store := assertstest.NewStoreStack("super", rootPrivKey, storePrivKey) + + c.Check(store.TrustedAccount.AccountID(), Equals, "super") + c.Check(store.TrustedAccount.IsCertified(), Equals, true) + + c.Check(store.TrustedKey.AccountID(), Equals, "super") + c.Check(store.TrustedKey.Name(), Equals, "root") + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + }) + c.Assert(err, IsNil) + + storeAccKey := store.StoreAccountKey("") + c.Assert(storeAccKey, NotNil) + + c.Check(storeAccKey.AccountID(), Equals, "super") + c.Check(storeAccKey.AccountID(), Equals, store.AuthorityID) + c.Check(storeAccKey.PublicKeyID(), Equals, store.KeyID) + c.Check(storeAccKey.Name(), Equals, "store") + + acct := assertstest.NewAccount(store, "devel1", nil, "") + c.Check(acct.Username(), Equals, "devel1") + c.Check(acct.AccountID(), HasLen, 32) + c.Check(acct.IsCertified(), Equals, false) + + err = db.Add(storeAccKey) + c.Assert(err, IsNil) + + err = db.Add(acct) + c.Assert(err, IsNil) + + devKey, _ := assertstest.GenerateKey(752) + + acctKey := assertstest.NewAccountKey(store, acct, nil, devKey.PublicKey(), "") + + err = db.Add(acctKey) + c.Assert(err, IsNil) + + c.Check(acctKey.Name(), Equals, "default") +} diff --git a/asserts/crypto.go b/asserts/crypto.go new file mode 100644 index 00000000..d9397be4 --- /dev/null +++ b/asserts/crypto.go @@ -0,0 +1,398 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + _ "crypto/sha256" // be explicit about supporting SHA256 + _ "crypto/sha512" // be explicit about needing SHA512 + "encoding/base64" + "fmt" + "io" + "time" + + "golang.org/x/crypto/openpgp/packet" + "golang.org/x/crypto/sha3" +) + +const ( + maxEncodeLineLength = 76 + v1 = 0x1 +) + +var ( + v1Header = []byte{v1} + v1FixedTimestamp = time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) +) + +func encodeV1(data []byte) []byte { + buf := new(bytes.Buffer) + buf.Grow(base64.StdEncoding.EncodedLen(len(data) + 1)) + enc := base64.NewEncoder(base64.StdEncoding, buf) + enc.Write(v1Header) + enc.Write(data) + enc.Close() + flat := buf.Bytes() + flatSize := len(flat) + + buf = new(bytes.Buffer) + buf.Grow(flatSize + flatSize/maxEncodeLineLength + 1) + off := 0 + for { + endOff := off + maxEncodeLineLength + if endOff > flatSize { + endOff = flatSize + } + buf.Write(flat[off:endOff]) + off = endOff + if off >= flatSize { + break + } + buf.WriteByte('\n') + } + + return buf.Bytes() +} + +type keyEncoder interface { + keyEncode(w io.Writer) error +} + +func encodeKey(key keyEncoder, kind string) ([]byte, error) { + buf := new(bytes.Buffer) + err := key.keyEncode(buf) + if err != nil { + return nil, fmt.Errorf("cannot encode %s: %v", kind, err) + } + return encodeV1(buf.Bytes()), nil +} + +type openpgpSigner interface { + sign(content []byte) (*packet.Signature, error) +} + +func signContent(content []byte, privateKey PrivateKey) ([]byte, error) { + signer, ok := privateKey.(openpgpSigner) + if !ok { + panic(fmt.Errorf("not an internally supported PrivateKey: %T", privateKey)) + } + + sig, err := signer.sign(content) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + err = sig.Serialize(buf) + if err != nil { + return nil, err + } + + return encodeV1(buf.Bytes()), nil +} + +func decodeV1(b []byte, kind string) (packet.Packet, error) { + if len(b) == 0 { + return nil, fmt.Errorf("cannot decode %s: no data", kind) + } + buf := make([]byte, base64.StdEncoding.DecodedLen(len(b))) + n, err := base64.StdEncoding.Decode(buf, b) + if err != nil { + return nil, fmt.Errorf("cannot decode %s: %v", kind, err) + } + if n == 0 { + return nil, fmt.Errorf("cannot decode %s: base64 without data", kind) + } + buf = buf[:n] + if buf[0] != v1 { + return nil, fmt.Errorf("unsupported %s format version: %d", kind, buf[0]) + } + rd := bytes.NewReader(buf[1:]) + pkt, err := packet.Read(rd) + if err != nil { + return nil, fmt.Errorf("cannot decode %s: %v", kind, err) + } + if rd.Len() != 0 { + return nil, fmt.Errorf("%s has spurious trailing data", kind) + } + return pkt, nil +} + +func decodeSignature(signature []byte) (*packet.Signature, error) { + pkt, err := decodeV1(signature, "signature") + if err != nil { + return nil, err + } + sig, ok := pkt.(*packet.Signature) + if !ok { + return nil, fmt.Errorf("expected signature, got instead: %T", pkt) + } + return sig, nil +} + +// PublicKey is the public part of a cryptographic private/public key pair. +type PublicKey interface { + // ID returns the id of the key used for lookup. + ID() string + + // verify verifies signature is valid for content using the key. + verify(content []byte, sig *packet.Signature) error + + keyEncoder +} + +type openpgpPubKey struct { + pubKey *packet.PublicKey + sha3_384 string +} + +func (opgPubKey *openpgpPubKey) ID() string { + return opgPubKey.sha3_384 +} + +func (opgPubKey *openpgpPubKey) verify(content []byte, sig *packet.Signature) error { + h := sig.Hash.New() + h.Write(content) + return opgPubKey.pubKey.VerifySignature(h, sig) +} + +func (opgPubKey openpgpPubKey) keyEncode(w io.Writer) error { + return opgPubKey.pubKey.Serialize(w) +} + +func newOpenPGPPubKey(intPubKey *packet.PublicKey) *openpgpPubKey { + h := sha3.New384() + h.Write(v1Header) + err := intPubKey.Serialize(h) + if err != nil { + panic("internal error: cannot compute public key sha3-384") + } + sha3_384, err := EncodeDigest(crypto.SHA3_384, h.Sum(nil)) + if err != nil { + panic("internal error: cannot compute public key sha3-384") + } + return &openpgpPubKey{pubKey: intPubKey, sha3_384: sha3_384} +} + +// RSAPublicKey returns a database useable public key out of rsa.PublicKey. +func RSAPublicKey(pubKey *rsa.PublicKey) PublicKey { + intPubKey := packet.NewRSAPublicKey(v1FixedTimestamp, pubKey) + return newOpenPGPPubKey(intPubKey) +} + +// DecodePublicKey deserializes a public key. +func DecodePublicKey(pubKey []byte) (PublicKey, error) { + pkt, err := decodeV1(pubKey, "public key") + if err != nil { + return nil, err + } + pubk, ok := pkt.(*packet.PublicKey) + if !ok { + return nil, fmt.Errorf("expected public key, got instead: %T", pkt) + } + rsaPubKey, ok := pubk.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("expected RSA public key, got instead: %T", pubk.PublicKey) + } + return RSAPublicKey(rsaPubKey), nil +} + +// EncodePublicKey serializes a public key, typically for embedding in an assertion. +func EncodePublicKey(pubKey PublicKey) ([]byte, error) { + return encodeKey(pubKey, "public key") +} + +// PrivateKey is a cryptographic private/public key pair. +type PrivateKey interface { + // PublicKey returns the public part of the pair. + PublicKey() PublicKey + + keyEncoder +} + +type openpgpPrivateKey struct { + privk *packet.PrivateKey +} + +func (opgPrivK openpgpPrivateKey) PublicKey() PublicKey { + return newOpenPGPPubKey(&opgPrivK.privk.PublicKey) +} + +func (opgPrivK openpgpPrivateKey) keyEncode(w io.Writer) error { + return opgPrivK.privk.Serialize(w) +} + +var openpgpConfig = &packet.Config{ + DefaultHash: crypto.SHA512, +} + +func (opgPrivK openpgpPrivateKey) sign(content []byte) (*packet.Signature, error) { + privk := opgPrivK.privk + sig := new(packet.Signature) + sig.PubKeyAlgo = privk.PubKeyAlgo + sig.Hash = openpgpConfig.Hash() + sig.CreationTime = time.Now() + + h := openpgpConfig.Hash().New() + h.Write(content) + + err := sig.Sign(h, privk, openpgpConfig) + if err != nil { + return nil, err + } + + return sig, nil +} + +func decodePrivateKey(privKey []byte) (PrivateKey, error) { + pkt, err := decodeV1(privKey, "private key") + if err != nil { + return nil, err + } + privk, ok := pkt.(*packet.PrivateKey) + if !ok { + return nil, fmt.Errorf("expected private key, got instead: %T", pkt) + } + if _, ok := privk.PrivateKey.(*rsa.PrivateKey); !ok { + return nil, fmt.Errorf("expected RSA private key, got instead: %T", privk.PrivateKey) + } + return openpgpPrivateKey{privk}, nil +} + +// RSAPrivateKey returns a PrivateKey for database use out of a rsa.PrivateKey. +func RSAPrivateKey(privk *rsa.PrivateKey) PrivateKey { + intPrivk := packet.NewRSAPrivateKey(v1FixedTimestamp, privk) + return openpgpPrivateKey{intPrivk} +} + +// GenerateKey generates a private/public key pair. +func GenerateKey() (PrivateKey, error) { + priv, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + return RSAPrivateKey(priv), nil +} + +func encodePrivateKey(privKey PrivateKey) ([]byte, error) { + return encodeKey(privKey, "private key") +} + +// externally held key pairs + +type extPGPPrivateKey struct { + pubKey PublicKey + from string + pgpFingerprint string + bitLen int + doSign func(content []byte) ([]byte, error) +} + +func newExtPGPPrivateKey(exportedPubKeyStream io.Reader, from string, sign func(content []byte) ([]byte, error)) (*extPGPPrivateKey, error) { + var pubKey *packet.PublicKey + + rd := packet.NewReader(exportedPubKeyStream) + for { + pkt, err := rd.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("cannot read exported public key: %v", err) + } + cand, ok := pkt.(*packet.PublicKey) + if ok { + if cand.IsSubkey { + continue + } + if pubKey != nil { + return nil, fmt.Errorf("cannot select exported public key, found many") + } + pubKey = cand + } + } + + if pubKey == nil { + return nil, fmt.Errorf("cannot read exported public key, found none (broken export)") + + } + + rsaPubKey, ok := pubKey.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("not a RSA key") + } + + return &extPGPPrivateKey{ + pubKey: RSAPublicKey(rsaPubKey), + from: from, + pgpFingerprint: fmt.Sprintf("%X", pubKey.Fingerprint), + bitLen: rsaPubKey.N.BitLen(), + doSign: sign, + }, nil +} + +func (expk *extPGPPrivateKey) fingerprint() string { + return expk.pgpFingerprint +} + +func (expk *extPGPPrivateKey) PublicKey() PublicKey { + return expk.pubKey +} + +func (expk *extPGPPrivateKey) keyEncode(w io.Writer) error { + return fmt.Errorf("cannot access external private key to encode it") +} + +func (expk *extPGPPrivateKey) sign(content []byte) (*packet.Signature, error) { + if expk.bitLen < 4096 { + return nil, fmt.Errorf("signing needs at least a 4096 bits key, got %d", expk.bitLen) + } + + out, err := expk.doSign(content) + if err != nil { + return nil, err + } + + badSig := fmt.Sprintf("bad %s produced signature: ", expk.from) + + sigpkt, err := packet.Read(bytes.NewBuffer(out)) + if err != nil { + return nil, fmt.Errorf(badSig+"%v", err) + } + + sig, ok := sigpkt.(*packet.Signature) + if !ok { + return nil, fmt.Errorf(badSig+"got %T", sigpkt) + } + + if sig.Hash != crypto.SHA512 { + return nil, fmt.Errorf(badSig + "expected SHA512 digest") + } + + err = expk.pubKey.verify(content, sig) + if err != nil { + return nil, fmt.Errorf(badSig+"it does not verify: %v", err) + } + + return sig, nil +} diff --git a/asserts/database.go b/asserts/database.go new file mode 100644 index 00000000..54b7fdb1 --- /dev/null +++ b/asserts/database.go @@ -0,0 +1,556 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +// Package asserts implements snappy assertions and a database +// abstraction for managing and holding them. +package asserts + +import ( + "errors" + "fmt" + "regexp" + "time" +) + +// A Backstore stores assertions. It can store and retrieve assertions +// by type under unique primary key headers (whose names are available +// from assertType.PrimaryKey). Plus it supports searching by headers. +// Lookups can be limited to a maximum allowed format. +type Backstore interface { + // Put stores an assertion. + // It is responsible for checking that assert is newer than a + // previously stored revision with the same primary key headers. + Put(assertType *AssertionType, assert Assertion) error + // Get returns the assertion with the given unique key for its primary key headers. + // If none is present it returns ErrNotFound. + Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error) + // Search returns assertions matching the given headers. + // It invokes foundCb for each found assertion. + Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error +} + +type nullBackstore struct{} + +func (nbs nullBackstore) Put(t *AssertionType, a Assertion) error { + return fmt.Errorf("cannot store assertions without setting a proper assertion backstore implementation") +} + +func (nbs nullBackstore) Get(t *AssertionType, k []string, maxFormat int) (Assertion, error) { + return nil, ErrNotFound +} + +func (nbs nullBackstore) Search(t *AssertionType, h map[string]string, f func(Assertion), maxFormat int) error { + return nil +} + +// A KeypairManager is a manager and backstore for private/public key pairs. +type KeypairManager interface { + // Put stores the given private/public key pair, + // making sure it can be later retrieved by its unique key id with Get. + // Trying to store a key with an already present key id should + // result in an error. + Put(privKey PrivateKey) error + // Get returns the private/public key pair with the given key id. + Get(keyID string) (PrivateKey, error) +} + +// DatabaseConfig for an assertion database. +type DatabaseConfig struct { + // trusted set of assertions (account and account-key supported) + Trusted []Assertion + // backstore for assertions, left unset storing assertions will error + Backstore Backstore + // manager/backstore for keypairs, defaults to in-memory implementation + KeypairManager KeypairManager + // assertion checkers used by Database.Check, left unset DefaultCheckers will be used which is recommended + Checkers []Checker +} + +// Well-known errors +var ( + ErrNotFound = errors.New("assertion not found") +) + +// RevisionError indicates a revision improperly used for an operation. +type RevisionError struct { + Used, Current int +} + +func (e *RevisionError) Error() string { + if e.Used < 0 || e.Current < 0 { + // TODO: message may need tweaking once there's a use. + return fmt.Sprintf("assertion revision is unknown") + } + if e.Used == e.Current { + return fmt.Sprintf("revision %d is already the current revision", e.Used) + } + if e.Used < e.Current { + return fmt.Sprintf("revision %d is older than current revision %d", e.Used, e.Current) + } + return fmt.Sprintf("revision %d is more recent than current revision %d", e.Used, e.Current) +} + +// UnsupportedFormatError indicates an assertion with a format iteration not yet supported by the present version of asserts. +type UnsupportedFormatError struct { + Ref *Ref + Format int + // Update marks there was already a current revision of the assertion and it has been kept. + Update bool +} + +func (e *UnsupportedFormatError) Error() string { + postfx := "" + if e.Update { + postfx = " (current not updated)" + } + return fmt.Sprintf("proposed %q assertion has format %d but %d is latest supported%s", e.Ref.Type.Name, e.Format, e.Ref.Type.MaxSupportedFormat(), postfx) +} + +// IsUnaccceptedUpdate returns whether the error indicates that an +// assertion revision was already present and has been kept because +// the update was not accepted. +func IsUnaccceptedUpdate(err error) bool { + switch x := err.(type) { + case *UnsupportedFormatError: + return x.Update + case *RevisionError: + return x.Used <= x.Current + } + return false +} + +// A RODatabase exposes read-only access to an assertion database. +type RODatabase interface { + // IsTrustedAccount returns whether the account is part of the trusted set. + IsTrustedAccount(accountID string) bool + // Find an assertion based on arbitrary headers. + // Provided headers must contain the primary key for the assertion type. + // It returns ErrNotFound if the assertion cannot be found. + Find(assertionType *AssertionType, headers map[string]string) (Assertion, error) + // FindTrusted finds an assertion in the trusted set based on arbitrary headers. + // Provided headers must contain the primary key for the assertion type. + // It returns ErrNotFound if the assertion cannot be found. + FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error) + // FindMany finds assertions based on arbitrary headers. + // It returns ErrNotFound if no assertion can be found. + FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) + // Check tests whether the assertion is properly signed and consistent with all the stored knowledge. + Check(assert Assertion) error +} + +// A Checker defines a check on an assertion considering aspects such as +// the signing key, and consistency with other +// assertions in the database. +type Checker func(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error + +// Database holds assertions and can be used to sign or check +// further assertions. +type Database struct { + bs Backstore + keypairMgr KeypairManager + trusted Backstore + backstores []Backstore + checkers []Checker +} + +// OpenDatabase opens the assertion database based on the configuration. +func OpenDatabase(cfg *DatabaseConfig) (*Database, error) { + bs := cfg.Backstore + keypairMgr := cfg.KeypairManager + + if bs == nil { + bs = nullBackstore{} + } + if keypairMgr == nil { + keypairMgr = NewMemoryKeypairManager() + } + + trustedBackstore := NewMemoryBackstore() + + for _, a := range cfg.Trusted { + switch accepted := a.(type) { + case *AccountKey: + accKey := accepted + err := trustedBackstore.Put(AccountKeyType, accKey) + if err != nil { + return nil, fmt.Errorf("error loading for use trusted account key %q for %q: %v", accKey.PublicKeyID(), accKey.AccountID(), err) + } + + case *Account: + acct := accepted + err := trustedBackstore.Put(AccountType, acct) + if err != nil { + return nil, fmt.Errorf("error loading for use trusted account %q: %v", acct.DisplayName(), err) + } + default: + return nil, fmt.Errorf("cannot load trusted assertions that are not account-key or account: %s", a.Type().Name) + } + } + + checkers := cfg.Checkers + if len(checkers) == 0 { + checkers = DefaultCheckers + } + dbCheckers := make([]Checker, len(checkers)) + copy(dbCheckers, checkers) + + return &Database{ + bs: bs, + keypairMgr: keypairMgr, + trusted: trustedBackstore, + // order here is relevant, Find* precedence and + // findAccountKey depend on it, trusted should win over the + // general backstore! + backstores: []Backstore{trustedBackstore, bs}, + checkers: dbCheckers, + }, nil +} + +// ImportKey stores the given private/public key pair. +func (db *Database) ImportKey(privKey PrivateKey) error { + return db.keypairMgr.Put(privKey) +} + +var ( + // for sanity checking of base64 hash strings + base64HashLike = regexp.MustCompile("^[[:alnum:]_-]*$") +) + +func (db *Database) safeGetPrivateKey(keyID string) (PrivateKey, error) { + if keyID == "" { + return nil, fmt.Errorf("key id is empty") + } + if !base64HashLike.MatchString(keyID) { + return nil, fmt.Errorf("key id contains unexpected chars: %q", keyID) + } + return db.keypairMgr.Get(keyID) +} + +// PublicKey returns the public key part of the key pair that has the given key id. +func (db *Database) PublicKey(keyID string) (PublicKey, error) { + privKey, err := db.safeGetPrivateKey(keyID) + if err != nil { + return nil, err + } + return privKey.PublicKey(), nil +} + +// Sign assembles an assertion with the provided information and signs it +// with the private key from `headers["authority-id"]` that has the provided key id. +func (db *Database) Sign(assertType *AssertionType, headers map[string]interface{}, body []byte, keyID string) (Assertion, error) { + privKey, err := db.safeGetPrivateKey(keyID) + if err != nil { + return nil, err + } + return assembleAndSign(assertType, headers, body, privKey) +} + +// findAccountKey finds an AccountKey exactly with account id and key id. +func (db *Database) findAccountKey(authorityID, keyID string) (*AccountKey, error) { + key := []string{keyID} + // consider trusted account keys then disk stored account keys + for _, bs := range db.backstores { + a, err := bs.Get(AccountKeyType, key, AccountKeyType.MaxSupportedFormat()) + if err == nil { + hit := a.(*AccountKey) + if hit.AccountID() != authorityID { + return nil, fmt.Errorf("found public key %q from %q but expected it from: %s", keyID, hit.AccountID(), authorityID) + } + return hit, nil + } + if err != ErrNotFound { + return nil, err + } + } + return nil, ErrNotFound +} + +// IsTrustedAccount returns whether the account is part of the trusted set. +func (db *Database) IsTrustedAccount(accountID string) bool { + if accountID == "" { + return false + } + _, err := db.trusted.Get(AccountType, []string{accountID}, AccountType.MaxSupportedFormat()) + return err == nil +} + +// Check tests whether the assertion is properly signed and consistent with all the stored knowledge. +func (db *Database) Check(assert Assertion) error { + if !assert.SupportedFormat() { + return &UnsupportedFormatError{Ref: assert.Ref(), Format: assert.Format()} + } + + typ := assert.Type() + now := time.Now() + + var accKey *AccountKey + var err error + if typ.flags&noAuthority == 0 { + // TODO: later may need to consider type of assert to find candidate keys + accKey, err = db.findAccountKey(assert.AuthorityID(), assert.SignKeyID()) + if err == ErrNotFound { + return fmt.Errorf("no matching public key %q for signature by %q", assert.SignKeyID(), assert.AuthorityID()) + } + if err != nil { + return fmt.Errorf("error finding matching public key for signature: %v", err) + } + } else { + if assert.AuthorityID() != "" { + return fmt.Errorf("internal error: %q assertion cannot have authority-id set", typ.Name) + } + } + + for _, checker := range db.checkers { + err := checker(assert, accKey, db, now) + if err != nil { + return err + } + } + + return nil +} + +// Add persists the assertion after ensuring it is properly signed and consistent with all the stored knowledge. +// It will return an error when trying to add an older revision of the assertion than the one currently stored. +func (db *Database) Add(assert Assertion) error { + ref := assert.Ref() + + if len(ref.PrimaryKey) == 0 { + return fmt.Errorf("internal error: assertion type %q has no primary key", ref.Type.Name) + } + + err := db.Check(assert) + if err != nil { + if ufe, ok := err.(*UnsupportedFormatError); ok { + _, err := ref.Resolve(db.Find) + if err != nil && err != ErrNotFound { + return err + } + return &UnsupportedFormatError{Ref: ufe.Ref, Format: ufe.Format, Update: err == nil} + } + return err + } + + for i, keyVal := range ref.PrimaryKey { + if keyVal == "" { + return fmt.Errorf("missing or non-string primary key header: %v", ref.Type.PrimaryKey[i]) + } + } + + // assuming trusted account keys/assertions will be managed + // through the os snap this seems the safest policy until we + // know more/better + _, err = db.trusted.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat()) + if err != ErrNotFound { + return fmt.Errorf("cannot add %q assertion with primary key clashing with a trusted assertion: %v", ref.Type.Name, ref.PrimaryKey) + } + + return db.bs.Put(ref.Type, assert) +} + +func searchMatch(assert Assertion, expectedHeaders map[string]string) bool { + // check non-primary-key headers as well + for expectedKey, expectedValue := range expectedHeaders { + if assert.Header(expectedKey) != expectedValue { + return false + } + } + return true +} + +func find(backstores []Backstore, assertionType *AssertionType, headers map[string]string, maxFormat int) (Assertion, error) { + err := checkAssertType(assertionType) + if err != nil { + return nil, err + } + maxSupp := assertionType.MaxSupportedFormat() + if maxFormat == -1 { + maxFormat = maxSupp + } else { + if maxFormat > maxSupp { + return nil, fmt.Errorf("cannot find %q assertions for format %d higher than supported format %d", assertionType.Name, maxFormat, maxSupp) + } + } + keyValues := make([]string, len(assertionType.PrimaryKey)) + for i, k := range assertionType.PrimaryKey { + keyVal := headers[k] + if keyVal == "" { + return nil, fmt.Errorf("must provide primary key: %v", k) + } + keyValues[i] = keyVal + } + + var assert Assertion + for _, bs := range backstores { + a, err := bs.Get(assertionType, keyValues, maxFormat) + if err == nil { + assert = a + break + } + if err != ErrNotFound { + return nil, err + } + } + + if assert == nil || !searchMatch(assert, headers) { + return nil, ErrNotFound + } + + return assert, nil +} + +// Find an assertion based on arbitrary headers. +// Provided headers must contain the primary key for the assertion type. +// It returns ErrNotFound if the assertion cannot be found. +func (db *Database) Find(assertionType *AssertionType, headers map[string]string) (Assertion, error) { + return find(db.backstores, assertionType, headers, -1) +} + +// FindMaxFormat finds an assertion like Find but such that its +// format is <= maxFormat by passing maxFormat along to the backend. +// It returns ErrNotFound if such an assertion cannot be found. +func (db *Database) FindMaxFormat(assertionType *AssertionType, headers map[string]string, maxFormat int) (Assertion, error) { + return find(db.backstores, assertionType, headers, maxFormat) +} + +// FindTrusted finds an assertion in the trusted set based on arbitrary headers. +// Provided headers must contain the primary key for the assertion type. +// It returns ErrNotFound if the assertion cannot be found. +func (db *Database) FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error) { + return find([]Backstore{db.trusted}, assertionType, headers, -1) +} + +// FindMany finds assertions based on arbitrary headers. +// It returns ErrNotFound if no assertion can be found. +func (db *Database) FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) { + err := checkAssertType(assertionType) + if err != nil { + return nil, err + } + res := []Assertion{} + + foundCb := func(assert Assertion) { + res = append(res, assert) + } + + // TODO: Find variant taking this + maxFormat := assertionType.MaxSupportedFormat() + for _, bs := range db.backstores { + err = bs.Search(assertionType, headers, foundCb, maxFormat) + if err != nil { + return nil, err + } + } + + if len(res) == 0 { + return nil, ErrNotFound + } + return res, nil +} + +// assertion checkers + +// CheckSigningKeyIsNotExpired checks that the signing key is not expired. +func CheckSigningKeyIsNotExpired(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error { + if signingKey == nil { + // assert isn't signed with an account-key key, CheckSignature + // will fail anyway unless we teach it more stuff, + // Also this check isn't so relevant for self-signed asserts + // (e.g. account-key-request) + return nil + } + if !signingKey.isKeyValidAt(checkTime) { + return fmt.Errorf("assertion is signed with expired public key %q from %q", assert.SignKeyID(), assert.AuthorityID()) + } + return nil +} + +// CheckSignature checks that the signature is valid. +func CheckSignature(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error { + var pubKey PublicKey + if signingKey != nil { + pubKey = signingKey.publicKey() + } else { + custom, ok := assert.(customSigner) + if !ok { + return fmt.Errorf("cannot check no-authority assertion type %q", assert.Type().Name) + } + pubKey = custom.signKey() + } + content, encSig := assert.Signature() + signature, err := decodeSignature(encSig) + if err != nil { + return err + } + err = pubKey.verify(content, signature) + if err != nil { + return fmt.Errorf("failed signature verification: %v", err) + } + return nil +} + +type timestamped interface { + Timestamp() time.Time +} + +// CheckTimestampVsSigningKeyValidity verifies that the timestamp of +// the assertion is within the signing key validity. +func CheckTimestampVsSigningKeyValidity(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error { + if signingKey == nil { + // assert isn't signed with an account-key key, CheckSignature + // will fail anyway unless we teach it more stuff. + // Also this check isn't so relevant for self-signed asserts + // (e.g. account-key-request) + return nil + } + if tstamped, ok := assert.(timestamped); ok { + checkTime := tstamped.Timestamp() + if !signingKey.isKeyValidAt(checkTime) { + return fmt.Errorf("%s assertion timestamp outside of signing key validity", assert.Type().Name) + } + } + return nil +} + +// XXX: keeping these in this form until we know better + +// A consistencyChecker performs further checks based on the full +// assertion database knowledge and its own signing key. +type consistencyChecker interface { + checkConsistency(roDB RODatabase, signingKey *AccountKey) error +} + +// CheckCrossConsistency verifies that the assertion is consistent with the other statements in the database. +func CheckCrossConsistency(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error { + // see if the assertion requires further checks + if checker, ok := assert.(consistencyChecker); ok { + return checker.checkConsistency(roDB, signingKey) + } + return nil +} + +// DefaultCheckers lists the default and recommended assertion +// checkers used by Database if none are specified in the +// DatabaseConfig.Checkers. +var DefaultCheckers = []Checker{ + CheckSigningKeyIsNotExpired, + CheckSignature, + CheckTimestampVsSigningKeyValidity, + CheckCrossConsistency, +} diff --git a/asserts/database_test.go b/asserts/database_test.go new file mode 100644 index 00000000..e2c30436 --- /dev/null +++ b/asserts/database_test.go @@ -0,0 +1,913 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts_test + +import ( + "bytes" + "crypto" + "encoding/base64" + "errors" + "io/ioutil" + "os" + "path/filepath" + "sort" + "testing" + "time" + + "golang.org/x/crypto/openpgp/packet" + "golang.org/x/crypto/sha3" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +func Test(t *testing.T) { TestingT(t) } + +var _ = Suite(&openSuite{}) +var _ = Suite(&revisionErrorSuite{}) + +type openSuite struct{} + +func (opens *openSuite) TestOpenDatabaseOK(c *C) { + cfg := &asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + c.Assert(db, NotNil) +} + +func (opens *openSuite) TestOpenDatabaseTrustedAccount(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "trusted", + "display-name": "Trusted", + "validation": "certified", + "timestamp": "2015-01-01T14:00:00Z", + } + acct, err := asserts.AssembleAndSignInTest(asserts.AccountType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + cfg := &asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: []asserts.Assertion{acct}, + } + + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + a, err := db.Find(asserts.AccountType, map[string]string{ + "account-id": "trusted", + }) + c.Assert(err, IsNil) + acct1 := a.(*asserts.Account) + c.Check(acct1.AccountID(), Equals, "trusted") + c.Check(acct1.DisplayName(), Equals, "Trusted") + + c.Check(db.IsTrustedAccount("trusted"), Equals, true) + + // empty account id (invalid) is not trusted + c.Check(db.IsTrustedAccount(""), Equals, false) +} + +func (opens *openSuite) TestOpenDatabaseTrustedWrongType(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "0", + } + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + cfg := &asserts.DatabaseConfig{ + Trusted: []asserts.Assertion{a}, + } + + _, err = asserts.OpenDatabase(cfg) + c.Assert(err, ErrorMatches, "cannot load trusted assertions that are not account-key or account: test-only") +} + +type databaseSuite struct { + topDir string + db *asserts.Database +} + +var _ = Suite(&databaseSuite{}) + +func (dbs *databaseSuite) SetUpTest(c *C) { + dbs.topDir = filepath.Join(c.MkDir(), "asserts-db") + fsKeypairMgr, err := asserts.OpenFSKeypairManager(dbs.topDir) + c.Assert(err, IsNil) + cfg := &asserts.DatabaseConfig{ + KeypairManager: fsKeypairMgr, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + dbs.db = db +} + +func (dbs *databaseSuite) TestImportKey(c *C) { + err := dbs.db.ImportKey(testPrivKey1) + c.Assert(err, IsNil) + + keyPath := filepath.Join(dbs.topDir, "private-keys-v1", testPrivKey1SHA3_384) + info, err := os.Stat(keyPath) + c.Assert(err, IsNil) + c.Check(info.Mode().Perm(), Equals, os.FileMode(0600)) // secret + // too white box? ok at least until we have more functionality + privKey, err := ioutil.ReadFile(keyPath) + c.Assert(err, IsNil) + + privKeyFromDisk, err := asserts.DecodePrivateKeyInTest(privKey) + c.Assert(err, IsNil) + + c.Check(privKeyFromDisk.PublicKey().ID(), Equals, testPrivKey1SHA3_384) +} + +func (dbs *databaseSuite) TestImportKeyAlreadyExists(c *C) { + err := dbs.db.ImportKey(testPrivKey1) + c.Assert(err, IsNil) + + err = dbs.db.ImportKey(testPrivKey1) + c.Check(err, ErrorMatches, "key pair with given key id already exists") +} + +func (dbs *databaseSuite) TestPublicKey(c *C) { + pk := testPrivKey1 + keyID := pk.PublicKey().ID() + err := dbs.db.ImportKey(pk) + c.Assert(err, IsNil) + + pubk, err := dbs.db.PublicKey(keyID) + c.Assert(err, IsNil) + c.Check(pubk.ID(), Equals, keyID) + + // usual pattern is to then encode it + encoded, err := asserts.EncodePublicKey(pubk) + c.Assert(err, IsNil) + data, err := base64.StdEncoding.DecodeString(string(encoded)) + c.Assert(err, IsNil) + c.Check(data[0], Equals, uint8(1)) // v1 + + // check details of packet + const newHeaderBits = 0x80 | 0x40 + c.Check(data[1]&newHeaderBits, Equals, uint8(newHeaderBits)) + c.Check(data[2] < 192, Equals, true) // small packet, 1 byte length + c.Check(data[3], Equals, uint8(4)) // openpgp v4 + pkt, err := packet.Read(bytes.NewBuffer(data[1:])) + c.Assert(err, IsNil) + pubKey, ok := pkt.(*packet.PublicKey) + c.Assert(ok, Equals, true) + c.Check(pubKey.PubKeyAlgo, Equals, packet.PubKeyAlgoRSA) + c.Check(pubKey.IsSubkey, Equals, false) + fixedTimestamp := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) + c.Check(pubKey.CreationTime.Equal(fixedTimestamp), Equals, true) + // hash of blob content == hash of key + h384 := sha3.Sum384(data) + encHash := base64.RawURLEncoding.EncodeToString(h384[:]) + c.Check(encHash, DeepEquals, testPrivKey1SHA3_384) +} + +func (dbs *databaseSuite) TestPublicKeyNotFound(c *C) { + pk := testPrivKey1 + keyID := pk.PublicKey().ID() + + _, err := dbs.db.PublicKey(keyID) + c.Check(err, ErrorMatches, "cannot find key pair") + + err = dbs.db.ImportKey(pk) + c.Assert(err, IsNil) + + _, err = dbs.db.PublicKey("ff" + keyID) + c.Check(err, ErrorMatches, "cannot find key pair") +} + +type checkSuite struct { + bs asserts.Backstore + a asserts.Assertion +} + +var _ = Suite(&checkSuite{}) + +func (chks *checkSuite) SetUpTest(c *C) { + var err error + + topDir := filepath.Join(c.MkDir(), "asserts-db") + chks.bs, err = asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "0", + } + chks.a, err = asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) +} + +func (chks *checkSuite) TestCheckNoPubKey(c *C) { + cfg := &asserts.DatabaseConfig{ + Backstore: chks.bs, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + err = db.Check(chks.a) + c.Assert(err, ErrorMatches, `no matching public key "[[:alnum:]_-]+" for signature by "canonical"`) +} + +func (chks *checkSuite) TestCheckExpiredPubKey(c *C) { + trustedKey := testPrivKey0 + + cfg := &asserts.DatabaseConfig{ + Backstore: chks.bs, + Trusted: []asserts.Assertion{asserts.ExpiredAccountKeyForTest("canonical", trustedKey.PublicKey())}, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + err = db.Check(chks.a) + c.Assert(err, ErrorMatches, `assertion is signed with expired public key "[[:alnum:]_-]+" from "canonical"`) +} + +func (chks *checkSuite) TestCheckForgery(c *C) { + trustedKey := testPrivKey0 + + cfg := &asserts.DatabaseConfig{ + Backstore: chks.bs, + Trusted: []asserts.Assertion{asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey())}, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + encoded := asserts.Encode(chks.a) + content, encodedSig := chks.a.Signature() + // forgery + forgedSig := new(packet.Signature) + forgedSig.PubKeyAlgo = packet.PubKeyAlgoRSA + forgedSig.Hash = crypto.SHA512 + forgedSig.CreationTime = time.Now() + h := crypto.SHA512.New() + h.Write(content) + pk1 := packet.NewRSAPrivateKey(time.Unix(1, 0), testPrivKey1RSA) + err = forgedSig.Sign(h, pk1, &packet.Config{DefaultHash: crypto.SHA512}) + c.Assert(err, IsNil) + buf := new(bytes.Buffer) + forgedSig.Serialize(buf) + b := append([]byte{0x1}, buf.Bytes()...) + forgedSigEncoded := base64.StdEncoding.EncodeToString(b) + forgedEncoded := bytes.Replace(encoded, encodedSig, []byte(forgedSigEncoded), 1) + c.Assert(forgedEncoded, Not(DeepEquals), encoded) + + forgedAssert, err := asserts.Decode(forgedEncoded) + c.Assert(err, IsNil) + + err = db.Check(forgedAssert) + c.Assert(err, ErrorMatches, "failed signature verification: .*") +} + +func (chks *checkSuite) TestCheckUnsupportedFormat(c *C) { + trustedKey := testPrivKey0 + + cfg := &asserts.DatabaseConfig{ + Backstore: chks.bs, + Trusted: []asserts.Assertion{asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey())}, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + var a asserts.Assertion + (func() { + restore := asserts.MockMaxSupportedFormat(asserts.TestOnlyType, 77) + defer restore() + var err error + + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "0", + "format": "77", + } + a, err = asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, trustedKey) + c.Assert(err, IsNil) + })() + + err = db.Check(a) + c.Assert(err, FitsTypeOf, &asserts.UnsupportedFormatError{}) + c.Check(err, ErrorMatches, `proposed "test-only" assertion has format 77 but 1 is latest supported`) +} + +type signAddFindSuite struct { + signingDB *asserts.Database + signingKeyID string + db *asserts.Database +} + +var _ = Suite(&signAddFindSuite{}) + +func (safs *signAddFindSuite) SetUpTest(c *C) { + cfg0 := &asserts.DatabaseConfig{} + db0, err := asserts.OpenDatabase(cfg0) + c.Assert(err, IsNil) + safs.signingDB = db0 + + pk := testPrivKey0 + err = db0.ImportKey(pk) + c.Assert(err, IsNil) + safs.signingKeyID = pk.PublicKey().ID() + + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + trustedKey := testPrivKey0 + cfg := &asserts.DatabaseConfig{ + Backstore: bs, + Trusted: []asserts.Assertion{ + asserts.BootstrapAccountForTest("canonical"), + asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey()), + }, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + safs.db = db +} + +func (safs *signAddFindSuite) TestSign(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Check(a1) + c.Check(err, IsNil) +} + +func (safs *signAddFindSuite) TestSignEmptyKeyID(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, "") + c.Assert(err, ErrorMatches, "key id is empty") + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignMissingAuthorityId(c *C) { + headers := map[string]interface{}{ + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `"authority-id" header is mandatory`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignMissingPrimaryKey(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `"primary-key" header is mandatory`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignPrimaryKeyWithSlash(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "baz/9000", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `"primary-key" primary key header cannot contain '/'`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignNoPrivateKey(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, "abcd") + c.Assert(err, ErrorMatches, "cannot find key pair") + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignUnknownType(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + } + a1, err := safs.signingDB.Sign(&asserts.AssertionType{Name: "xyz", PrimaryKey: nil}, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `internal error: unknown assertion type: "xyz"`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignNonPredefinedType(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + } + a1, err := safs.signingDB.Sign(&asserts.AssertionType{Name: "test-only", PrimaryKey: nil}, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `internal error: unpredefined assertion type for name "test-only" used.*`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignBadRevision(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "revision": "zzz", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `"revision" header is not an integer: zzz`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignBadFormat(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "format": "zzz", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `"format" header is not an integer: zzz`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignHeadersCheck(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "extra": []interface{}{1, 2}, + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Check(err, ErrorMatches, `header "extra": header values must be strings or nested lists or maps with strings as the only scalars: 1`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignHeadersCheckMap(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "extra": map[string]interface{}{"a": "a", "b": 1}, + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Check(err, ErrorMatches, `header "extra": header values must be strings or nested lists or maps with strings as the only scalars: 1`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignAssemblerError(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "count": "zzz", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `cannot assemble assertion test-only: "count" header is not an integer: zzz`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignUnsupportedFormat(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "format": "77", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `cannot sign "test-only" assertion with format 77 higher than max supported format 1`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestAddSuperseding(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(a1) + c.Assert(err, IsNil) + + retrieved1, err := safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "a", + }) + c.Assert(err, IsNil) + c.Check(retrieved1, NotNil) + c.Check(retrieved1.Revision(), Equals, 0) + + headers["revision"] = "1" + a2, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(a2) + c.Assert(err, IsNil) + + retrieved2, err := safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "a", + }) + c.Assert(err, IsNil) + c.Check(retrieved2, NotNil) + c.Check(retrieved2.Revision(), Equals, 1) + + err = safs.db.Add(a1) + c.Check(err, ErrorMatches, "revision 0 is older than current revision 1") + c.Check(asserts.IsUnaccceptedUpdate(err), Equals, true) +} + +func (safs *signAddFindSuite) TestAddNoAuthorityNoPrimaryKey(c *C) { + headers := map[string]interface{}{ + "hdr": "FOO", + } + a, err := asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + err = safs.db.Add(a) + c.Assert(err, ErrorMatches, `internal error: assertion type "test-only-no-authority" has no primary key`) +} + +func (safs *signAddFindSuite) TestAddNoAuthorityButPrimaryKey(c *C) { + headers := map[string]interface{}{ + "pk": "primary", + } + a, err := asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityPKType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + err = safs.db.Add(a) + c.Assert(err, ErrorMatches, `cannot check no-authority assertion type "test-only-no-authority-pk"`) +} + +func (safs *signAddFindSuite) TestAddUnsupportedFormat(c *C) { + const unsupported = "type: test-only\n" + + "format: 77\n" + + "authority-id: canonical\n" + + "primary-key: a\n" + + "payload: unsupported\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + aUnsupp, err := asserts.Decode([]byte(unsupported)) + c.Assert(err, IsNil) + c.Assert(aUnsupp.SupportedFormat(), Equals, false) + + err = safs.db.Add(aUnsupp) + c.Assert(err, FitsTypeOf, &asserts.UnsupportedFormatError{}) + c.Check(err.(*asserts.UnsupportedFormatError).Update, Equals, false) + c.Check(err, ErrorMatches, `proposed "test-only" assertion has format 77 but 1 is latest supported`) + c.Check(asserts.IsUnaccceptedUpdate(err), Equals, false) + + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "format": "1", + "payload": "supported", + } + aSupp, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + err = safs.db.Add(aSupp) + c.Assert(err, IsNil) + + err = safs.db.Add(aUnsupp) + c.Assert(err, FitsTypeOf, &asserts.UnsupportedFormatError{}) + c.Check(err.(*asserts.UnsupportedFormatError).Update, Equals, true) + c.Check(err, ErrorMatches, `proposed "test-only" assertion has format 77 but 1 is latest supported \(current not updated\)`) + c.Check(asserts.IsUnaccceptedUpdate(err), Equals, true) +} + +func (safs *signAddFindSuite) TestFindNotFound(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(a1) + c.Assert(err, IsNil) + + retrieved1, err := safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "b", + }) + c.Assert(err, Equals, asserts.ErrNotFound) + c.Check(retrieved1, IsNil) + + // checking also extra headers + retrieved1, err = safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "a", + "authority-id": "other-auth-id", + }) + c.Assert(err, Equals, asserts.ErrNotFound) + c.Check(retrieved1, IsNil) +} + +func (safs *signAddFindSuite) TestFindPrimaryLeftOut(c *C) { + retrieved1, err := safs.db.Find(asserts.TestOnlyType, map[string]string{}) + c.Assert(err, ErrorMatches, "must provide primary key: primary-key") + c.Check(retrieved1, IsNil) +} + +func (safs *signAddFindSuite) TestFindMany(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "other": "other-x", + } + aa, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + err = safs.db.Add(aa) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "b", + "other": "other-y", + } + ab, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + err = safs.db.Add(ab) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "c", + "other": "other-x", + } + ac, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + err = safs.db.Add(ac) + c.Assert(err, IsNil) + + res, err := safs.db.FindMany(asserts.TestOnlyType, map[string]string{ + "other": "other-x", + }) + c.Assert(err, IsNil) + c.Assert(res, HasLen, 2) + primKeys := []string{res[0].HeaderString("primary-key"), res[1].HeaderString("primary-key")} + sort.Strings(primKeys) + c.Check(primKeys, DeepEquals, []string{"a", "c"}) + + res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{ + "other": "other-y", + }) + c.Assert(err, IsNil) + c.Assert(res, HasLen, 1) + c.Check(res[0].Header("primary-key"), Equals, "b") + + res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{}) + c.Assert(err, IsNil) + c.Assert(res, HasLen, 3) + + res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{ + "primary-key": "b", + "other": "other-y", + }) + c.Assert(err, IsNil) + c.Assert(res, HasLen, 1) + + res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{ + "primary-key": "b", + "other": "other-x", + }) + c.Assert(res, HasLen, 0) + c.Check(err, Equals, asserts.ErrNotFound) +} + +func (safs *signAddFindSuite) TestFindFindsTrustedAccountKeys(c *C) { + pk1 := testPrivKey1 + + acct1 := assertstest.NewAccount(safs.signingDB, "acc-id1", map[string]interface{}{ + "authority-id": "canonical", + }, safs.signingKeyID) + + acct1Key := assertstest.NewAccountKey(safs.signingDB, acct1, map[string]interface{}{ + "authority-id": "canonical", + }, pk1.PublicKey(), safs.signingKeyID) + + err := safs.db.Add(acct1) + c.Assert(err, IsNil) + err = safs.db.Add(acct1Key) + c.Assert(err, IsNil) + + // find the trusted key as well + tKey, err := safs.db.Find(asserts.AccountKeyType, map[string]string{ + "account-id": "canonical", + "public-key-sha3-384": safs.signingKeyID, + }) + c.Assert(err, IsNil) + c.Assert(tKey.(*asserts.AccountKey).AccountID(), Equals, "canonical") + c.Assert(tKey.(*asserts.AccountKey).PublicKeyID(), Equals, safs.signingKeyID) + + // find trusted and indirectly trusted + accKeys, err := safs.db.FindMany(asserts.AccountKeyType, nil) + c.Assert(err, IsNil) + c.Check(accKeys, HasLen, 2) +} + +func (safs *signAddFindSuite) TestFindTrusted(c *C) { + pk1 := testPrivKey1 + + acct1 := assertstest.NewAccount(safs.signingDB, "acc-id1", map[string]interface{}{ + "authority-id": "canonical", + }, safs.signingKeyID) + + acct1Key := assertstest.NewAccountKey(safs.signingDB, acct1, map[string]interface{}{ + "authority-id": "canonical", + }, pk1.PublicKey(), safs.signingKeyID) + + err := safs.db.Add(acct1) + c.Assert(err, IsNil) + err = safs.db.Add(acct1Key) + c.Assert(err, IsNil) + + // find the trusted account + tAcct, err := safs.db.FindTrusted(asserts.AccountType, map[string]string{ + "account-id": "canonical", + }) + c.Assert(err, IsNil) + c.Assert(tAcct.(*asserts.Account).AccountID(), Equals, "canonical") + + // find the trusted key + tKey, err := safs.db.FindTrusted(asserts.AccountKeyType, map[string]string{ + "account-id": "canonical", + "public-key-sha3-384": safs.signingKeyID, + }) + c.Assert(err, IsNil) + c.Assert(tKey.(*asserts.AccountKey).AccountID(), Equals, "canonical") + c.Assert(tKey.(*asserts.AccountKey).PublicKeyID(), Equals, safs.signingKeyID) + + // doesn't find not trusted assertions + _, err = safs.db.FindTrusted(asserts.AccountType, map[string]string{ + "account-id": acct1.AccountID(), + }) + c.Check(err, Equals, asserts.ErrNotFound) + + _, err = safs.db.FindTrusted(asserts.AccountKeyType, map[string]string{ + "account-id": acct1.AccountID(), + "public-key-sha3-384": acct1Key.PublicKeyID(), + }) + c.Check(err, Equals, asserts.ErrNotFound) +} + +func (safs *signAddFindSuite) TestDontLetAddConfusinglyAssertionClashingWithTrustedOnes(c *C) { + // trusted + pubKey0, err := safs.signingDB.PublicKey(safs.signingKeyID) + c.Assert(err, IsNil) + pubKey0Encoded, err := asserts.EncodePublicKey(pubKey0) + c.Assert(err, IsNil) + + now := time.Now().UTC() + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "canonical", + "public-key-sha3-384": safs.signingKeyID, + "name": "default", + "since": now.Format(time.RFC3339), + "until": now.AddDate(1, 0, 0).Format(time.RFC3339), + } + tKey, err := safs.signingDB.Sign(asserts.AccountKeyType, headers, []byte(pubKey0Encoded), safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(tKey) + c.Check(err, ErrorMatches, `cannot add "account-key" assertion with primary key clashing with a trusted assertion: .*`) +} + +func (safs *signAddFindSuite) TestFindAndRefResolve(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "pk1": "ka", + "pk2": "kb", + } + a1, err := safs.signingDB.Sign(asserts.TestOnly2Type, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(a1) + c.Assert(err, IsNil) + + ref := &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"ka", "kb"}, + } + + resolved, err := ref.Resolve(safs.db.Find) + c.Assert(err, IsNil) + c.Check(resolved.Headers(), DeepEquals, map[string]interface{}{ + "type": "test-only-2", + "authority-id": "canonical", + "pk1": "ka", + "pk2": "kb", + "sign-key-sha3-384": resolved.SignKeyID(), + }) + + ref = &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"kb", "ka"}, + } + _, err = ref.Resolve(safs.db.Find) + c.Assert(err, Equals, asserts.ErrNotFound) +} + +func (safs *signAddFindSuite) TestFindMaxFormat(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "foo", + } + af0, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(af0) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "foo", + "format": "1", + "revision": "1", + } + af1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(af1) + c.Assert(err, IsNil) + + a, err := safs.db.FindMaxFormat(asserts.TestOnlyType, map[string]string{ + "primary-key": "foo", + }, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + + a, err = safs.db.FindMaxFormat(asserts.TestOnlyType, map[string]string{ + "primary-key": "foo", + }, 0) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + + a, err = safs.db.FindMaxFormat(asserts.TestOnlyType, map[string]string{ + "primary-key": "foo", + }, 3) + c.Check(err, ErrorMatches, `cannot find "test-only" assertions for format 3 higher than supported format 1`) +} + +type revisionErrorSuite struct{} + +func (res *revisionErrorSuite) TestErrorText(c *C) { + tests := []struct { + err error + expected string + }{ + // Invalid revisions. + {&asserts.RevisionError{Used: -1}, "assertion revision is unknown"}, + {&asserts.RevisionError{Used: -100}, "assertion revision is unknown"}, + {&asserts.RevisionError{Current: -1}, "assertion revision is unknown"}, + {&asserts.RevisionError{Current: -100}, "assertion revision is unknown"}, + {&asserts.RevisionError{Used: -1, Current: -1}, "assertion revision is unknown"}, + // Used == Current. + {&asserts.RevisionError{}, "revision 0 is already the current revision"}, + {&asserts.RevisionError{Used: 100, Current: 100}, "revision 100 is already the current revision"}, + // Used < Current. + {&asserts.RevisionError{Used: 1, Current: 2}, "revision 1 is older than current revision 2"}, + {&asserts.RevisionError{Used: 2, Current: 100}, "revision 2 is older than current revision 100"}, + // Used > Current. + {&asserts.RevisionError{Current: 1, Used: 2}, "revision 2 is more recent than current revision 1"}, + {&asserts.RevisionError{Current: 2, Used: 100}, "revision 100 is more recent than current revision 2"}, + } + + for _, test := range tests { + c.Check(test.err, ErrorMatches, test.expected) + } +} + +type isUnacceptedUpdateSuite struct{} + +func (s *isUnacceptedUpdateSuite) TestIsUnacceptedUpdate(c *C) { + tests := []struct { + err error + keptCurrent bool + }{ + {&asserts.UnsupportedFormatError{}, false}, + {&asserts.UnsupportedFormatError{Update: true}, true}, + {&asserts.RevisionError{Used: 1, Current: 1}, true}, + {&asserts.RevisionError{Used: 1, Current: 5}, true}, + {&asserts.RevisionError{Used: 3, Current: 1}, false}, + {errors.New("other error"), false}, + {asserts.ErrNotFound, false}, + } + + for _, t := range tests { + c.Check(asserts.IsUnaccceptedUpdate(t.err), Equals, t.keptCurrent, Commentf("%v", t.err)) + } +} diff --git a/asserts/device_asserts.go b/asserts/device_asserts.go new file mode 100644 index 00000000..1d10a268 --- /dev/null +++ b/asserts/device_asserts.go @@ -0,0 +1,420 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts + +import ( + "fmt" + "regexp" + "strings" + "time" +) + +// Model holds a model assertion, which is a statement by a brand +// about the properties of a device model. +type Model struct { + assertionBase + requiredSnaps []string + sysUserAuthority []string + timestamp time.Time +} + +// BrandID returns the brand identifier. Same as the authority id. +func (mod *Model) BrandID() string { + return mod.HeaderString("brand-id") +} + +// Model returns the model name identifier. +func (mod *Model) Model() string { + return mod.HeaderString("model") +} + +// DisplayName returns the human-friendly name of the model or +// falls back to Model if this was not set. +func (mod *Model) DisplayName() string { + display := mod.HeaderString("display-name") + if display == "" { + return mod.Model() + } + return display +} + +// Series returns the series of the core software the model uses. +func (mod *Model) Series() string { + return mod.HeaderString("series") +} + +// Architecture returns the archicteture the model is based on. +func (mod *Model) Architecture() string { + return mod.HeaderString("architecture") +} + +// Gadget returns the gadget snap the model uses. +func (mod *Model) Gadget() string { + return mod.HeaderString("gadget") +} + +// Kernel returns the kernel snap the model uses. +func (mod *Model) Kernel() string { + return mod.HeaderString("kernel") +} + +// Store returns the snap store the model uses. +func (mod *Model) Store() string { + return mod.HeaderString("store") +} + +// RequiredSnaps returns the snaps that must be installed at all times and cannot be removed for this model. +func (mod *Model) RequiredSnaps() []string { + return mod.requiredSnaps +} + +// SystemUserAuthority returns the authority ids that are accepted as signers of system-user assertions for this model. Empty list means any. +func (mod *Model) SystemUserAuthority() []string { + return mod.sysUserAuthority +} + +// Timestamp returns the time when the model assertion was issued. +func (mod *Model) Timestamp() time.Time { + return mod.timestamp +} + +// Implement further consistency checks. +func (mod *Model) checkConsistency(db RODatabase, acck *AccountKey) error { + // TODO: double check trust level of authority depending on class and possibly allowed-modes + return nil +} + +// sanity +var _ consistencyChecker = (*Model)(nil) + +// limit model to only lowercase for now +var validModel = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") + +func checkModel(headers map[string]interface{}) (string, error) { + s, err := checkStringMatches(headers, "model", validModel) + if err != nil { + return "", err + } + // TODO: support the concept of case insensitive/preserving string headers + if strings.ToLower(s) != s { + return "", fmt.Errorf(`"model" header cannot contain uppercase letters`) + } + return s, nil +} + +func checkAuthorityMatchesBrand(a Assertion) error { + typeName := a.Type().Name + authorityID := a.AuthorityID() + brand := a.HeaderString("brand-id") + if brand != authorityID { + return fmt.Errorf("authority-id and brand-id must match, %s assertions are expected to be signed by the brand: %q != %q", typeName, authorityID, brand) + } + return nil +} + +var ( + validAccountID = regexp.MustCompile("^(?:[a-z0-9A-Z]{32}|[-a-z0-9]{2,28})$") // account ids look like snap-ids or are nice identifier +) + +func checkOptionalSystemUserAuthority(headers map[string]interface{}, brandID string) ([]string, error) { + const name = "system-user-authority" + v, ok := headers[name] + if !ok { + return []string{brandID}, nil + } + switch x := v.(type) { + case string: + if x == "*" { + return nil, nil + } + case []interface{}: + lst, err := checkStringListMatches(headers, name, validAccountID) + if err == nil { + return lst, nil + } + } + return nil, fmt.Errorf("%q header must be '*' or a list of account ids", name) +} + +var modelMandatory = []string{"architecture", "gadget", "kernel"} + +func assembleModel(assert assertionBase) (Assertion, error) { + err := checkAuthorityMatchesBrand(&assert) + if err != nil { + return nil, err + } + + _, err = checkModel(assert.headers) + if err != nil { + return nil, err + } + + for _, mandatory := range modelMandatory { + if _, err := checkNotEmptyString(assert.headers, mandatory); err != nil { + return nil, err + } + } + + // store is optional but must be a string, defaults to the ubuntu store + _, err = checkOptionalString(assert.headers, "store") + if err != nil { + return nil, err + } + + // display-name is optional but must be a string + _, err = checkOptionalString(assert.headers, "display-name") + if err != nil { + return nil, err + } + + reqSnaps, err := checkStringList(assert.headers, "required-snaps") + if err != nil { + return nil, err + } + + sysUserAuthority, err := checkOptionalSystemUserAuthority(assert.headers, assert.HeaderString("brand-id")) + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + // NB: + // * core is not supported at this time, it defaults to ubuntu-core + // in prepare-image until rename and/or introduction of the header. + // * some form of allowed-modes, class are postponed, + // + // prepare-image takes care of not allowing them for now + + // ignore extra headers and non-empty body for future compatibility + return &Model{ + assertionBase: assert, + requiredSnaps: reqSnaps, + sysUserAuthority: sysUserAuthority, + timestamp: timestamp, + }, nil +} + +// Serial holds a serial assertion, which is a statement binding a +// device identity with the device public key. +type Serial struct { + assertionBase + timestamp time.Time + pubKey PublicKey +} + +// BrandID returns the brand identifier of the device. +func (ser *Serial) BrandID() string { + return ser.HeaderString("brand-id") +} + +// Model returns the model name identifier of the device. +func (ser *Serial) Model() string { + return ser.HeaderString("model") +} + +// Serial returns the serial identifier of the device, together with +// brand id and model they form the unique identifier of the device. +func (ser *Serial) Serial() string { + return ser.HeaderString("serial") +} + +// DeviceKey returns the public key of the device. +func (ser *Serial) DeviceKey() PublicKey { + return ser.pubKey +} + +// Timestamp returns the time when the serial assertion was issued. +func (ser *Serial) Timestamp() time.Time { + return ser.timestamp +} + +// TODO: implement further consistency checks for Serial but first review approach + +func assembleSerial(assert assertionBase) (Assertion, error) { + err := checkAuthorityMatchesBrand(&assert) + if err != nil { + return nil, err + } + + _, err = checkModel(assert.headers) + if err != nil { + return nil, err + } + + encodedKey, err := checkNotEmptyString(assert.headers, "device-key") + if err != nil { + return nil, err + } + pubKey, err := DecodePublicKey([]byte(encodedKey)) + if err != nil { + return nil, err + } + keyID, err := checkNotEmptyString(assert.headers, "device-key-sha3-384") + if err != nil { + return nil, err + } + if keyID != pubKey.ID() { + return nil, fmt.Errorf("device key does not match provided key id") + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + // ignore extra headers and non-empty body for future compatibility + return &Serial{ + assertionBase: assert, + timestamp: timestamp, + pubKey: pubKey, + }, nil +} + +// SerialRequest holds a serial-request assertion, which is a self-signed request to obtain a full device identity bound to the device public key. +type SerialRequest struct { + assertionBase + pubKey PublicKey +} + +// BrandID returns the brand identifier of the device making the request. +func (sreq *SerialRequest) BrandID() string { + return sreq.HeaderString("brand-id") +} + +// Model returns the model name identifier of the device making the request. +func (sreq *SerialRequest) Model() string { + return sreq.HeaderString("model") +} + +// Serial returns the optional proposed serial identifier for the device, the service taking the request might use it or ignore it. +func (sreq *SerialRequest) Serial() string { + return sreq.HeaderString("serial") +} + +// RequestID returns the id for the request, obtained from and to be presented to the serial signing service. +func (sreq *SerialRequest) RequestID() string { + return sreq.HeaderString("request-id") +} + +// DeviceKey returns the public key of the device making the request. +func (sreq *SerialRequest) DeviceKey() PublicKey { + return sreq.pubKey +} + +func assembleSerialRequest(assert assertionBase) (Assertion, error) { + _, err := checkNotEmptyString(assert.headers, "brand-id") + if err != nil { + return nil, err + } + + _, err = checkModel(assert.headers) + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "request-id") + if err != nil { + return nil, err + } + + _, err = checkOptionalString(assert.headers, "serial") + if err != nil { + return nil, err + } + + encodedKey, err := checkNotEmptyString(assert.headers, "device-key") + if err != nil { + return nil, err + } + pubKey, err := DecodePublicKey([]byte(encodedKey)) + if err != nil { + return nil, err + } + + if pubKey.ID() != assert.SignKeyID() { + return nil, fmt.Errorf("device key does not match included signing key id") + } + + // ignore extra headers and non-empty body for future compatibility + return &SerialRequest{ + assertionBase: assert, + pubKey: pubKey, + }, nil +} + +// DeviceSessionRequest holds a device-session-request assertion, which is a request wrapping a store-provided nonce to start a session by a device signed with its key. +type DeviceSessionRequest struct { + assertionBase + timestamp time.Time +} + +// BrandID returns the brand identifier of the device making the request. +func (req *DeviceSessionRequest) BrandID() string { + return req.HeaderString("brand-id") +} + +// Model returns the model name identifier of the device making the request. +func (req *DeviceSessionRequest) Model() string { + return req.HeaderString("model") +} + +// Serial returns the serial identifier of the device making the request, +// together with brand id and model it forms the unique identifier of +// the device. +func (req *DeviceSessionRequest) Serial() string { + return req.HeaderString("serial") +} + +// Nonce returns the nonce obtained from store and to be presented when requesting a device session. +func (req *DeviceSessionRequest) Nonce() string { + return req.HeaderString("nonce") +} + +// Timestamp returns the time when the device-session-request was created. +func (req *DeviceSessionRequest) Timestamp() time.Time { + return req.timestamp +} + +func assembleDeviceSessionRequest(assert assertionBase) (Assertion, error) { + _, err := checkModel(assert.headers) + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "nonce") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + // ignore extra headers and non-empty body for future compatibility + return &DeviceSessionRequest{ + assertionBase: assert, + timestamp: timestamp, + }, nil +} diff --git a/asserts/device_asserts_test.go b/asserts/device_asserts_test.go new file mode 100644 index 00000000..b4686838 --- /dev/null +++ b/asserts/device_asserts_test.go @@ -0,0 +1,470 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts_test + +import ( + "strings" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type modelSuite struct { + ts time.Time + tsLine string +} + +var ( + _ = Suite(&modelSuite{}) + _ = Suite(&serialSuite{}) +) + +func (mods *modelSuite) SetUpSuite(c *C) { + mods.ts = time.Now().Truncate(time.Second).UTC() + mods.tsLine = "timestamp: " + mods.ts.Format(time.RFC3339) + "\n" +} + +const ( + reqSnaps = "required-snaps:\n - foo\n - bar\n" + sysUserAuths = "system-user-authority: *\n" +) + +const modelExample = "type: model\n" + + "authority-id: brand-id1\n" + + "series: 16\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "display-name: Baz 3000\n" + + "architecture: amd64\n" + + "gadget: brand-gadget\n" + + "kernel: baz-linux\n" + + "store: brand-store\n" + + sysUserAuths + + reqSnaps + + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + +func (mods *modelSuite) TestDecodeOK(c *C) { + encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(model.AuthorityID(), Equals, "brand-id1") + c.Check(model.Timestamp(), Equals, mods.ts) + c.Check(model.Series(), Equals, "16") + c.Check(model.BrandID(), Equals, "brand-id1") + c.Check(model.Model(), Equals, "baz-3000") + c.Check(model.DisplayName(), Equals, "Baz 3000") + c.Check(model.Architecture(), Equals, "amd64") + c.Check(model.Gadget(), Equals, "brand-gadget") + c.Check(model.Kernel(), Equals, "baz-linux") + c.Check(model.Store(), Equals, "brand-store") + c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"}) + c.Check(model.SystemUserAuthority(), HasLen, 0) +} + +func (mods *modelSuite) TestDecodeStoreIsOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, "store: brand-store\n", "store: \n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.Store(), Equals, "") + + encoded = strings.Replace(withTimestamp, "store: brand-store\n", "", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model = a.(*asserts.Model) + c.Check(model.Store(), Equals, "") +} + +func (mods *modelSuite) TestDecodeDisplayNameIsOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, "display-name: Baz 3000\n", "display-name: \n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + // optional but we fallback to Model + c.Check(model.DisplayName(), Equals, "baz-3000") + + encoded = strings.Replace(withTimestamp, "display-name: Baz 3000\n", "", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model = a.(*asserts.Model) + // optional but we fallback to Model + c.Check(model.DisplayName(), Equals, "baz-3000") +} + +func (mods *modelSuite) TestDecodeRequiredSnapsAreOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, reqSnaps, "", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.RequiredSnaps(), HasLen, 0) +} + +func (mods *modelSuite) TestDecodeSystemUserAuthorityIsOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, sysUserAuths, "", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + // the default is just to accept the brand itself + c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1"}) + + encoded = strings.Replace(withTimestamp, sysUserAuths, "system-user-authority:\n - foo\n - bar\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model = a.(*asserts.Model) + c.Check(model.SystemUserAuthority(), DeepEquals, []string{"foo", "bar"}) +} + +const ( + modelErrPrefix = "assertion model: " +) + +func (mods *modelSuite) TestDecodeInvalid(c *C) { + encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series: 16\n", "", `"series" header is mandatory`}, + {"series: 16\n", "series: \n", `"series" header should not be empty`}, + {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`}, + {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`}, + {"brand-id: brand-id1\n", "brand-id: random\n", `authority-id and brand-id must match, model assertions are expected to be signed by the brand: "brand-id1" != "random"`}, + {"model: baz-3000\n", "", `"model" header is mandatory`}, + {"model: baz-3000\n", "model: \n", `"model" header should not be empty`}, + {"model: baz-3000\n", "model: baz/3000\n", `"model" primary key header cannot contain '/'`}, + // lift this restriction at a later point + {"model: baz-3000\n", "model: BAZ-3000\n", `"model" header cannot contain uppercase letters`}, + {"display-name: Baz 3000\n", "display-name:\n - xyz\n", `"display-name" header must be a string`}, + {"architecture: amd64\n", "", `"architecture" header is mandatory`}, + {"architecture: amd64\n", "architecture: \n", `"architecture" header should not be empty`}, + {"gadget: brand-gadget\n", "", `"gadget" header is mandatory`}, + {"gadget: brand-gadget\n", "gadget: \n", `"gadget" header should not be empty`}, + {"kernel: baz-linux\n", "", `"kernel" header is mandatory`}, + {"kernel: baz-linux\n", "kernel: \n", `"kernel" header should not be empty`}, + {"store: brand-store\n", "store:\n - xyz\n", `"store" header must be a string`}, + {mods.tsLine, "", `"timestamp" header is mandatory`}, + {mods.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {mods.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + {reqSnaps, "required-snaps: foo\n", `"required-snaps" header must be a list of strings`}, + {reqSnaps, "required-snaps:\n -\n - nested\n", `"required-snaps" header must be a list of strings`}, + {sysUserAuths, "system-user-authority:\n a: 1\n", `"system-user-authority" header must be '\*' or a list of account ids`}, + {sysUserAuths, "system-user-authority:\n - 5_6\n", `"system-user-authority" header must be '\*' or a list of account ids`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr) + } +} + +func (mods *modelSuite) TestModelCheck(c *C) { + ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1))) + c.Assert(err, IsNil) + + storeDB, db := makeStoreAndCheckDB(c) + brandDB := setup3rdPartySigning(c, "brand1", storeDB, db) + + headers := ex.Headers() + headers["brand-id"] = brandDB.AuthorityID + headers["timestamp"] = time.Now().Format(time.RFC3339) + model, err := brandDB.Sign(asserts.ModelType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(model) + c.Assert(err, IsNil) +} + +func (mods *modelSuite) TestModelCheckInconsistentTimestamp(c *C) { + ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1))) + c.Assert(err, IsNil) + + storeDB, db := makeStoreAndCheckDB(c) + brandDB := setup3rdPartySigning(c, "brand1", storeDB, db) + + headers := ex.Headers() + headers["brand-id"] = brandDB.AuthorityID + headers["timestamp"] = "2011-01-01T14:00:00Z" + model, err := brandDB.Sign(asserts.ModelType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(model) + c.Assert(err, ErrorMatches, "model assertion timestamp outside of signing key validity") +} + +type serialSuite struct { + ts time.Time + tsLine string + deviceKey asserts.PrivateKey + encodedDevKey string +} + +func (ss *serialSuite) SetUpSuite(c *C) { + ss.ts = time.Now().Truncate(time.Second).UTC() + ss.tsLine = "timestamp: " + ss.ts.Format(time.RFC3339) + "\n" + + ss.deviceKey = testPrivKey2 + encodedPubKey, err := asserts.EncodePublicKey(ss.deviceKey.PublicKey()) + c.Assert(err, IsNil) + ss.encodedDevKey = string(encodedPubKey) +} + +const serialExample = "type: serial\n" + + "authority-id: brand-id1\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "serial: 2700\n" + + "device-key:\n DEVICEKEY\n" + + "device-key-sha3-384: KEYID\n" + + "TSLINE" + + "body-length: 2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "HW" + + "\n\n" + + "AXNpZw==" + +func (ss *serialSuite) TestDecodeOK(c *C) { + encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1) + encoded = strings.Replace(encoded, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1) + encoded = strings.Replace(encoded, "KEYID", ss.deviceKey.PublicKey().ID(), 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SerialType) + serial := a.(*asserts.Serial) + c.Check(serial.AuthorityID(), Equals, "brand-id1") + c.Check(serial.Timestamp(), Equals, ss.ts) + c.Check(serial.BrandID(), Equals, "brand-id1") + c.Check(serial.Model(), Equals, "baz-3000") + c.Check(serial.Serial(), Equals, "2700") + c.Check(serial.DeviceKey().ID(), Equals, ss.deviceKey.PublicKey().ID()) +} + +const ( + deviceSessReqErrPrefix = "assertion device-session-request: " + serialErrPrefix = "assertion serial: " + serialProofErrPrefix = "assertion serial-proof: " + serialReqErrPrefix = "assertion serial-request: " +) + +func (ss *serialSuite) TestDecodeInvalid(c *C) { + encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1) + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`}, + {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`}, + {"authority-id: brand-id1\n", "authority-id: random\n", `authority-id and brand-id must match, serial assertions are expected to be signed by the brand: "random" != "brand-id1"`}, + {"model: baz-3000\n", "", `"model" header is mandatory`}, + {"model: baz-3000\n", "model: \n", `"model" header should not be empty`}, + {"serial: 2700\n", "", `"serial" header is mandatory`}, + {"serial: 2700\n", "serial: \n", `"serial" header should not be empty`}, + {ss.tsLine, "", `"timestamp" header is mandatory`}, + {ss.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {ss.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + {"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`}, + {"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`}, + {"device-key:\n DEVICEKEY\n", "device-key: $$$\n", `cannot decode public key: .*`}, + {"device-key-sha3-384: KEYID\n", "", `"device-key-sha3-384" header is mandatory`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1) + invalid = strings.Replace(invalid, "KEYID", ss.deviceKey.PublicKey().ID(), 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, serialErrPrefix+test.expectedErr) + } +} + +func (ss *serialSuite) TestDecodeKeyIDMismatch(c *C) { + invalid := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1) + invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1) + invalid = strings.Replace(invalid, "KEYID", "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", 1) + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, serialErrPrefix+"device key does not match provided key id") +} + +func (ss *serialSuite) TestSerialRequestHappy(c *C) { + sreq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType, + map[string]interface{}{ + "brand-id": "brand-id1", + "model": "baz-3000", + "device-key": ss.encodedDevKey, + "request-id": "REQID", + }, []byte("HW-DETAILS"), ss.deviceKey) + c.Assert(err, IsNil) + + // roundtrip + a, err := asserts.Decode(asserts.Encode(sreq)) + c.Assert(err, IsNil) + + sreq2, ok := a.(*asserts.SerialRequest) + c.Assert(ok, Equals, true) + + // standalone signature check + err = asserts.SignatureCheck(sreq2, sreq2.DeviceKey()) + c.Check(err, IsNil) + + c.Check(sreq2.BrandID(), Equals, "brand-id1") + c.Check(sreq2.Model(), Equals, "baz-3000") + c.Check(sreq2.RequestID(), Equals, "REQID") + + c.Check(sreq2.Serial(), Equals, "") +} + +func (ss *serialSuite) TestSerialRequestHappyOptionalSerial(c *C) { + sreq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType, + map[string]interface{}{ + "brand-id": "brand-id1", + "model": "baz-3000", + "serial": "pserial", + "device-key": ss.encodedDevKey, + "request-id": "REQID", + }, []byte("HW-DETAILS"), ss.deviceKey) + c.Assert(err, IsNil) + + // roundtrip + a, err := asserts.Decode(asserts.Encode(sreq)) + c.Assert(err, IsNil) + + sreq2, ok := a.(*asserts.SerialRequest) + c.Assert(ok, Equals, true) + + c.Check(sreq2.Model(), Equals, "baz-3000") + c.Check(sreq2.Serial(), Equals, "pserial") +} + +func (ss *serialSuite) TestSerialRequestDecodeInvalid(c *C) { + encoded := "type: serial-request\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "device-key:\n DEVICEKEY\n" + + "request-id: REQID\n" + + "serial: S\n" + + "body-length: 2\n" + + "sign-key-sha3-384: " + ss.deviceKey.PublicKey().ID() + "\n\n" + + "HW" + + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`}, + {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`}, + {"model: baz-3000\n", "", `"model" header is mandatory`}, + {"model: baz-3000\n", "model: \n", `"model" header should not be empty`}, + {"request-id: REQID\n", "", `"request-id" header is mandatory`}, + {"request-id: REQID\n", "request-id: \n", `"request-id" header should not be empty`}, + {"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`}, + {"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`}, + {"device-key:\n DEVICEKEY\n", "device-key: $$$\n", `cannot decode public key: .*`}, + {"serial: S\n", "serial:\n - xyz\n", `"serial" header must be a string`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1) + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, serialReqErrPrefix+test.expectedErr) + } +} + +func (ss *serialSuite) TestSerialRequestDecodeKeyIDMismatch(c *C) { + invalid := "type: serial-request\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "device-key:\n " + strings.Replace(ss.encodedDevKey, "\n", "\n ", -1) + "\n" + + "request-id: REQID\n" + + "body-length: 2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "HW" + + "\n\n" + + "AXNpZw==" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, "assertion serial-request: device key does not match included signing key id") +} + +func (ss *serialSuite) TestDeviceSessionRequest(c *C) { + ts := time.Now().UTC().Round(time.Second) + sessReq, err := asserts.SignWithoutAuthority(asserts.DeviceSessionRequestType, + map[string]interface{}{ + "brand-id": "brand-id1", + "model": "baz-3000", + "serial": "99990", + "nonce": "NONCE", + "timestamp": ts.Format(time.RFC3339), + }, nil, ss.deviceKey) + c.Assert(err, IsNil) + + // roundtrip + a, err := asserts.Decode(asserts.Encode(sessReq)) + c.Assert(err, IsNil) + + sessReq2, ok := a.(*asserts.DeviceSessionRequest) + c.Assert(ok, Equals, true) + + // standalone signature check + err = asserts.SignatureCheck(sessReq2, ss.deviceKey.PublicKey()) + c.Check(err, IsNil) + + c.Check(sessReq2.BrandID(), Equals, "brand-id1") + c.Check(sessReq2.Model(), Equals, "baz-3000") + c.Check(sessReq2.Serial(), Equals, "99990") + c.Check(sessReq2.Nonce(), Equals, "NONCE") + c.Check(sessReq2.Timestamp().Equal(ts), Equals, true) +} + +func (ss *serialSuite) TestDeviceSessionRequestDecodeInvalid(c *C) { + tsLine := "timestamp: " + time.Now().Format(time.RFC3339) + "\n" + encoded := "type: device-session-request\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "serial: 99990\n" + + "nonce: NONCE\n" + + tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: " + ss.deviceKey.PublicKey().ID() + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`}, + {"model: baz-3000\n", "model: \n", `"model" header should not be empty`}, + {"serial: 99990\n", "", `"serial" header is mandatory`}, + {"nonce: NONCE\n", "nonce: \n", `"nonce" header should not be empty`}, + {tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, deviceSessReqErrPrefix+test.expectedErr) + } +} diff --git a/asserts/digest.go b/asserts/digest.go new file mode 100644 index 00000000..6578772e --- /dev/null +++ b/asserts/digest.go @@ -0,0 +1,43 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts + +import ( + "crypto" + "encoding/base64" + "fmt" +) + +// EncodeDigest encodes the digest from hash algorithm to be put in an assertion header. +func EncodeDigest(hash crypto.Hash, hashDigest []byte) (string, error) { + algo := "" + switch hash { + case crypto.SHA512: + algo = "sha512" + case crypto.SHA3_384: + algo = "sha3-384" + default: + return "", fmt.Errorf("unsupported hash") + } + if len(hashDigest) != hash.Size() { + return "", fmt.Errorf("hash digest by %s should be %d bytes", algo, hash.Size()) + } + return base64.RawURLEncoding.EncodeToString(hashDigest), nil +} diff --git a/asserts/digest_test.go b/asserts/digest_test.go new file mode 100644 index 00000000..a9406925 --- /dev/null +++ b/asserts/digest_test.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts_test + +import ( + "crypto" + _ "crypto/sha256" + "encoding/base64" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type encodeDigestSuite struct{} + +var _ = Suite(&encodeDigestSuite{}) + +func (eds *encodeDigestSuite) TestEncodeDigestOK(c *C) { + h := crypto.SHA512.New() + h.Write([]byte("some stuff to hash")) + digest := h.Sum(nil) + encoded, err := asserts.EncodeDigest(crypto.SHA512, digest) + c.Assert(err, IsNil) + + decoded, err := base64.RawURLEncoding.DecodeString(encoded) + c.Assert(err, IsNil) + c.Check(decoded, DeepEquals, digest) + + // sha3-384 + b, err := base64.RawURLEncoding.DecodeString(blobSHA3_384) + c.Assert(err, IsNil) + encoded, err = asserts.EncodeDigest(crypto.SHA3_384, b) + c.Assert(err, IsNil) + c.Check(encoded, Equals, blobSHA3_384) + +} + +func (eds *encodeDigestSuite) TestEncodeDigestErrors(c *C) { + _, err := asserts.EncodeDigest(crypto.SHA1, nil) + c.Check(err, ErrorMatches, "unsupported hash") + + _, err = asserts.EncodeDigest(crypto.SHA512, []byte{1, 2}) + c.Check(err, ErrorMatches, "hash digest by sha512 should be 64 bytes") + + _, err = asserts.EncodeDigest(crypto.SHA3_384, []byte{1, 2}) + c.Check(err, ErrorMatches, "hash digest by sha3-384 should be 48 bytes") +} diff --git a/asserts/export_test.go b/asserts/export_test.go new file mode 100644 index 00000000..ef1a361c --- /dev/null +++ b/asserts/export_test.go @@ -0,0 +1,184 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts + +import ( + "io" + "time" +) + +// expose test-only things here + +var NumAssertionType = len(typeRegistry) + +// v1FixedTimestamp exposed for tests +var V1FixedTimestamp = v1FixedTimestamp + +// assembleAndSign exposed for tests +var AssembleAndSignInTest = assembleAndSign + +// decodePrivateKey exposed for tests +var DecodePrivateKeyInTest = decodePrivateKey + +// NewDecoderStressed makes a Decoder with a stressed setup with the given buffer and maximum sizes. +func NewDecoderStressed(r io.Reader, bufSize, maxHeadersSize, maxBodySize, maxSigSize int) *Decoder { + return (&Decoder{ + rd: r, + initialBufSize: bufSize, + maxHeadersSize: maxHeadersSize, + maxBodySize: maxBodySize, + maxSigSize: maxSigSize, + }).initBuffer() +} + +// Encoder.append exposed for tests +func EncoderAppend(enc *Encoder, encoded []byte) error { + return enc.append(encoded) +} + +func BootstrapAccountForTest(authorityID string) *Account { + return &Account{ + assertionBase: assertionBase{ + headers: map[string]interface{}{ + "type": "account", + "authority-id": authorityID, + "account-id": authorityID, + "validation": "certified", + }, + }, + timestamp: time.Now().UTC(), + } +} + +func makeAccountKeyForTest(authorityID string, openPGPPubKey PublicKey, validYears int) *AccountKey { + return &AccountKey{ + assertionBase: assertionBase{ + headers: map[string]interface{}{ + "type": "account-key", + "authority-id": authorityID, + "account-id": authorityID, + "public-key-sha3-384": openPGPPubKey.ID(), + }, + }, + since: time.Time{}, + until: time.Time{}.UTC().AddDate(validYears, 0, 0), + pubKey: openPGPPubKey, + } +} + +func BootstrapAccountKeyForTest(authorityID string, pubKey PublicKey) *AccountKey { + return makeAccountKeyForTest(authorityID, pubKey, 9999) +} + +func ExpiredAccountKeyForTest(authorityID string, pubKey PublicKey) *AccountKey { + return makeAccountKeyForTest(authorityID, pubKey, 1) +} + +// define dummy assertion types to use in the tests + +type TestOnly struct { + assertionBase +} + +func assembleTestOnly(assert assertionBase) (Assertion, error) { + // for testing error cases + if _, err := checkIntWithDefault(assert.headers, "count", 0); err != nil { + return nil, err + } + return &TestOnly{assert}, nil +} + +var TestOnlyType = &AssertionType{"test-only", []string{"primary-key"}, assembleTestOnly, 0} + +type TestOnly2 struct { + assertionBase +} + +func assembleTestOnly2(assert assertionBase) (Assertion, error) { + return &TestOnly2{assert}, nil +} + +var TestOnly2Type = &AssertionType{"test-only-2", []string{"pk1", "pk2"}, assembleTestOnly2, 0} + +type TestOnlyNoAuthority struct { + assertionBase +} + +func assembleTestOnlyNoAuthority(assert assertionBase) (Assertion, error) { + if _, err := checkNotEmptyString(assert.headers, "hdr"); err != nil { + return nil, err + } + return &TestOnlyNoAuthority{assert}, nil +} + +var TestOnlyNoAuthorityType = &AssertionType{"test-only-no-authority", nil, assembleTestOnlyNoAuthority, noAuthority} + +type TestOnlyNoAuthorityPK struct { + assertionBase +} + +func assembleTestOnlyNoAuthorityPK(assert assertionBase) (Assertion, error) { + return &TestOnlyNoAuthorityPK{assert}, nil +} + +var TestOnlyNoAuthorityPKType = &AssertionType{"test-only-no-authority-pk", []string{"pk"}, assembleTestOnlyNoAuthorityPK, noAuthority} + +func init() { + typeRegistry[TestOnlyType.Name] = TestOnlyType + maxSupportedFormat[TestOnlyType.Name] = 1 + typeRegistry[TestOnly2Type.Name] = TestOnly2Type + typeRegistry[TestOnlyNoAuthorityType.Name] = TestOnlyNoAuthorityType + typeRegistry[TestOnlyNoAuthorityPKType.Name] = TestOnlyNoAuthorityPKType +} + +// AccountKeyIsKeyValidAt exposes isKeyValidAt on AccountKey for tests +func AccountKeyIsKeyValidAt(ak *AccountKey, when time.Time) bool { + return ak.isKeyValidAt(when) +} + +type GPGRunner func(input []byte, args ...string) ([]byte, error) + +func MockRunGPG(mock func(prev GPGRunner, input []byte, args ...string) ([]byte, error)) (restore func()) { + prevRunGPG := runGPG + runGPG = func(input []byte, args ...string) ([]byte, error) { + return mock(prevRunGPG, input, args...) + } + return func() { + runGPG = prevRunGPG + } +} + +// Headers helpers to test +var ( + ParseHeaders = parseHeaders + AppendEntry = appendEntry +) + +// ParametersForGenerate exposes parametersForGenerate for tests. +func (gkm *GPGKeypairManager) ParametersForGenerate(passphrase string, name string) string { + return gkm.parametersForGenerate(passphrase, name) +} + +// ifacedecls tests +var ( + CompileAttributeConstraints = compileAttributeConstraints + CompilePlugRule = compilePlugRule + CompileSlotRule = compileSlotRule +) diff --git a/asserts/fetcher.go b/asserts/fetcher.go new file mode 100644 index 00000000..ea084a7a --- /dev/null +++ b/asserts/fetcher.go @@ -0,0 +1,121 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts + +import ( + "fmt" +) + +type fetchProgress int + +const ( + fetchNotSeen fetchProgress = iota + fetchRetrieved + fetchSaved +) + +// A Fetcher helps fetching assertions and their prerequisites. +type Fetcher interface { + // Fetch retrieves the assertion indicated by ref then its prerequisites + // recursively, along the way saving prerequisites before dependent assertions. + Fetch(*Ref) error + // Save retrieves the prerequisites of the assertion recursively, + // along the way saving them, and finally saves the assertion. + Save(Assertion) error +} + +type fetcher struct { + db RODatabase + retrieve func(*Ref) (Assertion, error) + save func(Assertion) error + + fetched map[string]fetchProgress +} + +// NewFetcher creates a Fetcher which will use trustedDB to determine trusted assertions, will fetch assertions following prerequisites using retrieve, and then will pass them to save, saving prerequisites before dependent assertions. +func NewFetcher(trustedDB RODatabase, retrieve func(*Ref) (Assertion, error), save func(Assertion) error) Fetcher { + return &fetcher{ + db: trustedDB, + retrieve: retrieve, + save: save, + fetched: make(map[string]fetchProgress), + } +} + +func (f *fetcher) chase(ref *Ref, a Assertion) error { + // check if ref points to a trusted assertion, in which case + // there is nothing to do + _, err := ref.Resolve(f.db.FindTrusted) + if err == nil { + return nil + } + if err != ErrNotFound { + return err + } + u := ref.Unique() + switch f.fetched[u] { + case fetchSaved: + return nil // nothing to do + case fetchRetrieved: + return fmt.Errorf("internal error: circular assertions are not expected: %s", ref) + } + if a == nil { + retrieved, err := f.retrieve(ref) + if err != nil { + return err + } + a = retrieved + } + f.fetched[u] = fetchRetrieved + for _, preref := range a.Prerequisites() { + if err := f.Fetch(preref); err != nil { + return err + } + } + if err := f.fetchAccountKey(a.SignKeyID()); err != nil { + return err + } + if err := f.save(a); err != nil { + return err + } + f.fetched[u] = fetchSaved + return nil +} + +// Fetch retrieves the assertion indicated by ref then its prerequisites +// recursively, along the way saving prerequisites before dependent assertions. +func (f *fetcher) Fetch(ref *Ref) error { + return f.chase(ref, nil) +} + +// fetchAccountKey behaves like Fetch for the account-key with the given key id. +func (f *fetcher) fetchAccountKey(keyID string) error { + keyRef := &Ref{ + Type: AccountKeyType, + PrimaryKey: []string{keyID}, + } + return f.Fetch(keyRef) +} + +// Save retrieves the prerequisites of the assertion recursively, +// along the way saving them, and finally saves the assertion. +func (f *fetcher) Save(a Assertion) error { + return f.chase(a.Ref(), a) +} diff --git a/asserts/fetcher_test.go b/asserts/fetcher_test.go new file mode 100644 index 00000000..56b8c1c5 --- /dev/null +++ b/asserts/fetcher_test.go @@ -0,0 +1,174 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts_test + +import ( + "crypto" + "encoding/hex" + "fmt" + "time" + + "golang.org/x/crypto/sha3" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +type fetcherSuite struct { + storeSigning *assertstest.StoreStack +} + +var _ = Suite(&fetcherSuite{}) + +func (s *fetcherSuite) SetUpTest(c *C) { + rootPrivKey, _ := assertstest.GenerateKey(1024) + storePrivKey, _ := assertstest.GenerateKey(752) + s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey) +} + +func fakeSnap(rev int) []byte { + fake := fmt.Sprintf("hsqs________________%d", rev) + return []byte(fake) +} + +func fakeHash(rev int) []byte { + h := sha3.Sum384(fakeSnap(rev)) + return h[:] +} + +func makeDigest(rev int) string { + d, err := asserts.EncodeDigest(crypto.SHA3_384, fakeHash(rev)) + if err != nil { + panic(err) + } + return string(d) +} + +func makeHexDigest(rev int) string { + return hex.EncodeToString(fakeHash(rev)) +} + +func (s *fetcherSuite) prereqSnapAssertions(c *C, revisions ...int) { + dev1Acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + err := s.storeSigning.Add(dev1Acct) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapDecl) + c.Assert(err, IsNil) + + for _, rev := range revisions { + headers = map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-sha3-384": makeDigest(rev), + "snap-size": "1000", + "snap-revision": fmt.Sprintf("%d", rev), + "developer-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapRev) + c.Assert(err, IsNil) + } +} + +func (s *fetcherSuite) TestFetch(c *C) { + s.prereqSnapAssertions(c, 10) + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: s.storeSigning.Trusted, + }) + c.Assert(err, IsNil) + + ref := &asserts.Ref{ + Type: asserts.SnapRevisionType, + PrimaryKey: []string{makeDigest(10)}, + } + + retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) { + return ref.Resolve(s.storeSigning.Find) + } + + f := asserts.NewFetcher(db, retrieve, db.Add) + + err = f.Fetch(ref) + c.Assert(err, IsNil) + + snapRev, err := ref.Resolve(db.Find) + c.Assert(err, IsNil) + c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 10) + + snapDecl, err := db.Find(asserts.SnapDeclarationType, map[string]string{ + "series": "16", + "snap-id": "snap-id-1", + }) + c.Assert(err, IsNil) + c.Check(snapDecl.(*asserts.SnapDeclaration).SnapName(), Equals, "foo") +} + +func (s *fetcherSuite) TestSave(c *C) { + s.prereqSnapAssertions(c, 10) + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: s.storeSigning.Trusted, + }) + c.Assert(err, IsNil) + + retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) { + return ref.Resolve(s.storeSigning.Find) + } + + f := asserts.NewFetcher(db, retrieve, db.Add) + + ref := &asserts.Ref{ + Type: asserts.SnapRevisionType, + PrimaryKey: []string{makeDigest(10)}, + } + rev, err := ref.Resolve(s.storeSigning.Find) + c.Assert(err, IsNil) + + err = f.Save(rev) + c.Assert(err, IsNil) + + snapRev, err := ref.Resolve(db.Find) + c.Assert(err, IsNil) + c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 10) + + snapDecl, err := db.Find(asserts.SnapDeclarationType, map[string]string{ + "series": "16", + "snap-id": "snap-id-1", + }) + c.Assert(err, IsNil) + c.Check(snapDecl.(*asserts.SnapDeclaration).SnapName(), Equals, "foo") +} diff --git a/asserts/findwildcard.go b/asserts/findwildcard.go new file mode 100644 index 00000000..abac11bf --- /dev/null +++ b/asserts/findwildcard.go @@ -0,0 +1,111 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +/* +findWildcard invokes foundCb once for each parent directory of regular files matching: + +//... + +where each descendantWithWildcard component can contain the * wildcard; + +foundCb is invoked with the paths of the found regular files relative to top (that means top/ is excluded). + +Unlike filepath.Glob any I/O operation error stops the walking and bottoms out, so does a foundCb invocation that returns an error. +*/ +func findWildcard(top string, descendantWithWildcard []string, foundCb func(relpath []string) error) error { + return findWildcardDescend(top, top, descendantWithWildcard, foundCb) +} + +func findWildcardBottom(top, current string, pat string, names []string, foundCb func(relpath []string) error) error { + var hits []string + for _, name := range names { + ok, err := filepath.Match(pat, name) + if err != nil { + return fmt.Errorf("findWildcard: invoked with malformed wildcard: %v", err) + } + if !ok { + continue + } + fn := filepath.Join(current, name) + finfo, err := os.Stat(fn) + if os.IsNotExist(err) { + continue + } + if err != nil { + return err + } + if !finfo.Mode().IsRegular() { + return fmt.Errorf("expected a regular file: %v", fn) + } + relpath, err := filepath.Rel(top, fn) + if err != nil { + return fmt.Errorf("findWildcard: unexpected to fail at computing rel path of descendant") + } + hits = append(hits, relpath) + } + if len(hits) == 0 { + return nil + } + return foundCb(hits) +} + +func findWildcardDescend(top, current string, descendantWithWildcard []string, foundCb func(relpath []string) error) error { + k := descendantWithWildcard[0] + if len(descendantWithWildcard) > 1 && strings.IndexByte(k, '*') == -1 { + return findWildcardDescend(top, filepath.Join(current, k), descendantWithWildcard[1:], foundCb) + } + + d, err := os.Open(current) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + defer d.Close() + names, err := d.Readdirnames(-1) + if err != nil { + return err + } + if len(descendantWithWildcard) == 1 { + return findWildcardBottom(top, current, k, names, foundCb) + } + for _, name := range names { + ok, err := filepath.Match(k, name) + if err != nil { + return fmt.Errorf("findWildcard: invoked with malformed wildcard: %v", err) + } + if ok { + err = findWildcardDescend(top, filepath.Join(current, name), descendantWithWildcard[1:], foundCb) + if err != nil { + return err + } + } + } + return nil +} diff --git a/asserts/findwildcard_test.go b/asserts/findwildcard_test.go new file mode 100644 index 00000000..f5094ca7 --- /dev/null +++ b/asserts/findwildcard_test.go @@ -0,0 +1,139 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts + +import ( + "errors" + "io/ioutil" + "os" + "path/filepath" + "sort" + + "gopkg.in/check.v1" +) + +type findWildcardSuite struct{} + +var _ = check.Suite(&findWildcardSuite{}) + +func (fs *findWildcardSuite) TestFindWildcard(c *check.C) { + top := filepath.Join(c.MkDir(), "top") + + err := os.MkdirAll(top, os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id1"), os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id1", "abcd"), os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id1", "e5cd"), os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id2"), os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id2", "f444"), os.ModePerm) + c.Assert(err, check.IsNil) + + err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "abcd", "active"), nil, os.ModePerm) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "abcd", "active.1"), nil, os.ModePerm) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "e5cd", "active"), nil, os.ModePerm) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(top, "acc-id2", "f444", "active"), nil, os.ModePerm) + c.Assert(err, check.IsNil) + + var res []string + foundCb := func(relpath []string) error { + res = append(res, relpath...) + return nil + } + + err = findWildcard(top, []string{"*", "*", "active"}, foundCb) + c.Assert(err, check.IsNil) + sort.Strings(res) + c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/e5cd/active", "acc-id2/f444/active"}) + + res = nil + err = findWildcard(top, []string{"*", "*", "active*"}, foundCb) + c.Assert(err, check.IsNil) + sort.Strings(res) + c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/abcd/active.1", "acc-id1/e5cd/active", "acc-id2/f444/active"}) + + res = nil + err = findWildcard(top, []string{"zoo", "*", "active"}, foundCb) + c.Assert(err, check.IsNil) + c.Check(res, check.HasLen, 0) + + res = nil + err = findWildcard(top, []string{"zoo", "*", "active*"}, foundCb) + c.Assert(err, check.IsNil) + c.Check(res, check.HasLen, 0) + + res = nil + err = findWildcard(top, []string{"a*", "zoo", "active"}, foundCb) + c.Assert(err, check.IsNil) + c.Check(res, check.HasLen, 0) + + res = nil + err = findWildcard(top, []string{"acc-id1", "*cd", "active"}, foundCb) + c.Assert(err, check.IsNil) + sort.Strings(res) + c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/e5cd/active"}) + + res = nil + err = findWildcard(top, []string{"acc-id1", "*cd", "active*"}, foundCb) + c.Assert(err, check.IsNil) + sort.Strings(res) + c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/abcd/active.1", "acc-id1/e5cd/active"}) + +} + +func (fs *findWildcardSuite) TestFindWildcardSomeErrors(c *check.C) { + top := filepath.Join(c.MkDir(), "top-errors") + + err := os.MkdirAll(top, os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id1"), os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id2"), os.ModePerm) + c.Assert(err, check.IsNil) + + err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "abcd"), nil, os.ModePerm) + c.Assert(err, check.IsNil) + + err = os.MkdirAll(filepath.Join(top, "acc-id2", "dddd"), os.ModePerm) + c.Assert(err, check.IsNil) + + var res []string + var retErr error + foundCb := func(relpath []string) error { + res = append(res, relpath...) + return retErr + } + + myErr := errors.New("boom") + retErr = myErr + err = findWildcard(top, []string{"acc-id1", "*"}, foundCb) + c.Check(err, check.Equals, myErr) + + retErr = nil + res = nil + err = findWildcard(top, []string{"acc-id2", "*"}, foundCb) + c.Check(err, check.ErrorMatches, "expected a regular file: .*") +} diff --git a/asserts/fsbackstore.go b/asserts/fsbackstore.go new file mode 100644 index 00000000..834248a6 --- /dev/null +++ b/asserts/fsbackstore.go @@ -0,0 +1,220 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "sync" +) + +// the default filesystem based backstore for assertions + +const ( + assertionsLayoutVersion = "v0" + assertionsRoot = "asserts-" + assertionsLayoutVersion +) + +type filesystemBackstore struct { + top string + mu sync.RWMutex +} + +// OpenFSBackstore opens a filesystem backed assertions backstore under path. +func OpenFSBackstore(path string) (Backstore, error) { + top := filepath.Join(path, assertionsRoot) + err := ensureTop(top) + if err != nil { + return nil, err + } + return &filesystemBackstore{top: top}, nil +} + +// guarantees that result assertion is of the expected type (both in the AssertionType and go type sense) +func (fsbs *filesystemBackstore) readAssertion(assertType *AssertionType, diskPrimaryPath string) (Assertion, error) { + encoded, err := readEntry(fsbs.top, assertType.Name, diskPrimaryPath) + if os.IsNotExist(err) { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("broken assertion storage, cannot read assertion: %v", err) + } + assert, err := Decode(encoded) + if err != nil { + return nil, fmt.Errorf("broken assertion storage, cannot decode assertion: %v", err) + } + if assert.Type() != assertType { + return nil, fmt.Errorf("assertion that is not of type %q under their storage tree", assertType.Name) + } + // because of Decode() construction assert has also the expected go type + return assert, nil +} + +func (fsbs *filesystemBackstore) pickLatestAssertion(assertType *AssertionType, diskPrimaryPaths []string, maxFormat int) (a Assertion, er error) { + for _, diskPrimaryPath := range diskPrimaryPaths { + fn := filepath.Base(diskPrimaryPath) + parts := strings.SplitN(fn, ".", 2) + formatnum := 0 + if len(parts) == 2 { + var err error + formatnum, err = strconv.Atoi(parts[1]) + if err != nil { + return nil, fmt.Errorf("invalid active assertion filename: %q", fn) + } + } + if formatnum <= maxFormat { + a1, err := fsbs.readAssertion(assertType, diskPrimaryPath) + if err != nil { + return nil, err + } + if a == nil || a1.Revision() > a.Revision() { + a = a1 + } + } + } + if a == nil { + return nil, ErrNotFound + } + return a, nil +} + +func diskPrimaryPathComps(primaryPath []string, active string) []string { + n := len(primaryPath) + comps := make([]string, n+1) + // safety against '/' etc + for i, comp := range primaryPath { + comps[i] = url.QueryEscape(comp) + } + comps[n] = active + return comps +} + +func (fsbs *filesystemBackstore) currentAssertion(assertType *AssertionType, primaryPath []string, maxFormat int) (Assertion, error) { + var a Assertion + namesCb := func(relpaths []string) error { + var err error + a, err = fsbs.pickLatestAssertion(assertType, relpaths, maxFormat) + if err == ErrNotFound { + return nil + } + return err + } + + comps := diskPrimaryPathComps(primaryPath, "active*") + assertTypeTop := filepath.Join(fsbs.top, assertType.Name) + err := findWildcard(assertTypeTop, comps, namesCb) + if err != nil { + return nil, fmt.Errorf("broken assertion storage, looking for %s: %v", assertType.Name, err) + } + + if a == nil { + return nil, ErrNotFound + } + + return a, nil +} + +func (fsbs *filesystemBackstore) Put(assertType *AssertionType, assert Assertion) error { + fsbs.mu.Lock() + defer fsbs.mu.Unlock() + + primaryPath := make([]string, len(assertType.PrimaryKey)) + for i, k := range assertType.PrimaryKey { + primaryPath[i] = assert.HeaderString(k) + } + + curAssert, err := fsbs.currentAssertion(assertType, primaryPath, assertType.MaxSupportedFormat()) + if err == nil { + curRev := curAssert.Revision() + rev := assert.Revision() + if curRev >= rev { + return &RevisionError{Current: curRev, Used: rev} + } + } else if err != ErrNotFound { + return err + } + + formatnum := assert.Format() + activeFn := "active" + if formatnum > 0 { + activeFn = fmt.Sprintf("active.%d", formatnum) + } + diskPrimaryPath := filepath.Join(diskPrimaryPathComps(primaryPath, activeFn)...) + err = atomicWriteEntry(Encode(assert), false, fsbs.top, assertType.Name, diskPrimaryPath) + if err != nil { + return fmt.Errorf("broken assertion storage, cannot write assertion: %v", err) + } + return nil +} + +func (fsbs *filesystemBackstore) Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error) { + fsbs.mu.RLock() + defer fsbs.mu.RUnlock() + + return fsbs.currentAssertion(assertType, key, maxFormat) +} + +func (fsbs *filesystemBackstore) search(assertType *AssertionType, diskPattern []string, foundCb func(Assertion), maxFormat int) error { + assertTypeTop := filepath.Join(fsbs.top, assertType.Name) + candCb := func(diskPrimaryPaths []string) error { + a, err := fsbs.pickLatestAssertion(assertType, diskPrimaryPaths, maxFormat) + if err == ErrNotFound { + return nil + } + if err != nil { + return err + } + foundCb(a) + return nil + } + err := findWildcard(assertTypeTop, diskPattern, candCb) + if err != nil { + return fmt.Errorf("broken assertion storage, searching for %s: %v", assertType.Name, err) + } + return nil +} + +func (fsbs *filesystemBackstore) Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error { + fsbs.mu.RLock() + defer fsbs.mu.RUnlock() + + n := len(assertType.PrimaryKey) + diskPattern := make([]string, n+1) + for i, k := range assertType.PrimaryKey { + keyVal := headers[k] + if keyVal == "" { + diskPattern[i] = "*" + } else { + diskPattern[i] = url.QueryEscape(keyVal) + } + } + diskPattern[n] = "active*" + + candCb := func(a Assertion) { + if searchMatch(a, headers) { + foundCb(a) + } + } + return fsbs.search(assertType, diskPattern, candCb, maxFormat) +} diff --git a/asserts/fsbackstore_test.go b/asserts/fsbackstore_test.go new file mode 100644 index 00000000..ce1ed7e8 --- /dev/null +++ b/asserts/fsbackstore_test.go @@ -0,0 +1,251 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts_test + +import ( + "os" + "path/filepath" + "syscall" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type fsBackstoreSuite struct{} + +var _ = Suite(&fsBackstoreSuite{}) + +func (fsbss *fsBackstoreSuite) TestOpenOK(c *C) { + // ensure umask is clean when creating the DB dir + oldUmask := syscall.Umask(0) + defer syscall.Umask(oldUmask) + + topDir := filepath.Join(c.MkDir(), "asserts-db") + + bs, err := asserts.OpenFSBackstore(topDir) + c.Check(err, IsNil) + c.Check(bs, NotNil) + + info, err := os.Stat(filepath.Join(topDir, "asserts-v0")) + c.Assert(err, IsNil) + c.Assert(info.IsDir(), Equals, true) + c.Check(info.Mode().Perm(), Equals, os.FileMode(0775)) +} + +func (fsbss *fsBackstoreSuite) TestOpenCreateFail(c *C) { + parent := filepath.Join(c.MkDir(), "var") + topDir := filepath.Join(parent, "asserts-db") + // make it not writable + err := os.Mkdir(parent, 0555) + c.Assert(err, IsNil) + + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, ErrorMatches, "cannot create assert storage root: .*") + c.Check(bs, IsNil) +} + +func (fsbss *fsBackstoreSuite) TestOpenWorldWritableFail(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + // make it world-writable + oldUmask := syscall.Umask(0) + os.MkdirAll(filepath.Join(topDir, "asserts-v0"), 0777) + syscall.Umask(oldUmask) + + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*") + c.Check(bs, IsNil) +} + +func (fsbss *fsBackstoreSuite) TestPutOldRevision(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + // Create two revisions of assertion. + a0, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + a1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + // Put newer revision, follwed by old revision. + err = bs.Put(asserts.TestOnlyType, a1) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a0) + + c.Check(err, ErrorMatches, `revision 0 is older than current revision 1`) + c.Check(err, DeepEquals, &asserts.RevisionError{Current: 1, Used: 0}) +} + +func (fsbss *fsBackstoreSuite) TestGetFormat(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + af0, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "format: 1\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af2, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: zoo\n" + + "format: 2\n" + + "revision: 22\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + err = bs.Put(asserts.TestOnlyType, af0) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, af1) + c.Assert(err, IsNil) + + a, err := bs.Get(asserts.TestOnlyType, []string{"foo"}, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + + a, err = bs.Get(asserts.TestOnlyType, []string{"foo"}, 0) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 0) + c.Assert(err, Equals, asserts.ErrNotFound) + + err = bs.Put(asserts.TestOnlyType, af2) + c.Assert(err, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 1) + c.Assert(err, Equals, asserts.ErrNotFound) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 2) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 22) +} + +func (fsbss *fsBackstoreSuite) TestSearchFormat(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + af0, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: bar\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af1, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: bar\n" + + "format: 1\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + af2, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: baz\n" + + "format: 2\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + err = bs.Put(asserts.TestOnly2Type, af0) + c.Assert(err, IsNil) + + queries := []map[string]string{ + {"pk1": "foo", "pk2": "bar"}, + {"pk1": "foo"}, + {"pk2": "bar"}, + } + + for _, q := range queries { + var a asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + a = a1 + } + err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + } + + err = bs.Put(asserts.TestOnly2Type, af1) + c.Assert(err, IsNil) + + for _, q := range queries { + var a asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + a = a1 + } + err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + + err = bs.Search(asserts.TestOnly2Type, q, foundCb, 0) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + } + + err = bs.Put(asserts.TestOnly2Type, af2) + c.Assert(err, IsNil) + + var as []asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + as = append(as, a1) + } + err = bs.Search(asserts.TestOnly2Type, map[string]string{ + "pk1": "foo", + }, foundCb, 1) // will not find af2 + c.Assert(err, IsNil) + c.Check(as, HasLen, 1) + c.Check(as[0].Revision(), Equals, 1) + +} diff --git a/asserts/fsentryutils.go b/asserts/fsentryutils.go new file mode 100644 index 00000000..ca057d8c --- /dev/null +++ b/asserts/fsentryutils.go @@ -0,0 +1,70 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/snapcore/snapd/osutil" +) + +// utilities to read/write fs entries + +func ensureTop(path string) error { + err := os.MkdirAll(path, 0775) + if err != nil { + return fmt.Errorf("cannot create assert storage root: %v", err) + } + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("cannot create assert storage root: %v", err) + } + if info.Mode().Perm()&0002 != 0 { + return fmt.Errorf("assert storage root unexpectedly world-writable: %v", path) + } + return nil +} + +func atomicWriteEntry(data []byte, secret bool, top string, subpath ...string) error { + fpath := filepath.Join(top, filepath.Join(subpath...)) + dir := filepath.Dir(fpath) + err := os.MkdirAll(dir, 0775) + if err != nil { + return err + } + fperm := 0664 + if secret { + fperm = 0600 + } + return osutil.AtomicWriteFile(fpath, data, os.FileMode(fperm), 0) +} + +func entryExists(top string, subpath ...string) bool { + fpath := filepath.Join(top, filepath.Join(subpath...)) + return osutil.FileExists(fpath) +} + +func readEntry(top string, subpath ...string) ([]byte, error) { + fpath := filepath.Join(top, filepath.Join(subpath...)) + return ioutil.ReadFile(fpath) +} diff --git a/asserts/fskeypairmgr.go b/asserts/fskeypairmgr.go new file mode 100644 index 00000000..5a58ae17 --- /dev/null +++ b/asserts/fskeypairmgr.go @@ -0,0 +1,92 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "sync" +) + +// the default simple filesystem based keypair manager/backstore + +const ( + privateKeysLayoutVersion = "v1" + privateKeysRoot = "private-keys-" + privateKeysLayoutVersion +) + +type filesystemKeypairManager struct { + top string + mu sync.RWMutex +} + +// OpenFSKeypairManager opens a filesystem backed assertions backstore under path. +func OpenFSKeypairManager(path string) (KeypairManager, error) { + top := filepath.Join(path, privateKeysRoot) + err := ensureTop(top) + if err != nil { + return nil, err + } + return &filesystemKeypairManager{top: top}, nil +} + +var errKeypairAlreadyExists = errors.New("key pair with given key id already exists") + +func (fskm *filesystemKeypairManager) Put(privKey PrivateKey) error { + keyID := privKey.PublicKey().ID() + if entryExists(fskm.top, keyID) { + return errKeypairAlreadyExists + } + encoded, err := encodePrivateKey(privKey) + if err != nil { + return fmt.Errorf("cannot store private key: %v", err) + } + + fskm.mu.Lock() + defer fskm.mu.Unlock() + + err = atomicWriteEntry(encoded, true, fskm.top, keyID) + if err != nil { + return fmt.Errorf("cannot store private key: %v", err) + } + return nil +} + +var errKeypairNotFound = errors.New("cannot find key pair") + +func (fskm *filesystemKeypairManager) Get(keyID string) (PrivateKey, error) { + fskm.mu.RLock() + defer fskm.mu.RUnlock() + + encoded, err := readEntry(fskm.top, keyID) + if os.IsNotExist(err) { + return nil, errKeypairNotFound + } + if err != nil { + return nil, fmt.Errorf("cannot read key pair: %v", err) + } + privKey, err := decodePrivateKey(encoded) + if err != nil { + return nil, fmt.Errorf("cannot decode key pair: %v", err) + } + return privKey, nil +} diff --git a/asserts/fskeypairmgr_test.go b/asserts/fskeypairmgr_test.go new file mode 100644 index 00000000..422ccdde --- /dev/null +++ b/asserts/fskeypairmgr_test.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts_test + +import ( + "os" + "path/filepath" + "syscall" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type fsKeypairMgrSuite struct{} + +var _ = Suite(&fsKeypairMgrSuite{}) + +func (fsbss *fsKeypairMgrSuite) TestOpenOK(c *C) { + // ensure umask is clean when creating the DB dir + oldUmask := syscall.Umask(0) + defer syscall.Umask(oldUmask) + + topDir := filepath.Join(c.MkDir(), "asserts-db") + err := os.MkdirAll(topDir, 0775) + c.Assert(err, IsNil) + + bs, err := asserts.OpenFSKeypairManager(topDir) + c.Check(err, IsNil) + c.Check(bs, NotNil) + + info, err := os.Stat(filepath.Join(topDir, "private-keys-v1")) + c.Assert(err, IsNil) + c.Assert(info.IsDir(), Equals, true) + c.Check(info.Mode().Perm(), Equals, os.FileMode(0775)) +} + +func (fsbss *fsKeypairMgrSuite) TestOpenWorldWritableFail(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + // make it world-writable + oldUmask := syscall.Umask(0) + os.MkdirAll(filepath.Join(topDir, "private-keys-v1"), 0777) + syscall.Umask(oldUmask) + + bs, err := asserts.OpenFSKeypairManager(topDir) + c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*") + c.Check(bs, IsNil) +} diff --git a/asserts/gpgkeypairmgr.go b/asserts/gpgkeypairmgr.go new file mode 100644 index 00000000..90249751 --- /dev/null +++ b/asserts/gpgkeypairmgr.go @@ -0,0 +1,359 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/snapcore/snapd/osutil" +) + +func ensureGPGHomeDirectory() (string, error) { + real, err := osutil.RealUser() + if err != nil { + return "", err + } + + uid, err := strconv.Atoi(real.Uid) + if err != nil { + return "", err + } + + gid, err := strconv.Atoi(real.Gid) + if err != nil { + return "", err + } + + homedir := os.Getenv("SNAP_GNUPG_HOME") + if homedir == "" { + homedir = filepath.Join(real.HomeDir, ".snap", "gnupg") + } + + if err := osutil.MkdirAllChown(homedir, 0700, uid, gid); err != nil { + return "", err + } + return homedir, nil +} + +// findGPGCommand returns the path to a suitable GnuPG binary to use. +// GnuPG 2 is mainly intended for desktop use, and is hard for us to use +// here: in particular, it's extremely difficult to use it to delete a +// secret key without a pinentry prompt (which would be necessary in our +// test suite). GnuPG 1 is still supported so it's reasonable to continue +// using that for now. +func findGPGCommand() (string, error) { + if path := os.Getenv("SNAP_GNUPG_CMD"); path != "" { + return path, nil + } + + path, err := exec.LookPath("gpg1") + if err != nil { + path, err = exec.LookPath("gpg") + } + return path, err +} + +func runGPGImpl(input []byte, args ...string) ([]byte, error) { + homedir, err := ensureGPGHomeDirectory() + if err != nil { + return nil, err + } + + // Ensure the gpg-agent knows what tty to talk to to ask for + // the passphrase. This is needed because we drive gpg over + // a pipe and if the agent is not already started it will + // fail to be able to ask for a password. + if os.Getenv("GPG_TTY") == "" { + tty, err := os.Readlink("/proc/self/fd/0") + if err != nil { + return nil, err + } + os.Setenv("GPG_TTY", tty) + } + + general := []string{"--homedir", homedir, "-q", "--no-auto-check-trustdb"} + allArgs := append(general, args...) + + path, err := findGPGCommand() + if err != nil { + return nil, err + } + cmd := exec.Command(path, allArgs...) + var outBuf bytes.Buffer + var errBuf bytes.Buffer + + if len(input) != 0 { + cmd.Stdin = bytes.NewBuffer(input) + } + + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("%s %s failed: %v (%q)", path, strings.Join(args, " "), err, errBuf.Bytes()) + } + + return outBuf.Bytes(), nil +} + +var runGPG = runGPGImpl + +// A key pair manager backed by a local GnuPG setup. +type GPGKeypairManager struct{} + +func (gkm *GPGKeypairManager) gpg(input []byte, args ...string) ([]byte, error) { + return runGPG(input, args...) +} + +// NewGPGKeypairManager creates a new key pair manager backed by a local GnuPG setup. +// Importing keys through the keypair manager interface is not +// suppored. +// Main purpose is allowing signing using keys from a GPG setup. +func NewGPGKeypairManager() *GPGKeypairManager { + return &GPGKeypairManager{} +} + +func (gkm *GPGKeypairManager) retrieve(fpr string) (PrivateKey, error) { + out, err := gkm.gpg(nil, "--batch", "--export", "--export-options", "export-minimal,export-clean,no-export-attributes", "0x"+fpr) + if err != nil { + return nil, err + } + if len(out) == 0 { + return nil, fmt.Errorf("cannot retrieve key with fingerprint %q in GPG keyring", fpr) + } + + pubKeyBuf := bytes.NewBuffer(out) + privKey, err := newExtPGPPrivateKey(pubKeyBuf, "GPG", func(content []byte) ([]byte, error) { + return gkm.sign(fpr, content) + }) + if err != nil { + return nil, fmt.Errorf("cannot load GPG public key with fingerprint %q: %v", fpr, err) + } + gotFingerprint := privKey.fingerprint() + if gotFingerprint != fpr { + return nil, fmt.Errorf("got wrong public key from GPG, expected fingerprint %q: %s", fpr, gotFingerprint) + } + return privKey, nil +} + +// Walk iterates over all the RSA private keys in the local GPG setup calling the provided callback until this returns an error +func (gkm *GPGKeypairManager) Walk(consider func(privk PrivateKey, fingerprint string, uid string) error) error { + // see GPG source doc/DETAILS + out, err := gkm.gpg(nil, "--batch", "--list-secret-keys", "--fingerprint", "--with-colons", "--fixed-list-mode") + if err != nil { + return err + } + lines := strings.Split(string(out), "\n") + n := len(lines) + if n > 0 && lines[n-1] == "" { + n-- + } + if n == 0 { + return nil + } + lines = lines[:n] + for j := 0; j < n; j++ { + // sec: line + line := lines[j] + if !strings.HasPrefix(line, "sec:") { + continue + } + secFields := strings.Split(line, ":") + if len(secFields) < 5 { + continue + } + if secFields[3] != "1" { // not RSA + continue + } + keyID := secFields[4] + uid := "" + fpr := "" + var privKey PrivateKey + // look for fpr:, uid: lines, order may vary and gpg2.1 + // may springle additional lines in (like gpr:) + Loop: + for k := j + 1; k < n && !strings.HasPrefix(lines[k], "sec:"); k++ { + switch { + case strings.HasPrefix(lines[k], "fpr:"): + fprFields := strings.Split(lines[k], ":") + // extract "Field 10 - User-ID" + // A FPR record stores the fingerprint here. + if len(fprFields) < 10 { + break Loop + } + fpr = fprFields[9] + if !strings.HasSuffix(fpr, keyID) { + break // strange, skip + } + privKey, err = gkm.retrieve(fpr) + if err != nil { + return err + } + case strings.HasPrefix(lines[k], "uid:"): + uidFields := strings.Split(lines[k], ":") + // extract "*** Field 10 - User-ID" + if len(uidFields) < 10 { + break Loop + } + uid = uidFields[9] + } + } + // sanity checking + if privKey == nil || uid == "" { + continue + } + // collected it all + err = consider(privKey, fpr, uid) + if err != nil { + return err + } + } + return nil +} + +func (gkm *GPGKeypairManager) Put(privKey PrivateKey) error { + // NOTE: we don't need this initially at least and this keypair mgr is not for general arbitrary usage + return fmt.Errorf("cannot import private key into GPG keyring") +} + +func (gkm *GPGKeypairManager) Get(keyID string) (PrivateKey, error) { + stop := errors.New("stop marker") + var hit PrivateKey + match := func(privk PrivateKey, fpr string, uid string) error { + if privk.PublicKey().ID() == keyID { + hit = privk + return stop + } + return nil + } + err := gkm.Walk(match) + if err == stop { + return hit, nil + } + if err != nil { + return nil, err + } + return nil, fmt.Errorf("cannot find key %q in GPG keyring", keyID) +} + +func (gkm *GPGKeypairManager) sign(fingerprint string, content []byte) ([]byte, error) { + out, err := gkm.gpg(content, "--personal-digest-preferences", "SHA512", "--default-key", "0x"+fingerprint, "--detach-sign") + if err != nil { + return nil, fmt.Errorf("cannot sign using GPG: %v", err) + } + return out, nil +} + +type gpgKeypairInfo struct { + privKey PrivateKey + fingerprint string +} + +func (gkm *GPGKeypairManager) findByName(name string) (*gpgKeypairInfo, error) { + stop := errors.New("stop marker") + var hit *gpgKeypairInfo + match := func(privk PrivateKey, fpr string, uid string) error { + if uid == name { + hit = &gpgKeypairInfo{ + privKey: privk, + fingerprint: fpr, + } + return stop + } + return nil + } + err := gkm.Walk(match) + if err == stop { + return hit, nil + } + if err != nil { + return nil, err + } + return nil, fmt.Errorf("cannot find key named %q in GPG keyring", name) +} + +// GetByName looks up a private key by name and returns it. +func (gkm *GPGKeypairManager) GetByName(name string) (PrivateKey, error) { + keyInfo, err := gkm.findByName(name) + if err != nil { + return nil, err + } + return keyInfo.privKey, nil +} + +var generateTemplate = ` +Key-Type: RSA +Key-Length: 4096 +Name-Real: %s +Creation-Date: seconds=%d +Preferences: SHA512 +` + +func (gkm *GPGKeypairManager) parametersForGenerate(passphrase string, name string) string { + fixedCreationTime := v1FixedTimestamp.Unix() + generateParams := fmt.Sprintf(generateTemplate, name, fixedCreationTime) + if passphrase != "" { + generateParams += "Passphrase: " + passphrase + "\n" + } + return generateParams +} + +// Generate creates a new key with the given passphrase and name. +func (gkm *GPGKeypairManager) Generate(passphrase string, name string) error { + _, err := gkm.findByName(name) + if err == nil { + return fmt.Errorf("key named %q already exists in GPG keyring", name) + } + generateParams := gkm.parametersForGenerate(passphrase, name) + _, err = gkm.gpg([]byte(generateParams), "--batch", "--gen-key") + if err != nil { + return err + } + return nil +} + +// Export returns the encoded text of the named public key. +func (gkm *GPGKeypairManager) Export(name string) ([]byte, error) { + keyInfo, err := gkm.findByName(name) + if err != nil { + return nil, err + } + return EncodePublicKey(keyInfo.privKey.PublicKey()) +} + +// Delete removes the named key pair from GnuPG's storage. +func (gkm *GPGKeypairManager) Delete(name string) error { + keyInfo, err := gkm.findByName(name) + if err != nil { + return err + } + _, err = gkm.gpg(nil, "--batch", "--delete-secret-and-public-key", "0x"+keyInfo.fingerprint) + if err != nil { + return err + } + return nil +} diff --git a/asserts/gpgkeypairmgr_test.go b/asserts/gpgkeypairmgr_test.go new file mode 100644 index 00000000..5968207b --- /dev/null +++ b/asserts/gpgkeypairmgr_test.go @@ -0,0 +1,331 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts_test + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "fmt" + "os" + "time" + + . "gopkg.in/check.v1" + + "golang.org/x/crypto/openpgp/packet" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/osutil" +) + +type gpgKeypairMgrSuite struct { + homedir string + keypairMgr asserts.KeypairManager +} + +var _ = Suite(&gpgKeypairMgrSuite{}) + +func (gkms *gpgKeypairMgrSuite) SetUpSuite(c *C) { + if !osutil.FileExists("/usr/bin/gpg1") && !osutil.FileExists("/usr/bin/gpg") { + c.Skip("gpg not installed") + } +} + +func (gkms *gpgKeypairMgrSuite) importKey(key string) { + assertstest.GPGImportKey(gkms.homedir, key) +} + +func (gkms *gpgKeypairMgrSuite) SetUpTest(c *C) { + gkms.homedir = c.MkDir() + os.Setenv("SNAP_GNUPG_HOME", gkms.homedir) + gkms.keypairMgr = asserts.NewGPGKeypairManager() + // import test key + gkms.importKey(assertstest.DevKey) +} + +func (gkms *gpgKeypairMgrSuite) TearDownTest(c *C) { + os.Unsetenv("SNAP_GNUPG_HOME") +} + +func (gkms *gpgKeypairMgrSuite) TestGetPublicKeyLooksGood(c *C) { + got, err := gkms.keypairMgr.Get(assertstest.DevKeyID) + c.Assert(err, IsNil) + keyID := got.PublicKey().ID() + c.Check(keyID, Equals, assertstest.DevKeyID) +} + +func (gkms *gpgKeypairMgrSuite) TestGetNotFound(c *C) { + got, err := gkms.keypairMgr.Get("ffffffffffffffff") + c.Check(err, ErrorMatches, `cannot find key "ffffffffffffffff" in GPG keyring`) + c.Check(got, IsNil) +} + +func (gkms *gpgKeypairMgrSuite) TestUseInSigning(c *C) { + store := assertstest.NewStoreStack("trusted", testPrivKey0, testPrivKey1) + + devKey, err := gkms.keypairMgr.Get(assertstest.DevKeyID) + c.Assert(err, IsNil) + + devAcct := assertstest.NewAccount(store, "devel1", map[string]interface{}{ + "account-id": "dev1-id", + }, "") + devAccKey := assertstest.NewAccountKey(store, devAcct, nil, devKey.PublicKey(), "") + + signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: gkms.keypairMgr, + }) + c.Assert(err, IsNil) + + checkDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + }) + c.Assert(err, IsNil) + // add store key + err = checkDB.Add(store.StoreAccountKey("")) + c.Assert(err, IsNil) + // enable devel key + err = checkDB.Add(devAcct) + c.Assert(err, IsNil) + err = checkDB.Add(devAccKey) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": "dev1-id", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": time.Now().Format(time.RFC3339), + } + snapBuild, err := signDB.Sign(asserts.SnapBuildType, headers, nil, assertstest.DevKeyID) + c.Assert(err, IsNil) + + err = checkDB.Check(snapBuild) + c.Check(err, IsNil) +} + +func (gkms *gpgKeypairMgrSuite) TestGetNotUnique(c *C) { + mockGPG := func(prev asserts.GPGRunner, input []byte, args ...string) ([]byte, error) { + if args[1] == "--list-secret-keys" { + return prev(input, args...) + } + c.Assert(args[1], Equals, "--export") + + pk1, err := rsa.GenerateKey(rand.Reader, 512) + c.Assert(err, IsNil) + pk2, err := rsa.GenerateKey(rand.Reader, 512) + c.Assert(err, IsNil) + + buf := new(bytes.Buffer) + err = packet.NewRSAPublicKey(time.Now(), &pk1.PublicKey).Serialize(buf) + c.Assert(err, IsNil) + err = packet.NewRSAPublicKey(time.Now(), &pk2.PublicKey).Serialize(buf) + c.Assert(err, IsNil) + + return buf.Bytes(), nil + } + restore := asserts.MockRunGPG(mockGPG) + defer restore() + + _, err := gkms.keypairMgr.Get(assertstest.DevKeyID) + c.Check(err, ErrorMatches, `cannot load GPG public key with fingerprint "[A-F0-9]+": cannot select exported public key, found many`) +} + +func (gkms *gpgKeypairMgrSuite) TestUseInSigningBrokenSignature(c *C) { + _, rsaPrivKey := assertstest.ReadPrivKey(assertstest.DevKey) + pgpPrivKey := packet.NewRSAPrivateKey(time.Unix(1, 0), rsaPrivKey) + + var breakSig func(sig *packet.Signature, cont []byte) []byte + + mockGPG := func(prev asserts.GPGRunner, input []byte, args ...string) ([]byte, error) { + if args[1] == "--list-secret-keys" || args[1] == "--export" { + return prev(input, args...) + } + n := len(args) + c.Assert(args[n-1], Equals, "--detach-sign") + + sig := new(packet.Signature) + sig.PubKeyAlgo = packet.PubKeyAlgoRSA + sig.Hash = crypto.SHA512 + sig.CreationTime = time.Now() + + // poking to break the signature + cont := breakSig(sig, input) + + h := sig.Hash.New() + h.Write([]byte(cont)) + + err := sig.Sign(h, pgpPrivKey, nil) + c.Assert(err, IsNil) + + buf := new(bytes.Buffer) + sig.Serialize(buf) + return buf.Bytes(), nil + } + restore := asserts.MockRunGPG(mockGPG) + defer restore() + + signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: gkms.keypairMgr, + }) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": "dev1-id", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": time.Now().Format(time.RFC3339), + } + + tests := []struct { + breakSig func(*packet.Signature, []byte) []byte + expectedErr string + }{ + {func(sig *packet.Signature, cont []byte) []byte { + sig.Hash = crypto.SHA1 + return cont + }, "cannot sign assertion: bad GPG produced signature: expected SHA512 digest"}, + {func(sig *packet.Signature, cont []byte) []byte { + return cont[:5] + }, "cannot sign assertion: bad GPG produced signature: it does not verify:.*"}, + } + + for _, t := range tests { + breakSig = t.breakSig + + _, err = signDB.Sign(asserts.SnapBuildType, headers, nil, assertstest.DevKeyID) + c.Check(err, ErrorMatches, t.expectedErr) + } + +} + +func (gkms *gpgKeypairMgrSuite) TestUseInSigningFailure(c *C) { + mockGPG := func(prev asserts.GPGRunner, input []byte, args ...string) ([]byte, error) { + if args[1] == "--list-secret-keys" || args[1] == "--export" { + return prev(input, args...) + } + n := len(args) + c.Assert(args[n-1], Equals, "--detach-sign") + return nil, fmt.Errorf("boom") + } + restore := asserts.MockRunGPG(mockGPG) + defer restore() + + signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: gkms.keypairMgr, + }) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": "dev1-id", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": time.Now().Format(time.RFC3339), + } + + _, err = signDB.Sign(asserts.SnapBuildType, headers, nil, assertstest.DevKeyID) + c.Check(err, ErrorMatches, "cannot sign assertion: cannot sign using GPG: boom") +} + +const shortPrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQOYBFdGO7MBCADltsXglnDQdfBw0yOVpKZdkuvSnJKKn1H72PapgAr7ucLqNBCA +js0kltDTa2LQP4vljiTyoMzOMnex4kXwRPlF+poZIEBHDLT0i/6sJ6mDukss1HBR +GgNpU3y49WTXc8qxFY4clhbuqgQmy6bUmaVoo3Z4z7cqbsCepWfx5y+vJwMYqlo3 +Nb4q2+hTKS/o3yLiYB7/hkEhMZrFrOPR5SM7Tz5y7cpF6ObY+JZIp/MK+LsLWLji +fEX/pcOtSjFdQqbcnhJJscXRERlFQDbc+gNmZYZ2RqdH5o46OliHkGhVDVTiW25A +SqhGfnodypbZ9QAPSRvhLrN64AqEsvRb3I13ABEBAAEAB/9cQKg8Nz6sQUkkDm9C +iCK1/qyNYwro9+3VXj9FOCJxEJuqMemUr4TMVnMcDQrchkC5GnpVJGXLw3HVcwFS +amjPhUKAp7aYsg40DcrjuXP27oiFQvWuZGuNT5WNtCNg8WQr9POjIFWqWIYdTHk9 +9Ux79vW7s/Oj62GY9OWHPSilxpq1MjDKo9CSMbLeWxW+gbDxaD7cK7H/ONcz8bZ7 +pRfEhNIx3mEbWaZpWRrf+dSUx2OJbPGRkeFFMbCNapqftse173BZCwUKsW7RTp2S +w8Vpo2Ky63Jlpz1DpoMDBz2vSH7pzaqAdnziI2r0IKiidajXFfpXJpJ3ICo/QhWj +x1eRBADrI4I99zHeyy+12QMpkDrOu+ahF6/emdsm1FIy88TqeBmLkeXCXKZIpU3c +USnxzm0nPNbOl7Nvf2VdAyeAftyag7t38Cud5MXldv/iY0e6oTKzxgha37yr6oRv +PZ6VGwbkBvWti1HL4yx1QnkHFS6ailR9WiiHr3HaWAklZAsC0QQA+hgOi0V9fMZZ +Y4/iFVRI9k1NK3pl0mP7pVTzbcjVYspLdIPQxPDsHJW0z48g23KOt0vL3yZvxdBx +cfYGqIonAX19aMD5D4bNLx616pZs78DKGlOz6iXDcaib+n/uCNWxd5R/0m/zugrB +qklpyIC/uxx+SmkJqqq378ytfvBMzccD/3Y6m3PM0ZnrIkr4Q7cKi9ao9rvM+J7o +ziMgfnKWedNDxNa4tIVYYGPiXsjxY/ASUyxVjUPbkyCy3ubZrew0zQ9+kQbO/6vB +WAg9ffT9M92QbSDjuxgUiC5GfvlCoDgJtuLRHd0YLDgUCS5nwb+teEsOpiNWEGXc +Tr+5HZO+g6wxT6W0BiAoeHh4KYkBOAQTAQIAIgUCV0Y7swIbLwYLCQgHAwIGFQgC +CQoLBBYCAwECHgECF4AACgkQEYacUJMr9p/i5wf/XbEiAe1+Y/ZNMO8PYnq1Nktk +CbZEfQo+QH/9gJpt4p78YseWeUp14gsULLks3xRojlKNzYkqBpJcP7Ex+hQ3LEp7 +9IVbept5md4uuZcU0GFF42WAYXExd2cuxPv3lmWHOPuN63a/xpp0M2vYDfpt63qi +Tly5/P4+NgpD6vAh8zwRHuBV/0mno/QX6cUCLVxq2v1aOqC9zq9B5sdYKQKjsQBP +NOXCt1wPaINkqiW/8w2KhUl6mL6vhO0Onqu/F7M/YNXitv6Z2NFdFUVBh58UZW3C +2jrc8JeRQ4Qlr1oeHh2loYOdZfxFPxRjhsRTnNKY8UHWLfbeI6lMqxR5G3DS+g== +=kQRo +-----END PGP PRIVATE KEY BLOCK----- +` + +func (gkms *gpgKeypairMgrSuite) TestUseInSigningKeyTooShort(c *C) { + gkms.importKey(shortPrivKey) + privk, _ := assertstest.ReadPrivKey(shortPrivKey) + + signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: gkms.keypairMgr, + }) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": "dev1-id", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": time.Now().Format(time.RFC3339), + } + + _, err = signDB.Sign(asserts.SnapBuildType, headers, nil, privk.PublicKey().ID()) + c.Check(err, ErrorMatches, `cannot sign assertion: signing needs at least a 4096 bits key, got 2048`) +} + +func (gkms *gpgKeypairMgrSuite) TestParametersForGenerate(c *C) { + gpgKeypairMgr := gkms.keypairMgr.(*asserts.GPGKeypairManager) + baseParameters := ` +Key-Type: RSA +Key-Length: 4096 +Name-Real: test-key +Creation-Date: seconds=1451606400 +Preferences: SHA512 +` + + tests := []struct { + passphrase string + extraParameters string + }{ + {"", ""}, + {"secret", "Passphrase: secret\n"}, + } + + for _, test := range tests { + parameters := gpgKeypairMgr.ParametersForGenerate(test.passphrase, "test-key") + c.Check(parameters, Equals, baseParameters+test.extraParameters) + } +} diff --git a/asserts/header_checks.go b/asserts/header_checks.go new file mode 100644 index 00000000..672c9474 --- /dev/null +++ b/asserts/header_checks.go @@ -0,0 +1,252 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts + +import ( + "crypto" + "encoding/base64" + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +// common checks used when decoding/assembling assertions + +func checkExistsString(headers map[string]interface{}, name string) (string, error) { + value, ok := headers[name] + if !ok { + return "", fmt.Errorf("%q header is mandatory", name) + } + s, ok := value.(string) + if !ok { + return "", fmt.Errorf("%q header must be a string", name) + } + return s, nil +} + +func checkNotEmptyString(headers map[string]interface{}, name string) (string, error) { + s, err := checkExistsString(headers, name) + if err != nil { + return "", err + } + if len(s) == 0 { + return "", fmt.Errorf("%q header should not be empty", name) + } + return s, nil +} + +func checkOptionalString(headers map[string]interface{}, name string) (string, error) { + value, ok := headers[name] + if !ok { + return "", nil + } + s, ok := value.(string) + if !ok { + return "", fmt.Errorf("%q header must be a string", name) + } + return s, nil +} + +func checkPrimaryKey(headers map[string]interface{}, primKey string) (string, error) { + value, err := checkNotEmptyString(headers, primKey) + if err != nil { + return "", err + } + if strings.Contains(value, "/") { + return "", fmt.Errorf("%q primary key header cannot contain '/'", primKey) + } + return value, nil +} + +func checkAssertType(assertType *AssertionType) error { + if assertType == nil { + return fmt.Errorf("internal error: assertion type cannot be nil") + } + // sanity check against known canonical + sanity := typeRegistry[assertType.Name] + switch sanity { + case assertType: + // fine, matches canonical + return nil + case nil: + return fmt.Errorf("internal error: unknown assertion type: %q", assertType.Name) + default: + return fmt.Errorf("internal error: unpredefined assertion type for name %q used (unexpected address %p)", assertType.Name, assertType) + } +} + +// use 'defl' default if missing +func checkIntWithDefault(headers map[string]interface{}, name string, defl int) (int, error) { + value, ok := headers[name] + if !ok { + return defl, nil + } + s, ok := value.(string) + if !ok { + return -1, fmt.Errorf("%q header is not an integer: %v", name, value) + } + m, err := strconv.Atoi(s) + if err != nil { + return -1, fmt.Errorf("%q header is not an integer: %v", name, s) + } + return m, nil +} + +func checkInt(headers map[string]interface{}, name string) (int, error) { + valueStr, err := checkNotEmptyString(headers, name) + if err != nil { + return -1, err + } + value, err := strconv.Atoi(valueStr) + if err != nil { + return -1, fmt.Errorf("%q header is not an integer: %v", name, valueStr) + } + return value, nil +} + +func checkRFC3339Date(headers map[string]interface{}, name string) (time.Time, error) { + dateStr, err := checkNotEmptyString(headers, name) + if err != nil { + return time.Time{}, err + } + date, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + return time.Time{}, fmt.Errorf("%q header is not a RFC3339 date: %v", name, err) + } + return date, nil +} + +func checkRFC3339DateWithDefault(headers map[string]interface{}, name string, defl time.Time) (time.Time, error) { + value, ok := headers[name] + if !ok { + return defl, nil + } + dateStr, ok := value.(string) + if !ok { + return time.Time{}, fmt.Errorf("%q header must be a string", name) + } + date, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + return time.Time{}, fmt.Errorf("%q header is not a RFC3339 date: %v", name, err) + } + return date, nil +} + +func checkUint(headers map[string]interface{}, name string, bitSize int) (uint64, error) { + valueStr, err := checkNotEmptyString(headers, name) + if err != nil { + return 0, err + } + + value, err := strconv.ParseUint(valueStr, 10, bitSize) + if err != nil { + return 0, fmt.Errorf("%q header is not an unsigned integer: %v", name, valueStr) + } + return value, nil +} + +func checkDigest(headers map[string]interface{}, name string, h crypto.Hash) ([]byte, error) { + digestStr, err := checkNotEmptyString(headers, name) + if err != nil { + return nil, err + } + b, err := base64.RawURLEncoding.DecodeString(digestStr) + if err != nil { + return nil, fmt.Errorf("%q header cannot be decoded: %v", name, err) + } + if len(b) != h.Size() { + return nil, fmt.Errorf("%q header does not have the expected bit length: %d", name, len(b)*8) + } + + return b, nil +} + +var anyString = regexp.MustCompile("") + +func checkStringListInMap(m map[string]interface{}, name, what string, pattern *regexp.Regexp) ([]string, error) { + value, ok := m[name] + if !ok { + return nil, nil + } + lst, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf("%s must be a list of strings", what) + } + if len(lst) == 0 { + return nil, nil + } + res := make([]string, len(lst)) + for i, v := range lst { + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("%s must be a list of strings", what) + } + if !pattern.MatchString(s) { + return nil, fmt.Errorf("%s contains an invalid element: %q", what, s) + } + res[i] = s + } + return res, nil +} + +func checkStringList(headers map[string]interface{}, name string) ([]string, error) { + return checkStringListMatches(headers, name, anyString) +} + +func checkStringListMatches(headers map[string]interface{}, name string, pattern *regexp.Regexp) ([]string, error) { + return checkStringListInMap(headers, name, fmt.Sprintf("%q header", name), pattern) +} + +func checkStringMatches(headers map[string]interface{}, name string, pattern *regexp.Regexp) (string, error) { + s, err := checkNotEmptyString(headers, name) + if err != nil { + return "", err + } + if !pattern.MatchString(s) { + return "", fmt.Errorf("%q header contains invalid characters: %q", name, s) + } + return s, nil +} + +func checkOptionalBool(headers map[string]interface{}, name string) (bool, error) { + value, ok := headers[name] + if !ok { + return false, nil + } + s, ok := value.(string) + if !ok || (s != "true" && s != "false") { + return false, fmt.Errorf("%q header must be 'true' or 'false'", name) + } + return s == "true", nil +} + +func checkMap(headers map[string]interface{}, name string) (map[string]interface{}, error) { + value, ok := headers[name] + if !ok { + return nil, nil + } + m, ok := value.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%q header must be a map", name) + } + return m, nil +} diff --git a/asserts/headers.go b/asserts/headers.go new file mode 100644 index 00000000..b7c42eb1 --- /dev/null +++ b/asserts/headers.go @@ -0,0 +1,318 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts + +import ( + "bytes" + "fmt" + "regexp" + "sort" + "strings" + "unicode/utf8" +) + +var ( + nl = []byte("\n") + nlnl = []byte("\n\n") + + // for basic sanity checking of header names + headerNameSanity = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$") +) + +func parseHeaders(head []byte) (map[string]interface{}, error) { + if !utf8.Valid(head) { + return nil, fmt.Errorf("header is not utf8") + } + headers := make(map[string]interface{}) + lines := strings.Split(string(head), "\n") + for i := 0; i < len(lines); { + entry := lines[i] + nameValueSplit := strings.Index(entry, ":") + if nameValueSplit == -1 { + return nil, fmt.Errorf("header entry missing ':' separator: %q", entry) + } + name := entry[:nameValueSplit] + if !headerNameSanity.MatchString(name) { + return nil, fmt.Errorf("invalid header name: %q", name) + } + + consumed := nameValueSplit + 1 + var value interface{} + var err error + value, i, err = parseEntry(consumed, i, lines, 0) + if err != nil { + return nil, err + } + + if _, ok := headers[name]; ok { + return nil, fmt.Errorf("repeated header: %q", name) + } + + headers[name] = value + } + return headers, nil +} + +const ( + commonPrefix = " " + multilinePrefix = " " + listChar = "-" + listPrefix = commonPrefix + listChar +) + +func nestingPrefix(baseIndent int, prefix string) string { + return strings.Repeat(" ", baseIndent) + prefix +} + +func parseEntry(consumedByIntro int, first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) { + entry := lines[first] + i := first + 1 + if consumedByIntro == len(entry) { + // multiline values + basePrefix := nestingPrefix(baseIndent, commonPrefix) + if i < len(lines) && strings.HasPrefix(lines[i], basePrefix) { + rest := lines[i][len(basePrefix):] + if strings.HasPrefix(rest, listChar) { + // list + return parseList(i, lines, baseIndent) + } + if len(rest) > 0 && rest[0] != ' ' { + // map + return parseMap(i, lines, baseIndent) + } + } + + return parseMultilineText(i, lines, baseIndent) + } + + // simple one-line value + if entry[consumedByIntro] != ' ' { + return nil, -1, fmt.Errorf("header entry should have a space or newline (for multiline) before value: %q", entry) + } + + return entry[consumedByIntro+1:], i, nil +} + +func parseMultilineText(first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) { + size := 0 + i := first + j := i + prefix := nestingPrefix(baseIndent, multilinePrefix) + for j < len(lines) { + iline := lines[j] + if !strings.HasPrefix(iline, prefix) { + break + } + size += len(iline) - len(prefix) + 1 + j++ + } + if j == i { + var cur string + if i == len(lines) { + cur = "EOF" + } else { + cur = fmt.Sprintf("%q", lines[i]) + } + return nil, -1, fmt.Errorf("expected %d chars nesting prefix after multiline introduction %q: %s", len(prefix), lines[i-1], cur) + } + + valueBuf := bytes.NewBuffer(make([]byte, 0, size-1)) + valueBuf.WriteString(lines[i][len(prefix):]) + i++ + for i < j { + valueBuf.WriteByte('\n') + valueBuf.WriteString(lines[i][len(prefix):]) + i++ + } + + return valueBuf.String(), i, nil +} + +func parseList(first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) { + lst := []interface{}(nil) + j := first + prefix := nestingPrefix(baseIndent, listPrefix) + for j < len(lines) { + if !strings.HasPrefix(lines[j], prefix) { + return lst, j, nil + } + var v interface{} + var err error + v, j, err = parseEntry(len(prefix), j, lines, baseIndent+len(listPrefix)-1) + if err != nil { + return nil, -1, err + } + lst = append(lst, v) + } + return lst, j, nil +} + +func parseMap(first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) { + m := make(map[string]interface{}) + j := first + prefix := nestingPrefix(baseIndent, commonPrefix) + for j < len(lines) { + if !strings.HasPrefix(lines[j], prefix) { + return m, j, nil + } + + entry := lines[j][len(prefix):] + keyValueSplit := strings.Index(entry, ":") + if keyValueSplit == -1 { + return nil, -1, fmt.Errorf("map entry missing ':' separator: %q", entry) + } + key := entry[:keyValueSplit] + if !headerNameSanity.MatchString(key) { + return nil, -1, fmt.Errorf("invalid map entry key: %q", key) + } + + consumed := keyValueSplit + 1 + var value interface{} + var err error + value, j, err = parseEntry(len(prefix)+consumed, j, lines, len(prefix)) + if err != nil { + return nil, -1, err + } + + if _, ok := m[key]; ok { + return nil, -1, fmt.Errorf("repeated map entry: %q", key) + } + + m[key] = value + } + return m, j, nil +} + +// checkHeader checks that the header values are strings, or nested lists or maps with strings as the only scalars +func checkHeader(v interface{}) error { + switch x := v.(type) { + case string: + return nil + case []interface{}: + for _, elem := range x { + err := checkHeader(elem) + if err != nil { + return err + } + } + return nil + case map[string]interface{}: + for _, elem := range x { + err := checkHeader(elem) + if err != nil { + return err + } + } + return nil + default: + return fmt.Errorf("header values must be strings or nested lists or maps with strings as the only scalars: %v", v) + } +} + +// checkHeaders checks that headers are of expected types +func checkHeaders(headers map[string]interface{}) error { + for name, value := range headers { + err := checkHeader(value) + if err != nil { + return fmt.Errorf("header %q: %v", name, err) + } + } + return nil +} + +// copyHeader helps deep copying header values to defend against external mutations +func copyHeader(v interface{}) interface{} { + switch x := v.(type) { + case string: + return x + case []interface{}: + res := make([]interface{}, len(x)) + for i, elem := range x { + res[i] = copyHeader(elem) + } + return res + case map[string]interface{}: + res := make(map[string]interface{}, len(x)) + for name, value := range x { + if value == nil { + continue // normalize nils out + } + res[name] = copyHeader(value) + } + return res + default: + panic(fmt.Sprintf("internal error: encountered unexpected value type copying headers: %v", v)) + } +} + +// copyHeader helps deep copying headers to defend against external mutations +func copyHeaders(headers map[string]interface{}) map[string]interface{} { + return copyHeader(headers).(map[string]interface{}) +} + +func appendEntry(buf *bytes.Buffer, intro string, v interface{}, baseIndent int) { + switch x := v.(type) { + case nil: + return // omit + case string: + buf.WriteByte('\n') + buf.WriteString(intro) + if strings.IndexRune(x, '\n') != -1 { + // multiline value => quote by 4-space indenting + buf.WriteByte('\n') + pfx := nestingPrefix(baseIndent, multilinePrefix) + buf.WriteString(pfx) + x = strings.Replace(x, "\n", "\n"+pfx, -1) + } else { + buf.WriteByte(' ') + } + buf.WriteString(x) + case []interface{}: + if len(x) == 0 { + return // simply omit + } + buf.WriteByte('\n') + buf.WriteString(intro) + pfx := nestingPrefix(baseIndent, listPrefix) + for _, elem := range x { + appendEntry(buf, pfx, elem, baseIndent+len(listPrefix)-1) + } + case map[string]interface{}: + if len(x) == 0 { + return // simply omit + } + buf.WriteByte('\n') + buf.WriteString(intro) + // emit entries sorted by key + keys := make([]string, len(x)) + i := 0 + for key := range x { + keys[i] = key + i++ + } + sort.Strings(keys) + pfx := nestingPrefix(baseIndent, commonPrefix) + for _, key := range keys { + appendEntry(buf, pfx+key+":", x[key], len(pfx)) + } + default: + panic(fmt.Sprintf("internal error: encountered unexpected value type formatting headers: %v", v)) + } +} diff --git a/asserts/headers_test.go b/asserts/headers_test.go new file mode 100644 index 00000000..4907b9c8 --- /dev/null +++ b/asserts/headers_test.go @@ -0,0 +1,396 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts_test + +import ( + "bytes" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type headersSuite struct{} + +var _ = Suite(&headersSuite{}) + +func (s *headersSuite) TestParseHeadersSimple(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: 1 +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": "1", + "bar": "baz", + }) +} + +func (s *headersSuite) TestParseHeadersMultiline(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: + abc + +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": "abc\n", + "bar": "baz", + }) + + m, err = asserts.ParseHeaders([]byte(`foo: 1 +bar: + baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": "1", + "bar": "baz", + }) + + m, err = asserts.ParseHeaders([]byte(`foo: 1 +bar: + baz + `)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": "1", + "bar": "baz\n", + }) + + m, err = asserts.ParseHeaders([]byte(`foo: 1 +bar: + baz + + baz2`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": "1", + "bar": "baz\n\nbaz2", + }) +} + +func (s *headersSuite) TestParseHeadersSimpleList(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: + - x + - y + - z +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": []interface{}{"x", "y", "z"}, + "bar": "baz", + }) +} + +func (s *headersSuite) TestParseHeadersListNestedMultiline(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: + - x + - + y1 + y2 + + - z +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": []interface{}{"x", "y1\ny2\n", "z"}, + "bar": "baz", + }) + + m, err = asserts.ParseHeaders([]byte(`bar: baz +foo: + - + - u1 + - u2 + - + y1 + y2 + `)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": []interface{}{[]interface{}{"u1", "u2"}, "y1\ny2\n"}, + "bar": "baz", + }) +} + +func (s *headersSuite) TestParseHeadersSimpleMap(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: + x: X + yy: YY + z5: +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": map[string]interface{}{ + "x": "X", + "yy": "YY", + "z5": "", + }, + "bar": "baz", + }) +} + +func (s *headersSuite) TestParseHeadersMapNestedMultiline(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: + x: X + yy: + YY1 + YY2 + u: + - u1 + - u2 +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": map[string]interface{}{ + "x": "X", + "yy": "YY1\nYY2", + "u": []interface{}{"u1", "u2"}, + }, + "bar": "baz", + }) + + m, err = asserts.ParseHeaders([]byte(`one: + two: + three: `)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "one": map[string]interface{}{ + "two": map[string]interface{}{ + "three": "", + }, + }, + }) + + m, err = asserts.ParseHeaders([]byte(`one: + two: + three`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "one": map[string]interface{}{ + "two": "three", + }, + }) + + m, err = asserts.ParseHeaders([]byte(`map-within-map: + lev1: + lev2: x`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "map-within-map": map[string]interface{}{ + "lev1": map[string]interface{}{ + "lev2": "x", + }, + }, + }) + + m, err = asserts.ParseHeaders([]byte(`list-of-maps: + - + entry: foo + bar: baz + - + entry: bar`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "list-of-maps": []interface{}{ + map[string]interface{}{ + "entry": "foo", + "bar": "baz", + }, + map[string]interface{}{ + "entry": "bar", + }, + }, + }) +} + +func (s *headersSuite) TestParseHeadersMapErrors(c *C) { + _, err := asserts.ParseHeaders([]byte(`foo: + x X +bar: baz`)) + c.Check(err, ErrorMatches, `map entry missing ':' separator: "x X"`) + + _, err = asserts.ParseHeaders([]byte(`foo: + 0x: X +bar: baz`)) + c.Check(err, ErrorMatches, `invalid map entry key: "0x"`) + + _, err = asserts.ParseHeaders([]byte(`foo: + a: a + a: b`)) + c.Check(err, ErrorMatches, `repeated map entry: "a"`) +} + +func (s *headersSuite) TestParseHeadersErrors(c *C) { + _, err := asserts.ParseHeaders([]byte(`foo: 1 +bar:baz`)) + c.Check(err, ErrorMatches, `header entry should have a space or newline \(for multiline\) before value: "bar:baz"`) + + _, err = asserts.ParseHeaders([]byte(`foo: + - x + - y + - z +bar: baz`)) + c.Check(err, ErrorMatches, `expected 4 chars nesting prefix after multiline introduction "foo:": " - x"`) + + _, err = asserts.ParseHeaders([]byte(`foo: + - x + - y + - z +bar:`)) + c.Check(err, ErrorMatches, `expected 4 chars nesting prefix after multiline introduction "bar:": EOF`) +} + +func (s *headersSuite) TestAppendEntrySimple(c *C) { + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", "baz", 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": "baz", + }) +} + +func (s *headersSuite) TestAppendEntryMultiline(c *C) { + multilines := []string{ + "a\n", + "a\nb", + "baz\n baz1\nbaz2", + "baz\n baz1\nbaz2\n", + "baz\n baz1\nbaz2\n\n", + } + + for _, multiline := range multilines { + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", multiline, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": multiline, + }) + } +} + +func (s *headersSuite) TestAppendEntrySimpleList(c *C) { + lst := []interface{}{"x", "y", "z"} + + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", lst, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": lst, + }) +} + +func (s *headersSuite) TestAppendEntryListNested(c *C) { + lst := []interface{}{"x", "a\nb\n", "", []interface{}{"u1", []interface{}{"w1", "w2"}}} + + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", lst, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": lst, + }) +} + +func (s *headersSuite) TestAppendEntrySimpleMap(c *C) { + mp := map[string]interface{}{ + "x": "X", + "yy": "YY", + "z5": "", + } + + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", mp, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": mp, + }) +} + +func (s *headersSuite) TestAppendEntryNestedMap(c *C) { + mp := map[string]interface{}{ + "x": "X", + "u": []interface{}{"u1", "u2"}, + "yy": "YY1\nYY2", + "m": map[string]interface{}{"a": "A", "b": map[string]interface{}{"x": "X", "y": "Y"}}, + } + + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", mp, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": mp, + }) +} + +func (s *headersSuite) TestAppendEntryOmitting(c *C) { + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", []interface{}{}, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + }) + + lst := []interface{}{nil, []interface{}{}, "z"} + + buf = bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", lst, 0) + + m, err = asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": []interface{}{"z"}, + }) + + buf = bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", map[string]interface{}{}, 0) + + m, err = asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + }) +} diff --git a/asserts/ifacedecls.go b/asserts/ifacedecls.go new file mode 100644 index 00000000..26a6d45e --- /dev/null +++ b/asserts/ifacedecls.go @@ -0,0 +1,808 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +type attrMatcher interface { + match(context string, v interface{}) error +} + +func chain(context, k string) string { + if context == "" { + return k + } + return fmt.Sprintf("%s.%s", context, k) +} + +type compileContext struct { + dotted string + hadMap bool + wasAlt bool +} + +func (cc compileContext) String() string { + return cc.dotted +} + +func (cc compileContext) keyEntry(k string) compileContext { + return compileContext{ + dotted: chain(cc.dotted, k), + hadMap: true, + wasAlt: false, + } +} + +func (cc compileContext) alt(alt int) compileContext { + return compileContext{ + dotted: fmt.Sprintf("%s/alt#%d/", cc.dotted, alt+1), + hadMap: cc.hadMap, + wasAlt: true, + } +} + +// compileAttrMatcher compiles an attrMatcher derived from constraints, +func compileAttrMatcher(cc compileContext, constraints interface{}) (attrMatcher, error) { + switch x := constraints.(type) { + case map[string]interface{}: + return compileMapAttrMatcher(cc, x) + case []interface{}: + if cc.wasAlt { + return nil, fmt.Errorf("cannot nest alternative constraints directly at %q", cc) + } + return compileAltAttrMatcher(cc, x) + case string: + if !cc.hadMap { + return nil, fmt.Errorf("first level of non alternative constraints must be a set of key-value contraints") + } + return compileRegexpAttrMatcher(cc, x) + default: + return nil, fmt.Errorf("constraint %q must be a key-value map, regexp or a list of alternative constraints: %v", cc, x) + } +} + +type mapAttrMatcher map[string]attrMatcher + +func compileMapAttrMatcher(cc compileContext, m map[string]interface{}) (attrMatcher, error) { + matcher := make(mapAttrMatcher) + for k, constraint := range m { + matcher1, err := compileAttrMatcher(cc.keyEntry(k), constraint) + if err != nil { + return nil, err + } + matcher[k] = matcher1 + } + return matcher, nil +} + +func matchEntry(context, k string, matcher1 attrMatcher, v interface{}) error { + context = chain(context, k) + if v == nil { + return fmt.Errorf("attribute %q has constraints but is unset", context) + } + if err := matcher1.match(context, v); err != nil { + return err + } + return nil +} + +func matchList(context string, matcher attrMatcher, l []interface{}) error { + for i, elem := range l { + if err := matcher.match(chain(context, strconv.Itoa(i)), elem); err != nil { + return err + } + } + return nil +} + +func (matcher mapAttrMatcher) match(context string, v interface{}) error { + switch x := v.(type) { + case map[string]interface{}: // maps in attributes look like this + for k, matcher1 := range matcher { + if err := matchEntry(context, k, matcher1, x[k]); err != nil { + return err + } + } + case []interface{}: + return matchList(context, matcher, x) + default: + return fmt.Errorf("attribute %q must be a map", context) + } + return nil +} + +type regexpAttrMatcher struct { + *regexp.Regexp +} + +func compileRegexpAttrMatcher(cc compileContext, s string) (attrMatcher, error) { + rx, err := regexp.Compile("^(" + s + ")$") + if err != nil { + return nil, fmt.Errorf("cannot compile %q constraint %q: %v", cc, s, err) + } + return regexpAttrMatcher{rx}, nil +} + +func (matcher regexpAttrMatcher) match(context string, v interface{}) error { + var s string + switch x := v.(type) { + case string: + s = x + case bool: + s = strconv.FormatBool(x) + case int64: + s = strconv.FormatInt(x, 10) + case []interface{}: + return matchList(context, matcher, x) + default: + return fmt.Errorf("attribute %q must be a scalar or list", context) + } + if !matcher.Regexp.MatchString(s) { + return fmt.Errorf("attribute %q value %q does not match %v", context, s, matcher.Regexp) + } + return nil + +} + +type altAttrMatcher struct { + alts []attrMatcher +} + +func compileAltAttrMatcher(cc compileContext, l []interface{}) (attrMatcher, error) { + alts := make([]attrMatcher, len(l)) + for i, constraint := range l { + matcher1, err := compileAttrMatcher(cc.alt(i), constraint) + if err != nil { + return nil, err + } + alts[i] = matcher1 + } + return altAttrMatcher{alts}, nil + +} + +func (matcher altAttrMatcher) match(context string, v interface{}) error { + var firstErr error + for _, alt := range matcher.alts { + err := alt.match(context, v) + if err == nil { + return nil + } + if firstErr == nil { + firstErr = err + } + } + ctxDescr := "" + if context != "" { + ctxDescr = fmt.Sprintf(" for attribute %q", context) + } + return fmt.Errorf("no alternative%s matches: %v", ctxDescr, firstErr) +} + +// AttributeConstraints implements a set of constraints on the attributes of a slot or plug. +type AttributeConstraints struct { + matcher attrMatcher +} + +// compileAttributeConstraints checks and compiles a mapping or list +// from the assertion format into AttributeConstraints. +func compileAttributeConstraints(constraints interface{}) (*AttributeConstraints, error) { + matcher, err := compileAttrMatcher(compileContext{}, constraints) + if err != nil { + return nil, err + } + return &AttributeConstraints{matcher: matcher}, nil +} + +type fixedAttrMatcher struct { + result error +} + +func (matcher fixedAttrMatcher) match(context string, v interface{}) error { + return matcher.result +} + +var ( + AlwaysMatchAttributes = &AttributeConstraints{matcher: fixedAttrMatcher{nil}} + NeverMatchAttributes = &AttributeConstraints{matcher: fixedAttrMatcher{errors.New("not allowed")}} +) + +// Check checks whether attrs don't match the constraints. +func (c *AttributeConstraints) Check(attrs map[string]interface{}) error { + return c.matcher.match("", attrs) +} + +// OnClassicConstraint specifies a constraint based whether the system is classic and optional specific distros' sets. +type OnClassicConstraint struct { + Classic bool + SystemIDs []string +} + +// rules + +var ( + validSnapType = regexp.MustCompile("^(?:core|kernel|gadget|app)$") + validDistro = regexp.MustCompile("^[-0-9a-z._]+$") + validSnapID = regexp.MustCompile("^[a-z0-9A-Z]{32}$") // snap-ids look like this + validPublisher = regexp.MustCompile("^(?:[a-z0-9A-Z]{32}|[-a-z0-9]{2,28}|\\$[A-Z][A-Z0-9_]*)$") // account ids look like snap-ids or are nice identifiers, support our own special markers $MARKER + + validIDConstraints = map[string]*regexp.Regexp{ + "slot-snap-type": validSnapType, + "slot-snap-id": validSnapID, + "slot-publisher-id": validPublisher, + "plug-snap-type": validSnapType, + "plug-snap-id": validSnapID, + "plug-publisher-id": validPublisher, + } +) + +func checkMapOrShortcut(context string, v interface{}) (m map[string]interface{}, invert bool, err error) { + switch x := v.(type) { + case map[string]interface{}: + return x, false, nil + case string: + switch x { + case "true": + return nil, false, nil + case "false": + return nil, true, nil + } + } + return nil, false, errors.New("unexpected type") +} + +type constraintsHolder interface { + setAttributeConstraints(field string, cstrs *AttributeConstraints) + setIDConstraints(field string, cstrs []string) + setOnClassicConstraint(onClassic *OnClassicConstraint) +} + +func baseCompileConstraints(context string, cDef constraintsDef, target constraintsHolder, attrConstraints, idConstraints []string) error { + cMap := cDef.cMap + if cMap == nil { + fixed := AlwaysMatchAttributes // "true" + if cDef.invert { // "false" + fixed = NeverMatchAttributes + } + for _, field := range attrConstraints { + target.setAttributeConstraints(field, fixed) + } + return nil + } + defaultUsed := 0 + for _, field := range idConstraints { + lst, err := checkStringListInMap(cMap, field, fmt.Sprintf("%s in %s", field, context), validIDConstraints[field]) + if err != nil { + return err + } + if lst == nil { + defaultUsed++ + } + target.setIDConstraints(field, lst) + } + for _, field := range attrConstraints { + cstrs := AlwaysMatchAttributes + v := cMap[field] + if v != nil { + var err error + cstrs, err = compileAttributeConstraints(cMap[field]) + if err != nil { + return fmt.Errorf("cannot compile %s in %s: %v", field, context, err) + } + } else { + defaultUsed++ + } + target.setAttributeConstraints(field, cstrs) + } + onClassic := cMap["on-classic"] + if onClassic == nil { + defaultUsed++ + } else { + var c *OnClassicConstraint + switch x := onClassic.(type) { + case string: + switch x { + case "true": + c = &OnClassicConstraint{Classic: true} + case "false": + c = &OnClassicConstraint{Classic: false} + } + case []interface{}: + lst, err := checkStringListInMap(cMap, "on-classic", fmt.Sprintf("on-classic in %s", context), validDistro) + if err != nil { + return err + } + c = &OnClassicConstraint{Classic: true, SystemIDs: lst} + } + if c == nil { + return fmt.Errorf("on-classic in %s must be 'true', 'false' or a list of operating system IDs", context) + } + target.setOnClassicConstraint(c) + } + if defaultUsed == len(attributeConstraints)+len(idConstraints)+1 { + return fmt.Errorf("%s must specify at least one of %s, %s, on-classic", context, strings.Join(attrConstraints, ", "), strings.Join(idConstraints, ", ")) + } + return nil +} + +type rule interface { + setConstraints(field string, cstrs []constraintsHolder) +} + +type constraintsDef struct { + cMap map[string]interface{} + invert bool +} + +type subruleCompiler func(context string, def constraintsDef) (constraintsHolder, error) + +func baseCompileRule(context string, rule interface{}, target rule, subrules []string, compilers map[string]subruleCompiler, defaultOutcome, invertedOutcome map[string]interface{}) error { + rMap, invert, err := checkMapOrShortcut(context, rule) + if err != nil { + return fmt.Errorf("%s must be a map or one of the shortcuts 'true' or 'false'", context) + } + if rMap == nil { + rMap = defaultOutcome // "true" + if invert { + rMap = invertedOutcome // "false" + } + } + defaultUsed := 0 + // compile and set subrules + for _, subrule := range subrules { + v := rMap[subrule] + var lst []interface{} + alternatives := false + switch x := v.(type) { + case nil: + v = defaultOutcome[subrule] + defaultUsed++ + case []interface{}: + alternatives = true + lst = x + } + if lst == nil { // v is map or a string, checked below + lst = []interface{}{v} + } + compiler := compilers[subrule] + if compiler == nil { + panic(fmt.Sprintf("no compiler for %s in %s", subrule, context)) + } + alts := make([]constraintsHolder, len(lst)) + for i, alt := range lst { + subctxt := fmt.Sprintf("%s in %s", subrule, context) + if alternatives { + subctxt = fmt.Sprintf("alternative %d of %s", i+1, subctxt) + } + cMap, invert, err := checkMapOrShortcut(subctxt, alt) + if err != nil || (cMap == nil && alternatives) { + efmt := "%s must be a map" + if !alternatives { + efmt = "%s must be a map or one of the shortcuts 'true' or 'false'" + } + return fmt.Errorf(efmt, subctxt) + } + + cstrs, err := compiler(subctxt, constraintsDef{ + cMap: cMap, + invert: invert, + }) + if err != nil { + return err + } + alts[i] = cstrs + } + target.setConstraints(subrule, alts) + } + if defaultUsed == len(subrules) { + return fmt.Errorf("%s must specify at least one of %s", context, strings.Join(subrules, ", ")) + } + return nil +} + +// PlugRule holds the rule of what is allowed, wrt installation and +// connection, for a plug of a specific interface for a snap. +type PlugRule struct { + Interface string + + AllowInstallation []*PlugInstallationConstraints + DenyInstallation []*PlugInstallationConstraints + + AllowConnection []*PlugConnectionConstraints + DenyConnection []*PlugConnectionConstraints + + AllowAutoConnection []*PlugConnectionConstraints + DenyAutoConnection []*PlugConnectionConstraints +} + +func castPlugInstallationConstraints(cstrs []constraintsHolder) (res []*PlugInstallationConstraints) { + res = make([]*PlugInstallationConstraints, len(cstrs)) + for i, cstr := range cstrs { + res[i] = cstr.(*PlugInstallationConstraints) + } + return res +} + +func castPlugConnectionConstraints(cstrs []constraintsHolder) (res []*PlugConnectionConstraints) { + res = make([]*PlugConnectionConstraints, len(cstrs)) + for i, cstr := range cstrs { + res[i] = cstr.(*PlugConnectionConstraints) + } + return res +} + +func (r *PlugRule) setConstraints(field string, cstrs []constraintsHolder) { + if len(cstrs) == 0 { + panic(fmt.Sprintf("cannot set PlugRule field %q to empty", field)) + } + switch cstrs[0].(type) { + case *PlugInstallationConstraints: + switch field { + case "allow-installation": + r.AllowInstallation = castPlugInstallationConstraints(cstrs) + return + case "deny-installation": + r.DenyInstallation = castPlugInstallationConstraints(cstrs) + return + } + case *PlugConnectionConstraints: + switch field { + case "allow-connection": + r.AllowConnection = castPlugConnectionConstraints(cstrs) + return + case "deny-connection": + r.DenyConnection = castPlugConnectionConstraints(cstrs) + return + case "allow-auto-connection": + r.AllowAutoConnection = castPlugConnectionConstraints(cstrs) + return + case "deny-auto-connection": + r.DenyAutoConnection = castPlugConnectionConstraints(cstrs) + return + } + } + panic(fmt.Sprintf("cannot set PlugRule field %q with %T elements", field, cstrs[0])) +} + +// PlugInstallationConstraints specifies a set of constraints on an interface plug relevant to the installation of snap. +type PlugInstallationConstraints struct { + PlugSnapTypes []string + + PlugAttributes *AttributeConstraints + + OnClassic *OnClassicConstraint +} + +func (c *PlugInstallationConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { + switch field { + case "plug-attributes": + c.PlugAttributes = cstrs + default: + panic("unknown PlugInstallationConstraints field " + field) + } +} + +func (c *PlugInstallationConstraints) setIDConstraints(field string, cstrs []string) { + switch field { + case "plug-snap-type": + c.PlugSnapTypes = cstrs + default: + panic("unknown PlugInstallationConstraints field " + field) + } +} + +func (c *PlugInstallationConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) { + c.OnClassic = onClassic +} + +func compilePlugInstallationConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { + plugInstCstrs := &PlugInstallationConstraints{} + err := baseCompileConstraints(context, cDef, plugInstCstrs, []string{"plug-attributes"}, []string{"plug-snap-type"}) + if err != nil { + return nil, err + } + return plugInstCstrs, nil +} + +// PlugConnectionConstraints specfies a set of constraints on an +// interface plug for a snap relevant to its connection or +// auto-connection. +type PlugConnectionConstraints struct { + SlotSnapTypes []string + SlotSnapIDs []string + SlotPublisherIDs []string + + PlugAttributes *AttributeConstraints + SlotAttributes *AttributeConstraints + + OnClassic *OnClassicConstraint +} + +func (c *PlugConnectionConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { + switch field { + case "plug-attributes": + c.PlugAttributes = cstrs + case "slot-attributes": + c.SlotAttributes = cstrs + default: + panic("unknown PlugConnectionConstraints field " + field) + } +} + +func (c *PlugConnectionConstraints) setIDConstraints(field string, cstrs []string) { + switch field { + case "slot-snap-type": + c.SlotSnapTypes = cstrs + case "slot-snap-id": + c.SlotSnapIDs = cstrs + case "slot-publisher-id": + c.SlotPublisherIDs = cstrs + default: + panic("unknown PlugConnectionConstraints field " + field) + } +} + +func (c *PlugConnectionConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) { + c.OnClassic = onClassic +} + +var ( + attributeConstraints = []string{"plug-attributes", "slot-attributes"} + plugIDConstraints = []string{"slot-snap-type", "slot-publisher-id", "slot-snap-id"} +) + +func compilePlugConnectionConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { + plugConnCstrs := &PlugConnectionConstraints{} + err := baseCompileConstraints(context, cDef, plugConnCstrs, attributeConstraints, plugIDConstraints) + if err != nil { + return nil, err + } + return plugConnCstrs, nil +} + +var ( + defaultOutcome = map[string]interface{}{ + "allow-installation": "true", + "allow-connection": "true", + "allow-auto-connection": "true", + "deny-installation": "false", + "deny-connection": "false", + "deny-auto-connection": "false", + } + + invertedOutcome = map[string]interface{}{ + "allow-installation": "false", + "allow-connection": "false", + "allow-auto-connection": "false", + "deny-installation": "true", + "deny-connection": "true", + "deny-auto-connection": "true", + } + + ruleSubrules = []string{"allow-installation", "deny-installation", "allow-connection", "deny-connection", "allow-auto-connection", "deny-auto-connection"} +) + +var plugRuleCompilers = map[string]subruleCompiler{ + "allow-installation": compilePlugInstallationConstraints, + "deny-installation": compilePlugInstallationConstraints, + "allow-connection": compilePlugConnectionConstraints, + "deny-connection": compilePlugConnectionConstraints, + "allow-auto-connection": compilePlugConnectionConstraints, + "deny-auto-connection": compilePlugConnectionConstraints, +} + +func compilePlugRule(interfaceName string, rule interface{}) (*PlugRule, error) { + context := fmt.Sprintf("plug rule for interface %q", interfaceName) + plugRule := &PlugRule{ + Interface: interfaceName, + } + err := baseCompileRule(context, rule, plugRule, ruleSubrules, plugRuleCompilers, defaultOutcome, invertedOutcome) + if err != nil { + return nil, err + } + return plugRule, nil +} + +// SlotRule holds the rule of what is allowed, wrt installation and +// connection, for a slot of a specific interface for a snap. +type SlotRule struct { + Interface string + + AllowInstallation []*SlotInstallationConstraints + DenyInstallation []*SlotInstallationConstraints + + AllowConnection []*SlotConnectionConstraints + DenyConnection []*SlotConnectionConstraints + + AllowAutoConnection []*SlotConnectionConstraints + DenyAutoConnection []*SlotConnectionConstraints +} + +func castSlotInstallationConstraints(cstrs []constraintsHolder) (res []*SlotInstallationConstraints) { + res = make([]*SlotInstallationConstraints, len(cstrs)) + for i, cstr := range cstrs { + res[i] = cstr.(*SlotInstallationConstraints) + } + return res +} + +func castSlotConnectionConstraints(cstrs []constraintsHolder) (res []*SlotConnectionConstraints) { + res = make([]*SlotConnectionConstraints, len(cstrs)) + for i, cstr := range cstrs { + res[i] = cstr.(*SlotConnectionConstraints) + } + return res +} + +func (r *SlotRule) setConstraints(field string, cstrs []constraintsHolder) { + if len(cstrs) == 0 { + panic(fmt.Sprintf("cannot set SlotRule field %q to empty", field)) + } + switch cstrs[0].(type) { + case *SlotInstallationConstraints: + switch field { + case "allow-installation": + r.AllowInstallation = castSlotInstallationConstraints(cstrs) + return + case "deny-installation": + r.DenyInstallation = castSlotInstallationConstraints(cstrs) + return + } + case *SlotConnectionConstraints: + switch field { + case "allow-connection": + r.AllowConnection = castSlotConnectionConstraints(cstrs) + return + case "deny-connection": + r.DenyConnection = castSlotConnectionConstraints(cstrs) + return + case "allow-auto-connection": + r.AllowAutoConnection = castSlotConnectionConstraints(cstrs) + return + case "deny-auto-connection": + r.DenyAutoConnection = castSlotConnectionConstraints(cstrs) + return + } + } + panic(fmt.Sprintf("cannot set SlotRule field %q with %T elements", field, cstrs[0])) +} + +// SlotInstallationConstraints specifies a set of constraints on an +// interface slot relevant to the installation of snap. +type SlotInstallationConstraints struct { + SlotSnapTypes []string + + SlotAttributes *AttributeConstraints + + OnClassic *OnClassicConstraint +} + +func (c *SlotInstallationConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { + switch field { + case "slot-attributes": + c.SlotAttributes = cstrs + default: + panic("unknown SlotInstallationConstraints field " + field) + } +} + +func (c *SlotInstallationConstraints) setIDConstraints(field string, cstrs []string) { + switch field { + case "slot-snap-type": + c.SlotSnapTypes = cstrs + default: + panic("unknown SlotInstallationConstraints field " + field) + } +} + +func (c *SlotInstallationConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) { + c.OnClassic = onClassic +} + +func compileSlotInstallationConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { + slotInstCstrs := &SlotInstallationConstraints{} + err := baseCompileConstraints(context, cDef, slotInstCstrs, []string{"slot-attributes"}, []string{"slot-snap-type"}) + if err != nil { + return nil, err + } + return slotInstCstrs, nil +} + +// SlotConnectionConstraints specfies a set of constraints on an +// interface slot for a snap relevant to its connection or +// auto-connection. +type SlotConnectionConstraints struct { + PlugSnapTypes []string + PlugSnapIDs []string + PlugPublisherIDs []string + + SlotAttributes *AttributeConstraints + PlugAttributes *AttributeConstraints + + OnClassic *OnClassicConstraint +} + +func (c *SlotConnectionConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { + switch field { + case "plug-attributes": + c.PlugAttributes = cstrs + case "slot-attributes": + c.SlotAttributes = cstrs + default: + panic("unknown SlotConnectionConstraints field " + field) + } +} + +func (c *SlotConnectionConstraints) setIDConstraints(field string, cstrs []string) { + switch field { + case "plug-snap-type": + c.PlugSnapTypes = cstrs + case "plug-snap-id": + c.PlugSnapIDs = cstrs + case "plug-publisher-id": + c.PlugPublisherIDs = cstrs + default: + panic("unknown SlotConnectionConstraints field " + field) + } +} + +var ( + slotIDConstraints = []string{"plug-snap-type", "plug-publisher-id", "plug-snap-id"} +) + +func (c *SlotConnectionConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) { + c.OnClassic = onClassic +} + +func compileSlotConnectionConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { + slotConnCstrs := &SlotConnectionConstraints{} + err := baseCompileConstraints(context, cDef, slotConnCstrs, attributeConstraints, slotIDConstraints) + if err != nil { + return nil, err + } + return slotConnCstrs, nil +} + +var slotRuleCompilers = map[string]subruleCompiler{ + "allow-installation": compileSlotInstallationConstraints, + "deny-installation": compileSlotInstallationConstraints, + "allow-connection": compileSlotConnectionConstraints, + "deny-connection": compileSlotConnectionConstraints, + "allow-auto-connection": compileSlotConnectionConstraints, + "deny-auto-connection": compileSlotConnectionConstraints, +} + +func compileSlotRule(interfaceName string, rule interface{}) (*SlotRule, error) { + context := fmt.Sprintf("slot rule for interface %q", interfaceName) + slotRule := &SlotRule{ + Interface: interfaceName, + } + err := baseCompileRule(context, rule, slotRule, ruleSubrules, slotRuleCompilers, defaultOutcome, invertedOutcome) + if err != nil { + return nil, err + } + return slotRule, nil +} diff --git a/asserts/ifacedecls_test.go b/asserts/ifacedecls_test.go new file mode 100644 index 00000000..c0282087 --- /dev/null +++ b/asserts/ifacedecls_test.go @@ -0,0 +1,1173 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + "gopkg.in/yaml.v2" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/snap" +) + +var ( + _ = Suite(&attrConstraintsSuite{}) + _ = Suite(&plugSlotRulesSuite{}) +) + +type attrConstraintsSuite struct{} + +func attrs(yml string) map[string]interface{} { + var attrs map[string]interface{} + err := yaml.Unmarshal([]byte(yml), &attrs) + if err != nil { + panic(err) + } + snapYaml, err := yaml.Marshal(map[string]interface{}{ + "name": "sample", + "plugs": map[string]interface{}{ + "plug": attrs, + }, + }) + if err != nil { + panic(err) + } + info, err := snap.InfoFromSnapYaml(snapYaml) + if err != nil { + panic(err) + } + return info.Plugs["plug"].Attrs +} + +func (s *attrConstraintsSuite) TestSimple(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: FOO + bar: BAR`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "bar": "BAR", + "baz": "BAZ", + }) + c.Check(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "bar": "BAZ", + "baz": "BAZ", + }) + c.Check(err, ErrorMatches, `attribute "bar" value "BAZ" does not match \^\(BAR\)\$`) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "baz": "BAZ", + }) + c.Check(err, ErrorMatches, `attribute "bar" has constraints but is unset`) +} + +func (s *attrConstraintsSuite) TestSimpleAnchorsVsRegexpAlt(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + bar: BAR|BAZ`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "bar": "BAR", + }) + c.Check(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "bar": "BARR", + }) + c.Check(err, ErrorMatches, `attribute "bar" value "BARR" does not match \^\(BAR|BAZ\)\$`) + + err = cstrs.Check(map[string]interface{}{ + "bar": "BBAZ", + }) + c.Check(err, ErrorMatches, `attribute "bar" value "BAZZ" does not match \^\(BAR|BAZ\)\$`) + + err = cstrs.Check(map[string]interface{}{ + "bar": "BABAZ", + }) + c.Check(err, ErrorMatches, `attribute "bar" value "BABAZ" does not match \^\(BAR|BAZ\)\$`) + + err = cstrs.Check(map[string]interface{}{ + "bar": "BARAZ", + }) + c.Check(err, ErrorMatches, `attribute "bar" value "BARAZ" does not match \^\(BAR|BAZ\)\$`) +} + +func (s *attrConstraintsSuite) TestNested(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: FOO + bar: + bar1: BAR1 + bar2: BAR2`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR2 + bar3: BAR3 +baz: BAZ +`)) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: BAZ +baz: BAZ +`)) + c.Check(err, ErrorMatches, `attribute "bar" must be a map`) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR22 + bar3: BAR3 +baz: BAZ +`)) + c.Check(err, ErrorMatches, `attribute "bar\.bar2" value "BAR22" does not match \^\(BAR2\)\$`) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: + bar22: true + bar3: BAR3 +baz: BAZ +`)) + c.Check(err, ErrorMatches, `attribute "bar\.bar2" must be a scalar or list`) +} + +func (s *attrConstraintsSuite) TestAlternative(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + - + foo: FOO + bar: BAR + - + foo: FOO + bar: BAZ`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].([]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "bar": "BAR", + "baz": "BAZ", + }) + c.Check(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "bar": "BAZ", + "baz": "BAZ", + }) + c.Check(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "bar": "BARR", + "baz": "BAR", + }) + c.Check(err, ErrorMatches, `no alternative matches: attribute "bar" value "BARR" does not match \^\(BAR\)\$`) +} + +func (s *attrConstraintsSuite) TestNestedAlternative(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: FOO + bar: + bar1: BAR1 + bar2: + - BAR2 + - BAR22`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR2 +`)) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR22 +`)) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR3 +`)) + c.Check(err, ErrorMatches, `no alternative for attribute "bar\.bar2" matches: attribute "bar\.bar2" value "BAR3" does not match \^\(BAR2\)\$`) +} + +func (s *attrConstraintsSuite) TestOtherScalars(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: 1 + bar: true`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(attrs(` +foo: 1 +bar: true +`)) + c.Check(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": int64(1), + "bar": true, + }) + c.Check(err, IsNil) +} + +func (s *attrConstraintsSuite) TestCompileErrors(c *C) { + _, err := asserts.CompileAttributeConstraints(map[string]interface{}{ + "foo": "[", + }) + c.Check(err, ErrorMatches, `cannot compile "foo" constraint "\[": error parsing regexp:.*`) + + _, err = asserts.CompileAttributeConstraints(map[string]interface{}{ + "foo": []interface{}{"foo", "["}, + }) + c.Check(err, ErrorMatches, `cannot compile "foo/alt#2/" constraint "\[": error parsing regexp:.*`) + + _, err = asserts.CompileAttributeConstraints(map[string]interface{}{ + "foo": []interface{}{"foo", []interface{}{"bar", "baz"}}, + }) + c.Check(err, ErrorMatches, `cannot nest alternative constraints directly at "foo/alt#2/"`) + + _, err = asserts.CompileAttributeConstraints("FOO") + c.Check(err, ErrorMatches, `first level of non alternative constraints must be a set of key-value contraints`) + + _, err = asserts.CompileAttributeConstraints([]interface{}{"FOO"}) + c.Check(err, ErrorMatches, `first level of non alternative constraints must be a set of key-value contraints`) +} + +func (s *attrConstraintsSuite) TestMatchingListsSimple(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: /foo/.*`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(attrs(` +foo: ["/foo/x", "/foo/y"] +`)) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: ["/foo/x", "/foo"] +`)) + c.Check(err, ErrorMatches, `attribute "foo\.1" value "/foo" does not match \^\(/foo/\.\*\)\$`) +} + +func (s *attrConstraintsSuite) TestMatchingListsMap(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: + p: /foo/.*`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(attrs(` +foo: [{p: "/foo/x"}, {p: "/foo/y"}] +`)) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: [{p: "zzz"}, {p: "/foo/y"}] +`)) + c.Check(err, ErrorMatches, `attribute "foo\.0\.p" value "zzz" does not match \^\(/foo/\.\*\)\$`) +} + +func (s *attrConstraintsSuite) TestAlwaysMatchAttributeConstraints(c *C) { + c.Check(asserts.AlwaysMatchAttributes.Check(nil), IsNil) +} + +func (s *attrConstraintsSuite) TestNeverMatchAttributeConstraints(c *C) { + c.Check(asserts.NeverMatchAttributes.Check(nil), NotNil) +} + +type plugSlotRulesSuite struct{} + +func checkAttrs(c *C, attrs *asserts.AttributeConstraints, witness, expected string) { + c.Check(attrs.Check(map[string]interface{}{ + witness: "XYZ", + }), ErrorMatches, fmt.Sprintf(`attribute "%s".*does not match.*`, witness)) + c.Check(attrs.Check(map[string]interface{}{ + witness: expected, + }), IsNil) +} + +func checkBoolPlugConnConstraints(c *C, cstrs []*asserts.PlugConnectionConstraints, always bool) { + expected := asserts.NeverMatchAttributes + if always { + expected = asserts.AlwaysMatchAttributes + } + c.Assert(cstrs, HasLen, 1) + cstrs1 := cstrs[0] + c.Check(cstrs1.PlugAttributes, Equals, expected) + c.Check(cstrs1.SlotAttributes, Equals, expected) + c.Check(cstrs1.SlotSnapIDs, HasLen, 0) + c.Check(cstrs1.SlotPublisherIDs, HasLen, 0) + c.Check(cstrs1.SlotSnapTypes, HasLen, 0) +} + +func checkBoolSlotConnConstraints(c *C, cstrs []*asserts.SlotConnectionConstraints, always bool) { + expected := asserts.NeverMatchAttributes + if always { + expected = asserts.AlwaysMatchAttributes + } + c.Assert(cstrs, HasLen, 1) + cstrs1 := cstrs[0] + c.Check(cstrs1.PlugAttributes, Equals, expected) + c.Check(cstrs1.SlotAttributes, Equals, expected) + c.Check(cstrs1.PlugSnapIDs, HasLen, 0) + c.Check(cstrs1.PlugPublisherIDs, HasLen, 0) + c.Check(cstrs1.PlugSnapTypes, HasLen, 0) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleAllAllowDenyStanzas(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: + plug-attributes: + a1: A1 + deny-installation: + plug-attributes: + a2: A2 + allow-connection: + plug-attributes: + pa3: PA3 + slot-attributes: + sa3: SA3 + deny-connection: + plug-attributes: + pa4: PA4 + slot-attributes: + sa4: SA4 + allow-auto-connection: + plug-attributes: + pa5: PA5 + slot-attributes: + sa5: SA5 + deny-auto-connection: + plug-attributes: + pa6: PA6 + slot-attributes: + sa6: SA6`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + checkAttrs(c, rule.AllowInstallation[0].PlugAttributes, "a1", "A1") + c.Assert(rule.DenyInstallation, HasLen, 1) + checkAttrs(c, rule.DenyInstallation[0].PlugAttributes, "a2", "A2") + // connection subrules + c.Assert(rule.AllowConnection, HasLen, 1) + checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3") + checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3") + c.Assert(rule.DenyConnection, HasLen, 1) + checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4") + checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4") + // auto-connection subrules + c.Assert(rule.AllowAutoConnection, HasLen, 1) + checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5") + checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5") + c.Assert(rule.DenyAutoConnection, HasLen, 1) + checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6") + checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6") +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleAllAllowDenyOrStanzas(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: + - + plug-attributes: + a1: A1 + - + plug-attributes: + a1: A1alt + deny-installation: + - + plug-attributes: + a2: A2 + - + plug-attributes: + a2: A2alt + allow-connection: + - + plug-attributes: + pa3: PA3 + slot-attributes: + sa3: SA3 + - + plug-attributes: + pa3: PA3alt + deny-connection: + - + plug-attributes: + pa4: PA4 + slot-attributes: + sa4: SA4 + - + plug-attributes: + pa4: PA4alt + allow-auto-connection: + - + plug-attributes: + pa5: PA5 + slot-attributes: + sa5: SA5 + - + plug-attributes: + pa5: PA5alt + deny-auto-connection: + - + plug-attributes: + pa6: PA6 + slot-attributes: + sa6: SA6 + - + plug-attributes: + pa6: PA6alt`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 2) + checkAttrs(c, rule.AllowInstallation[0].PlugAttributes, "a1", "A1") + checkAttrs(c, rule.AllowInstallation[1].PlugAttributes, "a1", "A1alt") + c.Assert(rule.DenyInstallation, HasLen, 2) + checkAttrs(c, rule.DenyInstallation[0].PlugAttributes, "a2", "A2") + checkAttrs(c, rule.DenyInstallation[1].PlugAttributes, "a2", "A2alt") + // connection subrules + c.Assert(rule.AllowConnection, HasLen, 2) + checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3") + checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3") + checkAttrs(c, rule.AllowConnection[1].PlugAttributes, "pa3", "PA3alt") + c.Assert(rule.DenyConnection, HasLen, 2) + checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4") + checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4") + checkAttrs(c, rule.DenyConnection[1].PlugAttributes, "pa4", "PA4alt") + // auto-connection subrules + c.Assert(rule.AllowAutoConnection, HasLen, 2) + checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5") + checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5") + checkAttrs(c, rule.AllowAutoConnection[1].PlugAttributes, "pa5", "PA5alt") + c.Assert(rule.DenyAutoConnection, HasLen, 2) + checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6") + checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6") + checkAttrs(c, rule.DenyAutoConnection[1].PlugAttributes, "pa6", "PA6alt") +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleShortcutTrue(c *C) { + rule, err := asserts.CompilePlugRule("iface", "true") + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) + // connection subrules + checkBoolPlugConnConstraints(c, rule.AllowConnection, true) + checkBoolPlugConnConstraints(c, rule.DenyConnection, false) + // auto-connection subrules + checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, true) + checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, false) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleShortcutFalse(c *C) { + rule, err := asserts.CompilePlugRule("iface", "false") + c.Assert(err, IsNil) + + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + // connection subrules + checkBoolPlugConnConstraints(c, rule.AllowConnection, false) + checkBoolPlugConnConstraints(c, rule.DenyConnection, true) + // auto-connection subrules + checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, false) + checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, true) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleDefaults(c *C) { + rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ + "deny-auto-connection": "true", + }) + c.Assert(err, IsNil) + + // everything follows the defaults... + + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) + // connection subrules + checkBoolPlugConnConstraints(c, rule.AllowConnection, true) + checkBoolPlugConnConstraints(c, rule.DenyConnection, false) + // auto-connection subrules + checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, true) + // ... but deny-auto-connection is on + checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, true) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleInstalationConstraintsIDConstraints(c *C) { + rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ + "allow-installation": map[string]interface{}{ + "plug-snap-type": []interface{}{"core", "kernel", "gadget", "app"}, + }, + }) + c.Assert(err, IsNil) + + c.Assert(rule.AllowInstallation, HasLen, 1) + cstrs := rule.AllowInstallation[0] + c.Check(cstrs.PlugSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"}) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleInstallationConstraintsOnClassic(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, IsNil) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: false`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: true`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: + - ubuntu + - debian`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsIDConstraints(c *C) { + rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ + "allow-connection": map[string]interface{}{ + "slot-snap-type": []interface{}{"core", "kernel", "gadget", "app"}, + "slot-snap-id": []interface{}{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}, + "slot-publisher-id": []interface{}{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"}, + }, + }) + c.Assert(err, IsNil) + + c.Assert(rule.AllowConnection, HasLen, 1) + cstrs := rule.AllowConnection[0] + c.Check(cstrs.SlotSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"}) + c.Check(cstrs.SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + c.Check(cstrs.SlotPublisherIDs, DeepEquals, []string{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"}) + +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsOnClassic(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-connection: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, IsNil) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: false`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: true`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: + - ubuntu + - debian`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsAttributesDefault(c *C) { + rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ + "allow-connection": map[string]interface{}{ + "slot-snap-id": []interface{}{"snapidsnapidsnapidsnapidsnapid01"}, + }, + }) + c.Assert(err, IsNil) + + // attributes default to always matching here + cstrs := rule.AllowConnection[0] + c.Check(cstrs.PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Check(cstrs.SlotAttributes, Equals, asserts.AlwaysMatchAttributes) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleErrors(c *C) { + tests := []struct { + stanza string + err string + }{ + {`iface: foo`, `plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + - allow`, `plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-installation: foo`, `allow-installation in plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + deny-installation: foo`, `deny-installation in plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-connection: foo`, `allow-connection in plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-connection: + - foo`, `alternative 1 of allow-connection in plug rule for interface "iface" must be a map`}, + {`iface: + allow-connection: + - true`, `alternative 1 of allow-connection in plug rule for interface "iface" must be a map`}, + {`iface: + allow-installation: + plug-attributes: + a1: [`, `cannot compile plug-attributes in allow-installation in plug rule for interface "iface": cannot compile "a1" constraint .*`}, + {`iface: + allow-connection: + slot-attributes: + a2: [`, `cannot compile slot-attributes in allow-connection in plug rule for interface "iface": cannot compile "a2" constraint .*`}, + {`iface: + allow-connection: + slot-snap-id: + - + foo: 1`, `slot-snap-id in allow-connection in plug rule for interface "iface" must be a list of strings`}, + {`iface: + allow-connection: + slot-snap-id: + - foo`, `slot-snap-id in allow-connection in plug rule for interface "iface" contains an invalid element: "foo"`}, + {`iface: + allow-connection: + slot-snap-type: + - foo`, `slot-snap-type in allow-connection in plug rule for interface "iface" contains an invalid element: "foo"`}, + {`iface: + allow-connection: + slot-snap-type: + - xapp`, `slot-snap-type in allow-connection in plug rule for interface "iface" contains an invalid element: "xapp"`}, + {`iface: + allow-connection: + slot-snap-ids: + - foo`, `allow-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + {`iface: + deny-connection: + slot-snap-ids: + - foo`, `deny-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + {`iface: + allow-auto-connection: + slot-snap-ids: + - foo`, `allow-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + {`iface: + deny-auto-connection: + slot-snap-ids: + - foo`, `deny-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + {`iface: + allow-connect: true`, `plug rule for interface "iface" must specify at least one of allow-installation, deny-installation, allow-connection, deny-connection, allow-auto-connection, deny-auto-connection`}, + } + + for _, t := range tests { + m, err := asserts.ParseHeaders([]byte(t.stanza)) + c.Assert(err, IsNil, Commentf(t.stanza)) + + _, err = asserts.CompilePlugRule("iface", m["iface"]) + c.Check(err, ErrorMatches, t.err, Commentf(t.stanza)) + } +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleAllAllowDenyStanzas(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: + slot-attributes: + a1: A1 + deny-installation: + slot-attributes: + a2: A2 + allow-connection: + plug-attributes: + pa3: PA3 + slot-attributes: + sa3: SA3 + deny-connection: + plug-attributes: + pa4: PA4 + slot-attributes: + sa4: SA4 + allow-auto-connection: + plug-attributes: + pa5: PA5 + slot-attributes: + sa5: SA5 + deny-auto-connection: + plug-attributes: + pa6: PA6 + slot-attributes: + sa6: SA6`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + checkAttrs(c, rule.AllowInstallation[0].SlotAttributes, "a1", "A1") + c.Assert(rule.DenyInstallation, HasLen, 1) + checkAttrs(c, rule.DenyInstallation[0].SlotAttributes, "a2", "A2") + // connection subrules + c.Assert(rule.AllowConnection, HasLen, 1) + checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3") + checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3") + c.Assert(rule.DenyConnection, HasLen, 1) + checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4") + checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4") + // auto-connection subrules + c.Assert(rule.AllowAutoConnection, HasLen, 1) + checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5") + checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5") + c.Assert(rule.DenyAutoConnection, HasLen, 1) + checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6") + checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6") +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleAllAllowDenyOrStanzas(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: + - + slot-attributes: + a1: A1 + - + slot-attributes: + a1: A1alt + deny-installation: + - + slot-attributes: + a2: A2 + - + slot-attributes: + a2: A2alt + allow-connection: + - + plug-attributes: + pa3: PA3 + slot-attributes: + sa3: SA3 + - + slot-attributes: + sa3: SA3alt + deny-connection: + - + plug-attributes: + pa4: PA4 + slot-attributes: + sa4: SA4 + - + slot-attributes: + sa4: SA4alt + allow-auto-connection: + - + plug-attributes: + pa5: PA5 + slot-attributes: + sa5: SA5 + - + slot-attributes: + sa5: SA5alt + deny-auto-connection: + - + plug-attributes: + pa6: PA6 + slot-attributes: + sa6: SA6 + - + slot-attributes: + sa6: SA6alt`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 2) + checkAttrs(c, rule.AllowInstallation[0].SlotAttributes, "a1", "A1") + checkAttrs(c, rule.AllowInstallation[1].SlotAttributes, "a1", "A1alt") + c.Assert(rule.DenyInstallation, HasLen, 2) + checkAttrs(c, rule.DenyInstallation[0].SlotAttributes, "a2", "A2") + checkAttrs(c, rule.DenyInstallation[1].SlotAttributes, "a2", "A2alt") + // connection subrules + c.Assert(rule.AllowConnection, HasLen, 2) + checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3") + checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3") + checkAttrs(c, rule.AllowConnection[1].SlotAttributes, "sa3", "SA3alt") + c.Assert(rule.DenyConnection, HasLen, 2) + checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4") + checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4") + checkAttrs(c, rule.DenyConnection[1].SlotAttributes, "sa4", "SA4alt") + // auto-connection subrules + c.Assert(rule.AllowAutoConnection, HasLen, 2) + checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5") + checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5") + checkAttrs(c, rule.AllowAutoConnection[1].SlotAttributes, "sa5", "SA5alt") + c.Assert(rule.DenyAutoConnection, HasLen, 2) + checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6") + checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6") + checkAttrs(c, rule.DenyAutoConnection[1].SlotAttributes, "sa6", "SA6alt") +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleShortcutTrue(c *C) { + rule, err := asserts.CompileSlotRule("iface", "true") + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) + // connection subrules + checkBoolSlotConnConstraints(c, rule.AllowConnection, true) + checkBoolSlotConnConstraints(c, rule.DenyConnection, false) + // auto-connection subrules + checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, true) + checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, false) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleShortcutFalse(c *C) { + rule, err := asserts.CompileSlotRule("iface", "false") + c.Assert(err, IsNil) + + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes) + // connection subrules + checkBoolSlotConnConstraints(c, rule.AllowConnection, false) + checkBoolSlotConnConstraints(c, rule.DenyConnection, true) + // auto-connection subrules + checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, false) + checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, true) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleDefaults(c *C) { + rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{ + "deny-auto-connection": "true", + }) + c.Assert(err, IsNil) + + // everything follows the defaults... + + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) + // connection subrules + checkBoolSlotConnConstraints(c, rule.AllowConnection, true) + checkBoolSlotConnConstraints(c, rule.DenyConnection, false) + // auto-connection subrules + checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, true) + // ... but deny-auto-connection is on + checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, true) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleInstallationConstraintsIDConstraints(c *C) { + rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{ + "allow-installation": map[string]interface{}{ + "slot-snap-type": []interface{}{"core", "kernel", "gadget", "app"}, + }, + }) + c.Assert(err, IsNil) + + c.Assert(rule.AllowInstallation, HasLen, 1) + cstrs := rule.AllowInstallation[0] + c.Check(cstrs.SlotSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"}) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleInstallationConstraintsOnClassic(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, IsNil) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: false`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: true`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: + - ubuntu + - debian`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsIDConstraints(c *C) { + rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{ + "allow-connection": map[string]interface{}{ + "plug-snap-type": []interface{}{"core", "kernel", "gadget", "app"}, + "plug-snap-id": []interface{}{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}, + "plug-publisher-id": []interface{}{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"}, + }, + }) + c.Assert(err, IsNil) + + c.Assert(rule.AllowConnection, HasLen, 1) + cstrs := rule.AllowConnection[0] + c.Check(cstrs.PlugSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"}) + c.Check(cstrs.PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + c.Check(cstrs.PlugPublisherIDs, DeepEquals, []string{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"}) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsOnClassic(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-connection: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, IsNil) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: false`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: true`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: + - ubuntu + - debian`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleErrors(c *C) { + tests := []struct { + stanza string + err string + }{ + {`iface: foo`, `slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + - allow`, `slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-installation: foo`, `allow-installation in slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + deny-installation: foo`, `deny-installation in slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-connection: foo`, `allow-connection in slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-connection: + - foo`, `alternative 1 of allow-connection in slot rule for interface "iface" must be a map`}, + {`iface: + allow-connection: + - true`, `alternative 1 of allow-connection in slot rule for interface "iface" must be a map`}, + {`iface: + allow-installation: + slot-attributes: + a1: [`, `cannot compile slot-attributes in allow-installation in slot rule for interface "iface": cannot compile "a1" constraint .*`}, + {`iface: + allow-connection: + plug-attributes: + a2: [`, `cannot compile plug-attributes in allow-connection in slot rule for interface "iface": cannot compile "a2" constraint .*`}, + {`iface: + allow-connection: + plug-snap-id: + - + foo: 1`, `plug-snap-id in allow-connection in slot rule for interface "iface" must be a list of strings`}, + {`iface: + allow-connection: + plug-snap-id: + - foo`, `plug-snap-id in allow-connection in slot rule for interface "iface" contains an invalid element: "foo"`}, + {`iface: + allow-connection: + plug-snap-type: + - foo`, `plug-snap-type in allow-connection in slot rule for interface "iface" contains an invalid element: "foo"`}, + {`iface: + allow-connection: + plug-snap-type: + - xapp`, `plug-snap-type in allow-connection in slot rule for interface "iface" contains an invalid element: "xapp"`}, + {`iface: + allow-connection: + on-classic: + x: 1`, `on-classic in allow-connection in slot rule for interface \"iface\" must be 'true', 'false' or a list of operating system IDs`}, + {`iface: + allow-connection: + on-classic: + - zoom!`, `on-classic in allow-connection in slot rule for interface \"iface\" contains an invalid element: \"zoom!\"`}, + {`iface: + allow-connection: + plug-snap-ids: + - foo`, `allow-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + {`iface: + deny-connection: + plug-snap-ids: + - foo`, `deny-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + {`iface: + allow-auto-connection: + plug-snap-ids: + - foo`, `allow-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + {`iface: + deny-auto-connection: + plug-snap-ids: + - foo`, `deny-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + {`iface: + allow-connect: true`, `slot rule for interface "iface" must specify at least one of allow-installation, deny-installation, allow-connection, deny-connection, allow-auto-connection, deny-auto-connection`}, + } + + for _, t := range tests { + m, err := asserts.ParseHeaders([]byte(t.stanza)) + c.Assert(err, IsNil, Commentf(t.stanza)) + _, err = asserts.CompileSlotRule("iface", m["iface"]) + c.Check(err, ErrorMatches, t.err, Commentf(t.stanza)) + } +} diff --git a/asserts/membackstore.go b/asserts/membackstore.go new file mode 100644 index 00000000..fe744fef --- /dev/null +++ b/asserts/membackstore.go @@ -0,0 +1,184 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts + +import ( + "sync" +) + +type memoryBackstore struct { + top memBSBranch + mu sync.RWMutex +} + +type memBSNode interface { + put(assertType *AssertionType, key []string, assert Assertion) error + get(key []string, maxFormat int) (Assertion, error) + search(hint []string, found func(Assertion), maxFormat int) +} + +type memBSBranch map[string]memBSNode + +type memBSLeaf map[string]map[int]Assertion + +func (br memBSBranch) put(assertType *AssertionType, key []string, assert Assertion) error { + key0 := key[0] + down := br[key0] + if down == nil { + if len(key) > 2 { + down = make(memBSBranch) + } else { + down = make(memBSLeaf) + } + br[key0] = down + } + return down.put(assertType, key[1:], assert) +} + +func (leaf memBSLeaf) cur(key0 string, maxFormat int) (a Assertion) { + for formatnum, a1 := range leaf[key0] { + if formatnum <= maxFormat { + if a == nil || a1.Revision() > a.Revision() { + a = a1 + } + } + } + return a +} + +func (leaf memBSLeaf) put(assertType *AssertionType, key []string, assert Assertion) error { + key0 := key[0] + cur := leaf.cur(key0, assertType.MaxSupportedFormat()) + if cur != nil { + rev := assert.Revision() + curRev := cur.Revision() + if curRev >= rev { + return &RevisionError{Current: curRev, Used: rev} + } + } + if _, ok := leaf[key0]; !ok { + leaf[key0] = make(map[int]Assertion) + } + leaf[key0][assert.Format()] = assert + return nil +} + +func (br memBSBranch) get(key []string, maxFormat int) (Assertion, error) { + key0 := key[0] + down := br[key0] + if down == nil { + return nil, ErrNotFound + } + return down.get(key[1:], maxFormat) +} + +func (leaf memBSLeaf) get(key []string, maxFormat int) (Assertion, error) { + key0 := key[0] + cur := leaf.cur(key0, maxFormat) + if cur == nil { + return nil, ErrNotFound + } + return cur, nil +} + +func (br memBSBranch) search(hint []string, found func(Assertion), maxFormat int) { + hint0 := hint[0] + if hint0 == "" { + for _, down := range br { + down.search(hint[1:], found, maxFormat) + } + return + } + down := br[hint0] + if down != nil { + down.search(hint[1:], found, maxFormat) + } + return +} + +func (leaf memBSLeaf) search(hint []string, found func(Assertion), maxFormat int) { + hint0 := hint[0] + if hint0 == "" { + for key := range leaf { + cand := leaf.cur(key, maxFormat) + if cand != nil { + found(cand) + } + } + return + } + + cur := leaf.cur(hint0, maxFormat) + if cur != nil { + found(cur) + } +} + +// NewMemoryBackstore creates a memory backed assertions backstore. +func NewMemoryBackstore() Backstore { + return &memoryBackstore{ + top: make(memBSBranch), + } +} + +func (mbs *memoryBackstore) Put(assertType *AssertionType, assert Assertion) error { + mbs.mu.Lock() + defer mbs.mu.Unlock() + + internalKey := make([]string, 1+len(assertType.PrimaryKey)) + internalKey[0] = assertType.Name + for i, name := range assertType.PrimaryKey { + internalKey[1+i] = assert.HeaderString(name) + } + + err := mbs.top.put(assertType, internalKey, assert) + return err +} + +func (mbs *memoryBackstore) Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error) { + mbs.mu.RLock() + defer mbs.mu.RUnlock() + + internalKey := make([]string, 1+len(assertType.PrimaryKey)) + internalKey[0] = assertType.Name + copy(internalKey[1:], key) + + return mbs.top.get(internalKey, maxFormat) +} + +func (mbs *memoryBackstore) Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error { + mbs.mu.RLock() + defer mbs.mu.RUnlock() + + hint := make([]string, 1+len(assertType.PrimaryKey)) + hint[0] = assertType.Name + for i, name := range assertType.PrimaryKey { + hint[1+i] = headers[name] + } + + candCb := func(a Assertion) { + if searchMatch(a, headers) { + foundCb(a) + } + } + + mbs.top.search(hint, candCb, maxFormat) + return nil +} diff --git a/asserts/membackstore_test.go b/asserts/membackstore_test.go new file mode 100644 index 00000000..f755ff81 --- /dev/null +++ b/asserts/membackstore_test.go @@ -0,0 +1,346 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type memBackstoreSuite struct { + bs asserts.Backstore + a asserts.Assertion +} + +var _ = Suite(&memBackstoreSuite{}) + +func (mbss *memBackstoreSuite) SetUpTest(c *C) { + mbss.bs = asserts.NewMemoryBackstore() + + encoded := "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + mbss.a = a +} + +func (mbss *memBackstoreSuite) TestPutAndGet(c *C) { + err := mbss.bs.Put(asserts.TestOnlyType, mbss.a) + c.Assert(err, IsNil) + + a, err := mbss.bs.Get(asserts.TestOnlyType, []string{"foo"}, 0) + c.Assert(err, IsNil) + + c.Check(a, Equals, mbss.a) +} + +func (mbss *memBackstoreSuite) TestGetNotFound(c *C) { + a, err := mbss.bs.Get(asserts.TestOnlyType, []string{"foo"}, 0) + c.Assert(err, Equals, asserts.ErrNotFound) + c.Check(a, IsNil) + + err = mbss.bs.Put(asserts.TestOnlyType, mbss.a) + c.Assert(err, IsNil) + + a, err = mbss.bs.Get(asserts.TestOnlyType, []string{"bar"}, 0) + c.Assert(err, Equals, asserts.ErrNotFound) + c.Check(a, IsNil) +} + +func (mbss *memBackstoreSuite) TestPutNotNewer(c *C) { + err := mbss.bs.Put(asserts.TestOnlyType, mbss.a) + c.Assert(err, IsNil) + + err = mbss.bs.Put(asserts.TestOnlyType, mbss.a) + c.Check(err, ErrorMatches, "revision 0 is already the current revision") +} + +func (mbss *memBackstoreSuite) TestSearch(c *C) { + encoded := "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: one\n" + + "other: other1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a1, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + encoded = "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: two\n" + + "other: other2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a2, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + err = mbss.bs.Put(asserts.TestOnlyType, a1) + c.Assert(err, IsNil) + err = mbss.bs.Put(asserts.TestOnlyType, a2) + c.Assert(err, IsNil) + + found := map[string]asserts.Assertion{} + cb := func(a asserts.Assertion) { + found[a.HeaderString("primary-key")] = a + } + err = mbss.bs.Search(asserts.TestOnlyType, nil, cb, 0) + c.Assert(err, IsNil) + c.Check(found, HasLen, 2) + + found = map[string]asserts.Assertion{} + err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "one", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]asserts.Assertion{ + "one": a1, + }) + + found = map[string]asserts.Assertion{} + err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{ + "other": "other2", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]asserts.Assertion{ + "two": a2, + }) + + found = map[string]asserts.Assertion{} + err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "two", + "other": "other1", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, HasLen, 0) +} + +func (mbss *memBackstoreSuite) TestSearch2Levels(c *C) { + encoded := "type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: a\n" + + "pk2: x\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + aAX, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + encoded = "type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: b\n" + + "pk2: x\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + aBX, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + err = mbss.bs.Put(asserts.TestOnly2Type, aAX) + c.Assert(err, IsNil) + err = mbss.bs.Put(asserts.TestOnly2Type, aBX) + c.Assert(err, IsNil) + + found := map[string]asserts.Assertion{} + cb := func(a asserts.Assertion) { + found[a.HeaderString("pk1")+":"+a.HeaderString("pk2")] = a + } + err = mbss.bs.Search(asserts.TestOnly2Type, map[string]string{ + "pk2": "x", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, HasLen, 2) +} + +func (mbss *memBackstoreSuite) TestPutOldRevision(c *C) { + bs := asserts.NewMemoryBackstore() + + // Create two revisions of assertion. + a0, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + a1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + // Put newer revision, follwed by old revision. + err = bs.Put(asserts.TestOnlyType, a1) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a0) + + c.Check(err, ErrorMatches, `revision 0 is older than current revision 1`) + c.Check(err, DeepEquals, &asserts.RevisionError{Current: 1, Used: 0}) +} + +func (mbss *memBackstoreSuite) TestGetFormat(c *C) { + bs := asserts.NewMemoryBackstore() + + af0, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "format: 1\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af2, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: zoo\n" + + "format: 2\n" + + "revision: 22\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + err = bs.Put(asserts.TestOnlyType, af0) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, af1) + c.Assert(err, IsNil) + + a, err := bs.Get(asserts.TestOnlyType, []string{"foo"}, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + + a, err = bs.Get(asserts.TestOnlyType, []string{"foo"}, 0) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 0) + c.Assert(err, Equals, asserts.ErrNotFound) + + err = bs.Put(asserts.TestOnlyType, af2) + c.Assert(err, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 1) + c.Assert(err, Equals, asserts.ErrNotFound) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 2) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 22) +} + +func (mbss *memBackstoreSuite) TestSearchFormat(c *C) { + bs := asserts.NewMemoryBackstore() + + af0, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: bar\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af1, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: bar\n" + + "format: 1\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + af2, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: baz\n" + + "format: 2\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + err = bs.Put(asserts.TestOnly2Type, af0) + c.Assert(err, IsNil) + + queries := []map[string]string{ + {"pk1": "foo", "pk2": "bar"}, + {"pk1": "foo"}, + {"pk2": "bar"}, + } + + for _, q := range queries { + var a asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + a = a1 + } + err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + } + + err = bs.Put(asserts.TestOnly2Type, af1) + c.Assert(err, IsNil) + + for _, q := range queries { + var a asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + a = a1 + } + err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + + err = bs.Search(asserts.TestOnly2Type, q, foundCb, 0) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + } + + err = bs.Put(asserts.TestOnly2Type, af2) + c.Assert(err, IsNil) + + var as []asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + as = append(as, a1) + } + err = bs.Search(asserts.TestOnly2Type, map[string]string{ + "pk1": "foo", + }, foundCb, 1) // will not find af2 + c.Assert(err, IsNil) + c.Check(as, HasLen, 1) + c.Check(as[0].Revision(), Equals, 1) + +} diff --git a/asserts/memkeypairmgr.go b/asserts/memkeypairmgr.go new file mode 100644 index 00000000..68293a25 --- /dev/null +++ b/asserts/memkeypairmgr.go @@ -0,0 +1,59 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts + +import ( + "sync" +) + +type memoryKeypairManager struct { + pairs map[string]PrivateKey + mu sync.RWMutex +} + +// NewMemoryKeypairManager creates a new key pair manager with a memory backstore. +func NewMemoryKeypairManager() KeypairManager { + return &memoryKeypairManager{ + pairs: make(map[string]PrivateKey), + } +} + +func (mkm *memoryKeypairManager) Put(privKey PrivateKey) error { + mkm.mu.Lock() + defer mkm.mu.Unlock() + + keyID := privKey.PublicKey().ID() + if mkm.pairs[keyID] != nil { + return errKeypairAlreadyExists + } + mkm.pairs[keyID] = privKey + return nil +} + +func (mkm *memoryKeypairManager) Get(keyID string) (PrivateKey, error) { + mkm.mu.RLock() + defer mkm.mu.RUnlock() + + privKey := mkm.pairs[keyID] + if privKey == nil { + return nil, errKeypairNotFound + } + return privKey, nil +} diff --git a/asserts/memkeypairmgr_test.go b/asserts/memkeypairmgr_test.go new file mode 100644 index 00000000..a99018ff --- /dev/null +++ b/asserts/memkeypairmgr_test.go @@ -0,0 +1,73 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type memKeypairMgtSuite struct { + keypairMgr asserts.KeypairManager +} + +var _ = Suite(&memKeypairMgtSuite{}) + +func (mkms *memKeypairMgtSuite) SetUpTest(c *C) { + mkms.keypairMgr = asserts.NewMemoryKeypairManager() +} + +func (mkms *memKeypairMgtSuite) TestPutAndGet(c *C) { + pk1 := testPrivKey1 + keyID := pk1.PublicKey().ID() + err := mkms.keypairMgr.Put(pk1) + c.Assert(err, IsNil) + + got, err := mkms.keypairMgr.Get(keyID) + c.Assert(err, IsNil) + c.Assert(got, NotNil) + c.Check(got.PublicKey().ID(), Equals, pk1.PublicKey().ID()) +} + +func (mkms *memKeypairMgtSuite) TestPutAlreadyExists(c *C) { + pk1 := testPrivKey1 + err := mkms.keypairMgr.Put(pk1) + c.Assert(err, IsNil) + + err = mkms.keypairMgr.Put(pk1) + c.Check(err, ErrorMatches, "key pair with given key id already exists") +} + +func (mkms *memKeypairMgtSuite) TestGetNotFound(c *C) { + pk1 := testPrivKey1 + keyID := pk1.PublicKey().ID() + + got, err := mkms.keypairMgr.Get(keyID) + c.Check(got, IsNil) + c.Check(err, ErrorMatches, "cannot find key pair") + + err = mkms.keypairMgr.Put(pk1) + c.Assert(err, IsNil) + + got, err = mkms.keypairMgr.Get(keyID + "x") + c.Check(got, IsNil) + c.Check(err, ErrorMatches, "cannot find key pair") +} diff --git a/asserts/privkeys_for_test.go b/asserts/privkeys_for_test.go new file mode 100644 index 00000000..ec433f00 --- /dev/null +++ b/asserts/privkeys_for_test.go @@ -0,0 +1,54 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts_test + +import ( + "encoding/base64" + + "golang.org/x/crypto/openpgp/packet" + "golang.org/x/crypto/sha3" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +// private keys to use in tests +var ( + // use a shorter key length here for test keys because otherwise + // they take too long to generate; + // the ones that care use pregenerated keys of the right length + // or use GenerateKey directly + testPrivKey0, _ = assertstest.GenerateKey(752) + testPrivKey1, testPrivKey1RSA = assertstest.GenerateKey(752) + testPrivKey2, _ = assertstest.GenerateKey(752) + + testPrivKey1SHA3_384 string +) + +func init() { + pkt := packet.NewRSAPrivateKey(asserts.V1FixedTimestamp, testPrivKey1RSA) + h := sha3.New384() + h.Write([]byte{0x1}) + err := pkt.PublicKey.Serialize(h) + if err != nil { + panic(err) + } + testPrivKey1SHA3_384 = base64.RawURLEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/asserts/signtool/sign.go b/asserts/signtool/sign.go new file mode 100644 index 00000000..2ef1bff0 --- /dev/null +++ b/asserts/signtool/sign.go @@ -0,0 +1,88 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +// Package signtool offers tooling to sign assertions. +package signtool + +import ( + "encoding/json" + "fmt" + + "github.com/snapcore/snapd/asserts" +) + +// Options specifies the complete input for signing an assertion. +type Options struct { + // KeyID specifies the key id of the key to use + KeyID string + + // Statement is used as input to construct the assertion + // it's a mapping encoded as JSON + // of the header fields of the assertion + // plus an optional pseudo-header "body" to specify + // the body of the assertion + Statement []byte +} + +// Sign produces the text of a signed assertion as specified by opts. +func Sign(opts *Options, keypairMgr asserts.KeypairManager) ([]byte, error) { + var headers map[string]interface{} + err := json.Unmarshal(opts.Statement, &headers) + if err != nil { + return nil, fmt.Errorf("cannot parse the assertion input as JSON: %v", err) + } + typCand, ok := headers["type"] + if !ok { + return nil, fmt.Errorf("missing assertion type header") + } + typStr, ok := typCand.(string) + if !ok { + return nil, fmt.Errorf("assertion type must be a string, not: %v", typCand) + } + typ := asserts.Type(typStr) + if typ == nil { + return nil, fmt.Errorf("invalid assertion type: %v", headers["type"]) + } + + var body []byte + if bodyCand, ok := headers["body"]; ok { + bodyStr, ok := bodyCand.(string) + if !ok { + return nil, fmt.Errorf("body if specified must be a string") + } + body = []byte(bodyStr) + delete(headers, "body") + } + + adb, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: keypairMgr, + }) + if err != nil { + return nil, err + } + + // TODO: teach Sign to cross check keyID and authority-id + // against an account-key + a, err := adb.Sign(typ, headers, body, opts.KeyID) + if err != nil { + return nil, err + } + + return asserts.Encode(a), nil +} diff --git a/asserts/signtool/sign_test.go b/asserts/signtool/sign_test.go new file mode 100644 index 00000000..007b7efc --- /dev/null +++ b/asserts/signtool/sign_test.go @@ -0,0 +1,179 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package signtool_test + +import ( + "encoding/json" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/signtool" +) + +func TestSigntool(t *testing.T) { TestingT(t) } + +type signSuite struct { + keypairMgr asserts.KeypairManager + testKeyID string +} + +var _ = Suite(&signSuite{}) + +func (s *signSuite) SetUpSuite(c *C) { + testKey, _ := assertstest.GenerateKey(752) + + s.keypairMgr = asserts.NewMemoryKeypairManager() + s.keypairMgr.Put(testKey) + s.testKeyID = testKey.PublicKey().ID() +} + +func expectedModelHeaders(a asserts.Assertion) map[string]interface{} { + m := map[string]interface{}{ + "type": "model", + "authority-id": "user-id1", + "series": "16", + "brand-id": "user-id1", + "model": "baz-3000", + "architecture": "amd64", + "gadget": "brand-gadget", + "kernel": "baz-linux", + "store": "brand-store", + "required-snaps": []interface{}{"foo", "bar"}, + "timestamp": "2015-11-25T20:00:00Z", + } + if a != nil { + m["sign-key-sha3-384"] = a.SignKeyID() + } + return m +} + +func exampleJSON(overrides map[string]interface{}) []byte { + m := expectedModelHeaders(nil) + for k, v := range overrides { + if v == nil { + delete(m, k) + } else { + m[k] = v + } + } + b, err := json.Marshal(m) + if err != nil { + panic(err) + } + return b +} + +func (s *signSuite) TestSignJSON(c *C) { + opts := signtool.Options{ + KeyID: s.testKeyID, + + Statement: exampleJSON(nil), + } + + assertText, err := signtool.Sign(&opts, s.keypairMgr) + c.Assert(err, IsNil) + + a, err := asserts.Decode(assertText) + c.Assert(err, IsNil) + + c.Check(a.Type(), Equals, asserts.ModelType) + c.Check(a.Revision(), Equals, 0) + expectedHeaders := expectedModelHeaders(a) + c.Check(a.Headers(), DeepEquals, expectedHeaders) + + for n, v := range a.Headers() { + c.Check(v, DeepEquals, expectedHeaders[n], Commentf(n)) + } + + c.Check(a.Body(), IsNil) +} + +func (s *signSuite) TestSignJSONWithBodyAndRevision(c *C) { + statement := exampleJSON(map[string]interface{}{ + "body": "BODY", + "revision": "11", + }) + opts := signtool.Options{ + KeyID: s.testKeyID, + + Statement: statement, + } + + assertText, err := signtool.Sign(&opts, s.keypairMgr) + c.Assert(err, IsNil) + + a, err := asserts.Decode(assertText) + c.Assert(err, IsNil) + + c.Check(a.Type(), Equals, asserts.ModelType) + c.Check(a.Revision(), Equals, 11) + + expectedHeaders := expectedModelHeaders(a) + expectedHeaders["revision"] = "11" + expectedHeaders["body-length"] = "4" + + c.Check(a.Headers(), DeepEquals, expectedHeaders) + + c.Check(a.Body(), DeepEquals, []byte("BODY")) +} + +func (s *signSuite) TestSignErrors(c *C) { + opts := signtool.Options{ + KeyID: s.testKeyID, + } + + emptyList := []interface{}{} + + tests := []struct { + expError string + brokenStatement []byte + }{ + {`cannot parse the assertion input as JSON:.*`, + []byte("\x00"), + }, + {`invalid assertion type: what`, + exampleJSON(map[string]interface{}{"type": "what"}), + }, + {`assertion type must be a string, not: \[\]`, + exampleJSON(map[string]interface{}{"type": emptyList}), + }, + {`missing assertion type header`, + exampleJSON(map[string]interface{}{"type": nil}), + }, + {"revision should be positive: -10", + exampleJSON(map[string]interface{}{"revision": "-10"})}, + {`"authority-id" header is mandatory`, + exampleJSON(map[string]interface{}{"authority-id": nil})}, + {`body if specified must be a string`, + exampleJSON(map[string]interface{}{"body": emptyList})}, + } + + for _, t := range tests { + fresh := opts + + fresh.Statement = t.brokenStatement + + _, err := signtool.Sign(&fresh, s.keypairMgr) + c.Check(err, ErrorMatches, t.expError) + } +} diff --git a/asserts/snap_asserts.go b/asserts/snap_asserts.go new file mode 100644 index 00000000..e1ff5456 --- /dev/null +++ b/asserts/snap_asserts.go @@ -0,0 +1,645 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts + +import ( + "bytes" + "crypto" + "fmt" + "regexp" + "time" + + _ "golang.org/x/crypto/sha3" // expected for digests + + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +// SnapDeclaration holds a snap-declaration assertion, declaring a +// snap binding its identifying snap-id to a name, asserting its +// publisher and its other properties. +type SnapDeclaration struct { + assertionBase + refreshControl []string + plugRules map[string]*PlugRule + slotRules map[string]*SlotRule + autoAliases []string + timestamp time.Time +} + +// Series returns the series for which the snap is being declared. +func (snapdcl *SnapDeclaration) Series() string { + return snapdcl.HeaderString("series") +} + +// SnapID returns the snap id of the declared snap. +func (snapdcl *SnapDeclaration) SnapID() string { + return snapdcl.HeaderString("snap-id") +} + +// SnapName returns the declared snap name. +func (snapdcl *SnapDeclaration) SnapName() string { + return snapdcl.HeaderString("snap-name") +} + +// PublisherID returns the identifier of the publisher of the declared snap. +func (snapdcl *SnapDeclaration) PublisherID() string { + return snapdcl.HeaderString("publisher-id") +} + +// Timestamp returns the time when the snap-declaration was issued. +func (snapdcl *SnapDeclaration) Timestamp() time.Time { + return snapdcl.timestamp +} + +// RefreshControl returns the ids of snaps whose updates are controlled by this declaration. +func (snapdcl *SnapDeclaration) RefreshControl() []string { + return snapdcl.refreshControl +} + +// PlugRule returns the plug-side rule about the given interface if one was included in the plugs stanza of the declaration, otherwise it returns nil. +func (snapdcl *SnapDeclaration) PlugRule(interfaceName string) *PlugRule { + return snapdcl.plugRules[interfaceName] +} + +// SlotRule returns the slot-side rule about the given interface if one was included in the slots stanza of the declaration, otherwise it returns nil. +func (snapdcl *SnapDeclaration) SlotRule(interfaceName string) *SlotRule { + return snapdcl.slotRules[interfaceName] +} + +// AutoAliases returns the optional auto-aliases granted to this snap. +func (snapdcl *SnapDeclaration) AutoAliases() []string { + return snapdcl.autoAliases +} + +// Implement further consistency checks. +func (snapdcl *SnapDeclaration) checkConsistency(db RODatabase, acck *AccountKey) error { + if !db.IsTrustedAccount(snapdcl.AuthorityID()) { + return fmt.Errorf("snap-declaration assertion for %q (id %q) is not signed by a directly trusted authority: %s", snapdcl.SnapName(), snapdcl.SnapID(), snapdcl.AuthorityID()) + } + _, err := db.Find(AccountType, map[string]string{ + "account-id": snapdcl.PublisherID(), + }) + if err == ErrNotFound { + return fmt.Errorf("snap-declaration assertion for %q (id %q) does not have a matching account assertion for the publisher %q", snapdcl.SnapName(), snapdcl.SnapID(), snapdcl.PublisherID()) + } + if err != nil { + return err + } + + return nil +} + +// sanity +var _ consistencyChecker = (*SnapDeclaration)(nil) + +// Prerequisites returns references to this snap-declaration's prerequisite assertions. +func (snapdcl *SnapDeclaration) Prerequisites() []*Ref { + return []*Ref{ + {Type: AccountType, PrimaryKey: []string{snapdcl.PublisherID()}}, + } +} + +var validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") + +func assembleSnapDeclaration(assert assertionBase) (Assertion, error) { + _, err := checkExistsString(assert.headers, "snap-name") + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "publisher-id") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + var refControl []string + var plugRules map[string]*PlugRule + var slotRules map[string]*SlotRule + + refControl, err = checkStringList(assert.headers, "refresh-control") + if err != nil { + return nil, err + } + + plugs, err := checkMap(assert.headers, "plugs") + if err != nil { + return nil, err + } + if plugs != nil { + plugRules = make(map[string]*PlugRule, len(plugs)) + for iface, rule := range plugs { + plugRule, err := compilePlugRule(iface, rule) + if err != nil { + return nil, err + } + plugRules[iface] = plugRule + } + } + + slots, err := checkMap(assert.headers, "slots") + if err != nil { + return nil, err + } + if slots != nil { + slotRules = make(map[string]*SlotRule, len(slots)) + for iface, rule := range slots { + slotRule, err := compileSlotRule(iface, rule) + if err != nil { + return nil, err + } + slotRules[iface] = slotRule + } + } + + autoAliases, err := checkStringListMatches(assert.headers, "auto-aliases", validAlias) + if err != nil { + return nil, err + } + + return &SnapDeclaration{ + assertionBase: assert, + refreshControl: refControl, + plugRules: plugRules, + slotRules: slotRules, + autoAliases: autoAliases, + timestamp: timestamp, + }, nil +} + +// SnapFileSHA3_384 computes the SHA3-384 digest of the given snap file. +// It also returns its size. +func SnapFileSHA3_384(snapPath string) (digest string, size uint64, err error) { + sha3_384Dgst, size, err := osutil.FileDigest(snapPath, crypto.SHA3_384) + if err != nil { + return "", 0, fmt.Errorf("cannot compute snap %q digest: %v", snapPath, err) + } + + sha3_384, err := EncodeDigest(crypto.SHA3_384, sha3_384Dgst) + if err != nil { + return "", 0, fmt.Errorf("cannot encode snap %q digest: %v", snapPath, err) + } + return sha3_384, size, nil +} + +// SnapBuild holds a snap-build assertion, asserting the properties of a snap +// at the time it was built by the developer. +type SnapBuild struct { + assertionBase + size uint64 + timestamp time.Time +} + +// SnapSHA3_384 returns the SHA3-384 digest of the snap. +func (snapbld *SnapBuild) SnapSHA3_384() string { + return snapbld.HeaderString("snap-sha3-384") +} + +// SnapID returns the snap id of the snap. +func (snapbld *SnapBuild) SnapID() string { + return snapbld.HeaderString("snap-id") +} + +// SnapSize returns the size of the snap. +func (snapbld *SnapBuild) SnapSize() uint64 { + return snapbld.size +} + +// Grade returns the grade of the snap: devel|stable +func (snapbld *SnapBuild) Grade() string { + return snapbld.HeaderString("grade") +} + +// Timestamp returns the time when the snap-build assertion was created. +func (snapbld *SnapBuild) Timestamp() time.Time { + return snapbld.timestamp +} + +func assembleSnapBuild(assert assertionBase) (Assertion, error) { + _, err := checkDigest(assert.headers, "snap-sha3-384", crypto.SHA3_384) + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "snap-id") + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "grade") + if err != nil { + return nil, err + } + + size, err := checkUint(assert.headers, "snap-size", 64) + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + // ignore extra headers and non-empty body for future compatibility + return &SnapBuild{ + assertionBase: assert, + size: size, + timestamp: timestamp, + }, nil +} + +// SnapRevision holds a snap-revision assertion, which is a statement by the +// store acknowledging the receipt of a build of a snap and labeling it with a +// snap revision. +type SnapRevision struct { + assertionBase + snapSize uint64 + snapRevision int + timestamp time.Time +} + +// SnapSHA3_384 returns the SHA3-384 digest of the snap. +func (snaprev *SnapRevision) SnapSHA3_384() string { + return snaprev.HeaderString("snap-sha3-384") +} + +// SnapID returns the snap id of the snap. +func (snaprev *SnapRevision) SnapID() string { + return snaprev.HeaderString("snap-id") +} + +// SnapSize returns the size in bytes of the snap submitted to the store. +func (snaprev *SnapRevision) SnapSize() uint64 { + return snaprev.snapSize +} + +// SnapRevision returns the revision assigned to this build of the snap. +func (snaprev *SnapRevision) SnapRevision() int { + return snaprev.snapRevision +} + +// DeveloperID returns the id of the developer that submitted this build of the +// snap. +func (snaprev *SnapRevision) DeveloperID() string { + return snaprev.HeaderString("developer-id") +} + +// Timestamp returns the time when the snap-revision was issued. +func (snaprev *SnapRevision) Timestamp() time.Time { + return snaprev.timestamp +} + +// Implement further consistency checks. +func (snaprev *SnapRevision) checkConsistency(db RODatabase, acck *AccountKey) error { + // TODO: expand this to consider other stores signing on their own + if !db.IsTrustedAccount(snaprev.AuthorityID()) { + return fmt.Errorf("snap-revision assertion for snap id %q is not signed by a store: %s", snaprev.SnapID(), snaprev.AuthorityID()) + } + _, err := db.Find(AccountType, map[string]string{ + "account-id": snaprev.DeveloperID(), + }) + if err == ErrNotFound { + return fmt.Errorf("snap-revision assertion for snap id %q does not have a matching account assertion for the developer %q", snaprev.SnapID(), snaprev.DeveloperID()) + } + if err != nil { + return err + } + _, err = db.Find(SnapDeclarationType, map[string]string{ + // XXX: mediate getting current series through some context object? this gets the job done for now + "series": release.Series, + "snap-id": snaprev.SnapID(), + }) + if err == ErrNotFound { + return fmt.Errorf("snap-revision assertion for snap id %q does not have a matching snap-declaration assertion", snaprev.SnapID()) + } + if err != nil { + return err + } + return nil +} + +// sanity +var _ consistencyChecker = (*SnapRevision)(nil) + +// Prerequisites returns references to this snap-revision's prerequisite assertions. +func (snaprev *SnapRevision) Prerequisites() []*Ref { + return []*Ref{ + // XXX: mediate getting current series through some context object? this gets the job done for now + {Type: SnapDeclarationType, PrimaryKey: []string{release.Series, snaprev.SnapID()}}, + {Type: AccountType, PrimaryKey: []string{snaprev.DeveloperID()}}, + } +} + +func assembleSnapRevision(assert assertionBase) (Assertion, error) { + _, err := checkDigest(assert.headers, "snap-sha3-384", crypto.SHA3_384) + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "snap-id") + if err != nil { + return nil, err + } + + snapSize, err := checkUint(assert.headers, "snap-size", 64) + if err != nil { + return nil, err + } + + snapRevision, err := checkInt(assert.headers, "snap-revision") + if err != nil { + return nil, err + } + if snapRevision < 1 { + return nil, fmt.Errorf(`"snap-revision" header must be >=1: %d`, snapRevision) + } + + _, err = checkNotEmptyString(assert.headers, "developer-id") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &SnapRevision{ + assertionBase: assert, + snapSize: snapSize, + snapRevision: snapRevision, + timestamp: timestamp, + }, nil +} + +// Validation holds a validation assertion, describing that a combination of +// (snap-id, approved-snap-id, approved-revision) has been validated for +// the series, meaning updating to that revision of approved-snap-id +// has been approved by the owner of the gating snap with snap-id. +type Validation struct { + assertionBase + revoked bool + timestamp time.Time + approvedSnapRevision int +} + +// Series returns the series for which the validation holds. +func (validation *Validation) Series() string { + return validation.HeaderString("series") +} + +// SnapID returns the ID of the gating snap. +func (validation *Validation) SnapID() string { + return validation.HeaderString("snap-id") +} + +// ApprovedSnapID returns the ID of the gated snap. +func (validation *Validation) ApprovedSnapID() string { + return validation.HeaderString("approved-snap-id") +} + +// ApprovedSnapRevision returns the approved revision of the gated snap. +func (validation *Validation) ApprovedSnapRevision() int { + return validation.approvedSnapRevision +} + +// Revoked returns true if the validation has been revoked. +func (validation *Validation) Revoked() bool { + return validation.revoked +} + +// Timestamp returns the time when the validation was issued. +func (validation *Validation) Timestamp() time.Time { + return validation.timestamp +} + +// Implement further consistency checks. +func (validation *Validation) checkConsistency(db RODatabase, acck *AccountKey) error { + _, err := db.Find(SnapDeclarationType, map[string]string{ + "series": validation.Series(), + "snap-id": validation.ApprovedSnapID(), + }) + if err == ErrNotFound { + return fmt.Errorf("validation assertion by snap-id %q does not have a matching snap-declaration assertion for approved-snap-id %q", validation.SnapID(), validation.ApprovedSnapID()) + } + if err != nil { + return err + } + a, err := db.Find(SnapDeclarationType, map[string]string{ + "series": validation.Series(), + "snap-id": validation.SnapID(), + }) + if err == ErrNotFound { + return fmt.Errorf("validation assertion by snap-id %q does not have a matching snap-declaration assertion", validation.SnapID()) + } + if err != nil { + return err + } + + gatingDecl := a.(*SnapDeclaration) + if gatingDecl.PublisherID() != validation.AuthorityID() { + return fmt.Errorf("validation assertion by snap %q (id %q) not signed by its publisher", gatingDecl.SnapName(), validation.SnapID()) + } + + return nil +} + +// sanity +var _ consistencyChecker = (*Validation)(nil) + +// Prerequisites returns references to this validation's prerequisite assertions. +func (validation *Validation) Prerequisites() []*Ref { + return []*Ref{ + {Type: SnapDeclarationType, PrimaryKey: []string{validation.Series(), validation.SnapID()}}, + {Type: SnapDeclarationType, PrimaryKey: []string{validation.Series(), validation.ApprovedSnapID()}}, + } +} + +func assembleValidation(assert assertionBase) (Assertion, error) { + approvedSnapRevision, err := checkInt(assert.headers, "approved-snap-revision") + if err != nil { + return nil, err + } + if approvedSnapRevision < 1 { + return nil, fmt.Errorf(`"approved-snap-revision" header must be >=1: %d`, approvedSnapRevision) + } + + revoked, err := checkOptionalBool(assert.headers, "revoked") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &Validation{ + assertionBase: assert, + revoked: revoked, + timestamp: timestamp, + approvedSnapRevision: approvedSnapRevision, + }, nil +} + +// BaseDeclaration holds a base-declaration assertion, declaring the +// policies (to start with interface ones) applying to all snaps of +// a series. +type BaseDeclaration struct { + assertionBase + plugRules map[string]*PlugRule + slotRules map[string]*SlotRule + timestamp time.Time +} + +// Series returns the series whose snaps are governed by the declaration. +func (basedcl *BaseDeclaration) Series() string { + return basedcl.HeaderString("series") +} + +// Timestamp returns the time when the base-declaration was issued. +func (basedcl *BaseDeclaration) Timestamp() time.Time { + return basedcl.timestamp +} + +// PlugRule returns the plug-side rule about the given interface if one was included in the plugs stanza of the declaration, otherwise it returns nil. +func (basedcl *BaseDeclaration) PlugRule(interfaceName string) *PlugRule { + return basedcl.plugRules[interfaceName] +} + +// SlotRule returns the slot-side rule about the given interface if one was included in the slots stanza of the declaration, otherwise it returns nil. +func (basedcl *BaseDeclaration) SlotRule(interfaceName string) *SlotRule { + return basedcl.slotRules[interfaceName] +} + +// Implement further consistency checks. +func (basedcl *BaseDeclaration) checkConsistency(db RODatabase, acck *AccountKey) error { + // XXX: not signed or stored yet in a db, but being ready for that + if !db.IsTrustedAccount(basedcl.AuthorityID()) { + return fmt.Errorf("base-declaration assertion for series %s is not signed by a directly trusted authority: %s", basedcl.Series(), basedcl.AuthorityID()) + } + return nil +} + +// sanity +var _ consistencyChecker = (*BaseDeclaration)(nil) + +func assembleBaseDeclaration(assert assertionBase) (Assertion, error) { + var plugRules map[string]*PlugRule + plugs, err := checkMap(assert.headers, "plugs") + if err != nil { + return nil, err + } + if plugs != nil { + plugRules = make(map[string]*PlugRule, len(plugs)) + for iface, rule := range plugs { + plugRule, err := compilePlugRule(iface, rule) + if err != nil { + return nil, err + } + plugRules[iface] = plugRule + } + } + + var slotRules map[string]*SlotRule + slots, err := checkMap(assert.headers, "slots") + if err != nil { + return nil, err + } + if slots != nil { + slotRules = make(map[string]*SlotRule, len(slots)) + for iface, rule := range slots { + slotRule, err := compileSlotRule(iface, rule) + if err != nil { + return nil, err + } + slotRules[iface] = slotRule + } + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &BaseDeclaration{ + assertionBase: assert, + plugRules: plugRules, + slotRules: slotRules, + timestamp: timestamp, + }, nil +} + +var builtinBaseDeclaration *BaseDeclaration + +// BuiltinBaseDeclaration exposes the initialized builtin base-declaration assertion. This is used by overlord/assertstate, other code should use assertstate.BaseDeclaration. +func BuiltinBaseDeclaration() *BaseDeclaration { + return builtinBaseDeclaration +} + +var ( + builtinBaseDeclarationCheckOrder = []string{"type", "authority-id", "series"} + builtinBaseDeclarationExpectedHeaders = map[string]interface{}{ + "type": "base-declaration", + "authority-id": "canonical", + "series": release.Series, + } +) + +// InitBuiltinBaseDeclaration initializes the builtin base-declaration based on headers (or resets it if headers is nil). +func InitBuiltinBaseDeclaration(headers []byte) error { + if headers == nil { + builtinBaseDeclaration = nil + return nil + } + trimmed := bytes.TrimSpace(headers) + h, err := parseHeaders(trimmed) + if err != nil { + return err + } + for _, name := range builtinBaseDeclarationCheckOrder { + expected := builtinBaseDeclarationExpectedHeaders[name] + if h[name] != expected { + return fmt.Errorf("the builtin base-declaration %q header is not set to expected value %q", name, expected) + } + } + revision, err := checkRevision(h) + if err != nil { + return fmt.Errorf("cannot assemble the builtin-base declaration: %v", err) + } + h["timestamp"] = time.Now().UTC().Format(time.RFC3339) + a, err := assembleBaseDeclaration(assertionBase{ + headers: h, + body: nil, + revision: revision, + content: trimmed, + signature: []byte("$builtin"), + }) + if err != nil { + return fmt.Errorf("cannot assemble the builtin base-declaration: %v", err) + } + builtinBaseDeclaration = a.(*BaseDeclaration) + return nil +} diff --git a/asserts/snap_asserts_test.go b/asserts/snap_asserts_test.go new file mode 100644 index 00000000..8037d623 --- /dev/null +++ b/asserts/snap_asserts_test.go @@ -0,0 +1,1258 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package asserts_test + +import ( + "encoding/base64" + "io/ioutil" + "path/filepath" + "strings" + "time" + + "golang.org/x/crypto/sha3" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +var ( + _ = Suite(&snapDeclSuite{}) + _ = Suite(&snapFileDigestSuite{}) + _ = Suite(&snapBuildSuite{}) + _ = Suite(&snapRevSuite{}) + _ = Suite(&validationSuite{}) + _ = Suite(&baseDeclSuite{}) +) + +type snapDeclSuite struct { + ts time.Time + tsLine string +} + +func (sds *snapDeclSuite) SetUpSuite(c *C) { + sds.ts = time.Now().Truncate(time.Second).UTC() + sds.tsLine = "timestamp: " + sds.ts.Format(time.RFC3339) + "\n" +} + +func (sds *snapDeclSuite) TestDecodeOK(c *C) { + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: first\n" + + "publisher-id: dev-id1\n" + + "refresh-control:\n - foo\n - bar\n" + + "auto-aliases:\n - cmd1\n - cmd_2\n - Cmd-3\n - CMD.4\n" + + sds.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapDeclarationType) + snapDecl := a.(*asserts.SnapDeclaration) + c.Check(snapDecl.AuthorityID(), Equals, "canonical") + c.Check(snapDecl.Timestamp(), Equals, sds.ts) + c.Check(snapDecl.Series(), Equals, "16") + c.Check(snapDecl.SnapID(), Equals, "snap-id-1") + c.Check(snapDecl.SnapName(), Equals, "first") + c.Check(snapDecl.PublisherID(), Equals, "dev-id1") + c.Check(snapDecl.RefreshControl(), DeepEquals, []string{"foo", "bar"}) + c.Check(snapDecl.AutoAliases(), DeepEquals, []string{"cmd1", "cmd_2", "Cmd-3", "CMD.4"}) +} + +func (sds *snapDeclSuite) TestEmptySnapName(c *C) { + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: \n" + + "publisher-id: dev-id1\n" + + sds.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + snapDecl := a.(*asserts.SnapDeclaration) + c.Check(snapDecl.SnapName(), Equals, "") +} + +func (sds *snapDeclSuite) TestMissingRefreshControlAutoAliases(c *C) { + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: \n" + + "publisher-id: dev-id1\n" + + sds.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + snapDecl := a.(*asserts.SnapDeclaration) + c.Check(snapDecl.RefreshControl(), HasLen, 0) + c.Check(snapDecl.AutoAliases(), HasLen, 0) +} + +const ( + snapDeclErrPrefix = "assertion snap-declaration: " +) + +func (sds *snapDeclSuite) TestDecodeInvalid(c *C) { + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: first\n" + + "publisher-id: dev-id1\n" + + "refresh-control:\n - foo\n - bar\n" + + "auto-aliases:\n - cmd1\n - cmd2\n" + + "plugs:\n interface1: true\n" + + "slots:\n interface2: true\n" + + sds.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series: 16\n", "", `"series" header is mandatory`}, + {"series: 16\n", "series: \n", `"series" header should not be empty`}, + {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`}, + {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`}, + {"snap-name: first\n", "", `"snap-name" header is mandatory`}, + {"publisher-id: dev-id1\n", "", `"publisher-id" header is mandatory`}, + {"publisher-id: dev-id1\n", "publisher-id: \n", `"publisher-id" header should not be empty`}, + {"refresh-control:\n - foo\n - bar\n", "refresh-control: foo\n", `"refresh-control" header must be a list of strings`}, + {"refresh-control:\n - foo\n - bar\n", "refresh-control:\n -\n - nested\n", `"refresh-control" header must be a list of strings`}, + {"plugs:\n interface1: true\n", "plugs: \n", `"plugs" header must be a map`}, + {"plugs:\n interface1: true\n", "plugs:\n intf1:\n foo: bar\n", `plug rule for interface "intf1" must specify at least one of.*`}, + {"slots:\n interface2: true\n", "slots: \n", `"slots" header must be a map`}, + {"slots:\n interface2: true\n", "slots:\n intf1:\n foo: bar\n", `slot rule for interface "intf1" must specify at least one of.*`}, + {"auto-aliases:\n - cmd1\n - cmd2\n", "auto-aliases: cmd0\n", `"auto-aliases" header must be a list of strings`}, + {"auto-aliases:\n - cmd1\n - cmd2\n", "auto-aliases:\n -\n - nested\n", `"auto-aliases" header must be a list of strings`}, + {"auto-aliases:\n - cmd1\n - cmd2\n", "auto-aliases:\n - _cmd-1\n - cmd2\n", `"auto-aliases" header contains an invalid element: "_cmd-1"`}, + {sds.tsLine, "", `"timestamp" header is mandatory`}, + {sds.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {sds.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, snapDeclErrPrefix+test.expectedErr) + } + +} + +func (sds *snapDeclSuite) TestDecodePlugsAndSlots(c *C) { + encoded := `type: snap-declaration +authority-id: canonical +series: 16 +snap-id: snap-id-1 +snap-name: first +publisher-id: dev-id1 +plugs: + interface1: + deny-installation: false + allow-auto-connection: + slot-snap-type: + - app + slot-publisher-id: + - acme + slot-attributes: + a1: /foo/.* + plug-attributes: + b1: B1 + deny-auto-connection: + slot-attributes: + a1: !A1 + plug-attributes: + b1: !B1 + interface2: + allow-installation: true + allow-connection: + plug-attributes: + a2: A2 + slot-attributes: + b2: B2 + deny-connection: + slot-snap-id: + - snapidsnapidsnapidsnapidsnapid01 + - snapidsnapidsnapidsnapidsnapid02 + plug-attributes: + a2: !A2 + slot-attributes: + b2: !B2 +slots: + interface3: + deny-installation: false + allow-auto-connection: + plug-snap-type: + - app + plug-publisher-id: + - acme + slot-attributes: + c1: /foo/.* + plug-attributes: + d1: C1 + deny-auto-connection: + slot-attributes: + c1: !C1 + plug-attributes: + d1: !D1 + interface4: + allow-connection: + plug-attributes: + c2: C2 + slot-attributes: + d2: D2 + deny-connection: + plug-snap-id: + - snapidsnapidsnapidsnapidsnapid01 + - snapidsnapidsnapidsnapidsnapid02 + plug-attributes: + c2: !D2 + slot-attributes: + d2: !D2 + allow-installation: + slot-snap-type: + - app + slot-attributes: + e1: E1 +TSLINE +body-length: 0 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AXNpZw==` + encoded = strings.Replace(encoded, "TSLINE\n", sds.tsLine, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.SupportedFormat(), Equals, true) + snapDecl := a.(*asserts.SnapDeclaration) + c.Check(snapDecl.Series(), Equals, "16") + c.Check(snapDecl.SnapID(), Equals, "snap-id-1") + + c.Check(snapDecl.PlugRule("interfaceX"), IsNil) + c.Check(snapDecl.SlotRule("interfaceX"), IsNil) + + plugRule1 := snapDecl.PlugRule("interface1") + c.Assert(plugRule1, NotNil) + c.Assert(plugRule1.DenyInstallation, HasLen, 1) + c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(plugRule1.AllowAutoConnection, HasLen, 1) + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"}) + c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"}) + c.Assert(plugRule1.DenyAutoConnection, HasLen, 1) + c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`) + plugRule2 := snapDecl.PlugRule("interface2") + c.Assert(plugRule2, NotNil) + c.Assert(plugRule2.AllowInstallation, HasLen, 1) + c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(plugRule2.AllowConnection, HasLen, 1) + c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`) + c.Assert(plugRule2.DenyConnection, HasLen, 1) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + + slotRule3 := snapDecl.SlotRule("interface3") + c.Assert(slotRule3, NotNil) + c.Assert(slotRule3.DenyInstallation, HasLen, 1) + c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(slotRule3.AllowAutoConnection, HasLen, 1) + c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"}) + c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"}) + c.Assert(slotRule3.DenyAutoConnection, HasLen, 1) + c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`) + slotRule4 := snapDecl.SlotRule("interface4") + c.Assert(slotRule4, NotNil) + c.Assert(slotRule4.AllowAutoConnection, HasLen, 1) + c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`) + c.Assert(slotRule4.DenyAutoConnection, HasLen, 1) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + c.Assert(slotRule4.AllowInstallation, HasLen, 1) + c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) +} + +func prereqDevAccount(c *C, storeDB assertstest.SignerDB, db *asserts.Database) { + dev1Acct := assertstest.NewAccount(storeDB, "developer1", map[string]interface{}{ + "account-id": "dev-id1", + }, "") + err := db.Add(dev1Acct) + c.Assert(err, IsNil) +} + +func (sds *snapDeclSuite) TestSnapDeclarationCheck(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "dev-id1", + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapDecl) + c.Assert(err, IsNil) +} + +func (sds *snapDeclSuite) TestSnapDeclarationCheckUntrustedAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "dev-id1", + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := otherDB.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapDecl) + c.Assert(err, ErrorMatches, `snap-declaration assertion for "foo" \(id "snap-id-1"\) is not signed by a directly trusted authority:.*`) +} + +func (sds *snapDeclSuite) TestSnapDeclarationCheckMissingPublisherAccount(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "dev-id1", + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapDecl) + c.Assert(err, ErrorMatches, `snap-declaration assertion for "foo" \(id "snap-id-1"\) does not have a matching account assertion for the publisher "dev-id1"`) +} + +type snapFileDigestSuite struct{} + +func (s *snapFileDigestSuite) TestSnapFileSHA3_384(c *C) { + exData := []byte("hashmeplease") + + tempdir := c.MkDir() + snapFn := filepath.Join(tempdir, "ex.snap") + err := ioutil.WriteFile(snapFn, exData, 0644) + c.Assert(err, IsNil) + + encDgst, size, err := asserts.SnapFileSHA3_384(snapFn) + c.Assert(err, IsNil) + c.Check(size, Equals, uint64(len(exData))) + + h3_384 := sha3.Sum384(exData) + expected := base64.RawURLEncoding.EncodeToString(h3_384[:]) + c.Check(encDgst, DeepEquals, expected) +} + +type snapBuildSuite struct { + ts time.Time + tsLine string +} + +func (sds *snapDeclSuite) TestPrerequisites(c *C) { + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: first\n" + + "publisher-id: dev-id1\n" + + sds.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + prereqs := a.Prerequisites() + c.Assert(prereqs, HasLen, 1) + c.Check(prereqs[0], DeepEquals, &asserts.Ref{ + Type: asserts.AccountType, + PrimaryKey: []string{"dev-id1"}, + }) +} + +func (sbs *snapBuildSuite) SetUpSuite(c *C) { + sbs.ts = time.Now().Truncate(time.Second).UTC() + sbs.tsLine = "timestamp: " + sbs.ts.Format(time.RFC3339) + "\n" +} + +const ( + blobSHA3_384 = "QlqR0uAWEAWF5Nwnzj5kqmmwFslYPu1IL16MKtLKhwhv0kpBv5wKZ_axf_nf_2cL" +) + +func (sbs *snapBuildSuite) TestDecodeOK(c *C) { + encoded := "type: snap-build\n" + + "authority-id: dev-id1\n" + + "snap-sha3-384: " + blobSHA3_384 + "\n" + + "grade: stable\n" + + "snap-id: snap-id-1\n" + + "snap-size: 10000\n" + + sbs.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapBuildType) + snapBuild := a.(*asserts.SnapBuild) + c.Check(snapBuild.AuthorityID(), Equals, "dev-id1") + c.Check(snapBuild.Timestamp(), Equals, sbs.ts) + c.Check(snapBuild.SnapID(), Equals, "snap-id-1") + c.Check(snapBuild.SnapSHA3_384(), Equals, blobSHA3_384) + c.Check(snapBuild.SnapSize(), Equals, uint64(10000)) + c.Check(snapBuild.Grade(), Equals, "stable") +} + +const ( + snapBuildErrPrefix = "assertion snap-build: " +) + +func (sbs *snapBuildSuite) TestDecodeInvalid(c *C) { + digestHdr := "snap-sha3-384: " + blobSHA3_384 + "\n" + + encoded := "type: snap-build\n" + + "authority-id: dev-id1\n" + + digestHdr + + "grade: stable\n" + + "snap-id: snap-id-1\n" + + "snap-size: 10000\n" + + sbs.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`}, + {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`}, + {digestHdr, "", `"snap-sha3-384" header is mandatory`}, + {digestHdr, "snap-sha3-384: \n", `"snap-sha3-384" header should not be empty`}, + {digestHdr, "snap-sha3-384: #\n", `"snap-sha3-384" header cannot be decoded:.*`}, + {"snap-size: 10000\n", "", `"snap-size" header is mandatory`}, + {"snap-size: 10000\n", "snap-size: -1\n", `"snap-size" header is not an unsigned integer: -1`}, + {"snap-size: 10000\n", "snap-size: zzz\n", `"snap-size" header is not an unsigned integer: zzz`}, + {"grade: stable\n", "", `"grade" header is mandatory`}, + {"grade: stable\n", "grade: \n", `"grade" header should not be empty`}, + {sbs.tsLine, "", `"timestamp" header is mandatory`}, + {sbs.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {sbs.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, snapBuildErrPrefix+test.expectedErr) + } +} + +func makeStoreAndCheckDB(c *C) (storeDB *assertstest.SigningDB, checkDB *asserts.Database) { + trustedPrivKey := testPrivKey0 + storePrivKey := testPrivKey1 + + store := assertstest.NewStoreStack("canonical", trustedPrivKey, storePrivKey) + cfg := &asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + } + checkDB, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + // add store key + err = checkDB.Add(store.StoreAccountKey("")) + c.Assert(err, IsNil) + + return store.SigningDB, checkDB +} + +func setup3rdPartySigning(c *C, username string, storeDB *assertstest.SigningDB, checkDB *asserts.Database) (signingDB *assertstest.SigningDB) { + privKey := testPrivKey2 + + acct := assertstest.NewAccount(storeDB, username, map[string]interface{}{ + "account-id": username, + }, "") + accKey := assertstest.NewAccountKey(storeDB, acct, nil, privKey.PublicKey(), "") + + err := checkDB.Add(acct) + c.Assert(err, IsNil) + err = checkDB.Add(accKey) + c.Assert(err, IsNil) + + return assertstest.NewSigningDB(acct.AccountID(), privKey) +} + +func (sbs *snapBuildSuite) TestSnapBuildCheck(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "devel1", storeDB, db) + + headers := map[string]interface{}{ + "authority-id": "devel1", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": time.Now().Format(time.RFC3339), + } + snapBuild, err := devDB.Sign(asserts.SnapBuildType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapBuild) + c.Assert(err, IsNil) +} + +func (sbs *snapBuildSuite) TestSnapBuildCheckInconsistentTimestamp(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "devel1", storeDB, db) + + headers := map[string]interface{}{ + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": "2013-01-01T14:00:00Z", + } + snapBuild, err := devDB.Sign(asserts.SnapBuildType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapBuild) + c.Assert(err, ErrorMatches, "snap-build assertion timestamp outside of signing key validity") +} + +type snapRevSuite struct { + ts time.Time + tsLine string + validEncoded string +} + +func (srs *snapRevSuite) SetUpSuite(c *C) { + srs.ts = time.Now().Truncate(time.Second).UTC() + srs.tsLine = "timestamp: " + srs.ts.Format(time.RFC3339) + "\n" +} + +func (srs *snapRevSuite) makeValidEncoded() string { + return "type: snap-revision\n" + + "authority-id: store-id1\n" + + "snap-sha3-384: " + blobSHA3_384 + "\n" + + "snap-id: snap-id-1\n" + + "snap-size: 123\n" + + "snap-revision: 1\n" + + "developer-id: dev-id1\n" + + "revision: 1\n" + + srs.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" +} + +func (srs *snapRevSuite) makeHeaders(overrides map[string]interface{}) map[string]interface{} { + headers := map[string]interface{}{ + "authority-id": "canonical", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "snap-size": "123", + "snap-revision": "1", + "developer-id": "dev-id1", + "revision": "1", + "timestamp": time.Now().Format(time.RFC3339), + } + for k, v := range overrides { + headers[k] = v + } + return headers +} + +func (srs *snapRevSuite) TestDecodeOK(c *C) { + encoded := srs.makeValidEncoded() + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapRevisionType) + snapRev := a.(*asserts.SnapRevision) + c.Check(snapRev.AuthorityID(), Equals, "store-id1") + c.Check(snapRev.Timestamp(), Equals, srs.ts) + c.Check(snapRev.SnapID(), Equals, "snap-id-1") + c.Check(snapRev.SnapSHA3_384(), Equals, blobSHA3_384) + c.Check(snapRev.SnapSize(), Equals, uint64(123)) + c.Check(snapRev.SnapRevision(), Equals, 1) + c.Check(snapRev.DeveloperID(), Equals, "dev-id1") + c.Check(snapRev.Revision(), Equals, 1) +} + +const ( + snapRevErrPrefix = "assertion snap-revision: " +) + +func (srs *snapRevSuite) TestDecodeInvalid(c *C) { + encoded := srs.makeValidEncoded() + + digestHdr := "snap-sha3-384: " + blobSHA3_384 + "\n" + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`}, + {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`}, + {digestHdr, "", `"snap-sha3-384" header is mandatory`}, + {digestHdr, "snap-sha3-384: \n", `"snap-sha3-384" header should not be empty`}, + {digestHdr, "snap-sha3-384: #\n", `"snap-sha3-384" header cannot be decoded:.*`}, + {digestHdr, "snap-sha3-384: eHl6\n", `"snap-sha3-384" header does not have the expected bit length: 24`}, + {"snap-size: 123\n", "", `"snap-size" header is mandatory`}, + {"snap-size: 123\n", "snap-size: \n", `"snap-size" header should not be empty`}, + {"snap-size: 123\n", "snap-size: -1\n", `"snap-size" header is not an unsigned integer: -1`}, + {"snap-size: 123\n", "snap-size: zzz\n", `"snap-size" header is not an unsigned integer: zzz`}, + {"snap-revision: 1\n", "", `"snap-revision" header is mandatory`}, + {"snap-revision: 1\n", "snap-revision: \n", `"snap-revision" header should not be empty`}, + {"snap-revision: 1\n", "snap-revision: -1\n", `"snap-revision" header must be >=1: -1`}, + {"snap-revision: 1\n", "snap-revision: 0\n", `"snap-revision" header must be >=1: 0`}, + {"snap-revision: 1\n", "snap-revision: zzz\n", `"snap-revision" header is not an integer: zzz`}, + {"developer-id: dev-id1\n", "", `"developer-id" header is mandatory`}, + {"developer-id: dev-id1\n", "developer-id: \n", `"developer-id" header should not be empty`}, + {srs.tsLine, "", `"timestamp" header is mandatory`}, + {srs.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {srs.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, snapRevErrPrefix+test.expectedErr) + } +} + +func prereqSnapDecl(c *C, storeDB assertstest.SignerDB, db *asserts.Database) { + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "dev-id1", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) +} + +func (srs *snapRevSuite) TestSnapRevisionCheck(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + prereqSnapDecl(c, storeDB, db) + + headers := srs.makeHeaders(nil) + snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Assert(err, IsNil) +} + +func (srs *snapRevSuite) TestSnapRevisionCheckInconsistentTimestamp(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + headers := srs.makeHeaders(map[string]interface{}{ + "timestamp": "2013-01-01T14:00:00Z", + }) + snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Assert(err, ErrorMatches, "snap-revision assertion timestamp outside of signing key validity") +} + +func (srs *snapRevSuite) TestSnapRevisionCheckUntrustedAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := srs.makeHeaders(nil) + snapRev, err := otherDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Assert(err, ErrorMatches, `snap-revision assertion for snap id "snap-id-1" is not signed by a store:.*`) +} + +func (srs *snapRevSuite) TestSnapRevisionCheckMissingDeveloperAccount(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + headers := srs.makeHeaders(nil) + snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Assert(err, ErrorMatches, `snap-revision assertion for snap id "snap-id-1" does not have a matching account assertion for the developer "dev-id1"`) +} + +func (srs *snapRevSuite) TestSnapRevisionCheckMissingDeclaration(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + + headers := srs.makeHeaders(nil) + snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Assert(err, ErrorMatches, `snap-revision assertion for snap id "snap-id-1" does not have a matching snap-declaration assertion`) +} + +func (srs *snapRevSuite) TestPrimaryKey(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + prereqSnapDecl(c, storeDB, db) + + headers := srs.makeHeaders(nil) + snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapRev) + c.Assert(err, IsNil) + + _, err = db.Find(asserts.SnapRevisionType, map[string]string{ + "snap-sha3-384": headers["snap-sha3-384"].(string), + }) + c.Assert(err, IsNil) +} + +func (srs *snapRevSuite) TestPrerequisites(c *C) { + encoded := srs.makeValidEncoded() + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + prereqs := a.Prerequisites() + c.Assert(prereqs, HasLen, 2) + c.Check(prereqs[0], DeepEquals, &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{"16", "snap-id-1"}, + }) + c.Check(prereqs[1], DeepEquals, &asserts.Ref{ + Type: asserts.AccountType, + PrimaryKey: []string{"dev-id1"}, + }) +} + +type validationSuite struct { + ts time.Time + tsLine string +} + +func (vs *validationSuite) SetUpSuite(c *C) { + vs.ts = time.Now().Truncate(time.Second).UTC() + vs.tsLine = "timestamp: " + vs.ts.Format(time.RFC3339) + "\n" +} + +func (vs *validationSuite) makeValidEncoded() string { + return "type: validation\n" + + "authority-id: dev-id1\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "approved-snap-id: snap-id-2\n" + + "approved-snap-revision: 42\n" + + "revision: 1\n" + + vs.tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" +} + +func (vs *validationSuite) makeHeaders(overrides map[string]interface{}) map[string]interface{} { + headers := map[string]interface{}{ + "authority-id": "dev-id1", + "series": "16", + "snap-id": "snap-id-1", + "approved-snap-id": "snap-id-2", + "approved-snap-revision": "42", + "revision": "1", + "timestamp": time.Now().Format(time.RFC3339), + } + for k, v := range overrides { + headers[k] = v + } + return headers +} + +func (vs *validationSuite) TestDecodeOK(c *C) { + encoded := vs.makeValidEncoded() + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ValidationType) + validation := a.(*asserts.Validation) + c.Check(validation.AuthorityID(), Equals, "dev-id1") + c.Check(validation.Timestamp(), Equals, vs.ts) + c.Check(validation.Series(), Equals, "16") + c.Check(validation.SnapID(), Equals, "snap-id-1") + c.Check(validation.ApprovedSnapID(), Equals, "snap-id-2") + c.Check(validation.ApprovedSnapRevision(), Equals, 42) + c.Check(validation.Revoked(), Equals, false) + c.Check(validation.Revision(), Equals, 1) +} + +const ( + validationErrPrefix = "assertion validation: " +) + +func (vs *validationSuite) TestDecodeInvalid(c *C) { + encoded := vs.makeValidEncoded() + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series: 16\n", "", `"series" header is mandatory`}, + {"series: 16\n", "series: \n", `"series" header should not be empty`}, + {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`}, + {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`}, + {"approved-snap-id: snap-id-2\n", "", `"approved-snap-id" header is mandatory`}, + {"approved-snap-id: snap-id-2\n", "approved-snap-id: \n", `"approved-snap-id" header should not be empty`}, + {"approved-snap-revision: 42\n", "", `"approved-snap-revision" header is mandatory`}, + {"approved-snap-revision: 42\n", "approved-snap-revision: z\n", `"approved-snap-revision" header is not an integer: z`}, + {"approved-snap-revision: 42\n", "approved-snap-revision: 0\n", `"approved-snap-revision" header must be >=1: 0`}, + {"approved-snap-revision: 42\n", "approved-snap-revision: -1\n", `"approved-snap-revision" header must be >=1: -1`}, + {vs.tsLine, "", `"timestamp" header is mandatory`}, + {vs.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {vs.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, validationErrPrefix+test.expectedErr) + } +} + +func prereqSnapDecl2(c *C, storeDB assertstest.SignerDB, db *asserts.Database) { + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-2", + "snap-name": "bar", + "publisher-id": "dev-id1", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) +} + +func (vs *validationSuite) TestValidationCheck(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + prereqSnapDecl(c, storeDB, db) + prereqSnapDecl2(c, storeDB, db) + + headers := vs.makeHeaders(nil) + validation, err := devDB.Sign(asserts.ValidationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(validation) + c.Assert(err, IsNil) +} + +func (vs *validationSuite) TestValidationCheckWrongAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + prereqSnapDecl(c, storeDB, db) + prereqSnapDecl2(c, storeDB, db) + + headers := vs.makeHeaders(nil) + validation, err := storeDB.Sign(asserts.ValidationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(validation) + c.Assert(err, ErrorMatches, `validation assertion by snap "foo" \(id "snap-id-1"\) not signed by its publisher`) +} + +func (vs *validationSuite) TestRevocation(c *C) { + encoded := "type: validation\n" + + "authority-id: dev-id1\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "approved-snap-id: snap-id-2\n" + + "approved-snap-revision: 42\n" + + "revoked: true\n" + + "revision: 1\n" + + vs.tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + validation := a.(*asserts.Validation) + c.Check(validation.Revoked(), Equals, true) +} + +func (vs *validationSuite) TestRevokedFalse(c *C) { + encoded := "type: validation\n" + + "authority-id: dev-id1\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "approved-snap-id: snap-id-2\n" + + "approved-snap-revision: 42\n" + + "revoked: false\n" + + "revision: 1\n" + + vs.tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + validation := a.(*asserts.Validation) + c.Check(validation.Revoked(), Equals, false) +} + +func (vs *validationSuite) TestRevokedInvalid(c *C) { + encoded := "type: validation\n" + + "authority-id: dev-id1\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "approved-snap-id: snap-id-2\n" + + "approved-snap-revision: 42\n" + + "revoked: foo\n" + + "revision: 1\n" + + vs.tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, ErrorMatches, `.*: "revoked" header must be 'true' or 'false'`) +} + +func (vs *validationSuite) TestMissingGatedSnapDeclaration(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + + headers := vs.makeHeaders(nil) + a, err := storeDB.Sign(asserts.ValidationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(a) + c.Assert(err, ErrorMatches, `validation assertion by snap-id "snap-id-1" does not have a matching snap-declaration assertion for approved-snap-id "snap-id-2"`) +} + +func (vs *validationSuite) TestMissingGatingSnapDeclaration(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + prereqSnapDecl2(c, storeDB, db) + + headers := vs.makeHeaders(nil) + a, err := storeDB.Sign(asserts.ValidationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(a) + c.Assert(err, ErrorMatches, `validation assertion by snap-id "snap-id-1" does not have a matching snap-declaration assertion`) +} + +func (vs *validationSuite) TestPrerequisites(c *C) { + encoded := vs.makeValidEncoded() + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + prereqs := a.Prerequisites() + c.Assert(prereqs, HasLen, 2) + c.Check(prereqs[0], DeepEquals, &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{"16", "snap-id-1"}, + }) + c.Check(prereqs[1], DeepEquals, &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{"16", "snap-id-2"}, + }) +} + +type baseDeclSuite struct{} + +func (s *baseDeclSuite) TestDecodeOK(c *C) { + encoded := `type: base-declaration +authority-id: canonical +series: 16 +plugs: + interface1: + deny-installation: false + allow-auto-connection: + slot-snap-type: + - app + slot-publisher-id: + - acme + slot-attributes: + a1: /foo/.* + plug-attributes: + b1: B1 + deny-auto-connection: + slot-attributes: + a1: !A1 + plug-attributes: + b1: !B1 + interface2: + allow-installation: true + allow-connection: + plug-attributes: + a2: A2 + slot-attributes: + b2: B2 + deny-connection: + slot-snap-id: + - snapidsnapidsnapidsnapidsnapid01 + - snapidsnapidsnapidsnapidsnapid02 + plug-attributes: + a2: !A2 + slot-attributes: + b2: !B2 +slots: + interface3: + deny-installation: false + allow-auto-connection: + plug-snap-type: + - app + plug-publisher-id: + - acme + slot-attributes: + c1: /foo/.* + plug-attributes: + d1: C1 + deny-auto-connection: + slot-attributes: + c1: !C1 + plug-attributes: + d1: !D1 + interface4: + allow-connection: + plug-attributes: + c2: C2 + slot-attributes: + d2: D2 + deny-connection: + plug-snap-id: + - snapidsnapidsnapidsnapidsnapid01 + - snapidsnapidsnapidsnapidsnapid02 + plug-attributes: + c2: !D2 + slot-attributes: + d2: !D2 + allow-installation: + slot-snap-type: + - app + slot-attributes: + e1: E1 +timestamp: 2016-09-29T19:50:49Z +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AXNpZw==` + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + baseDecl := a.(*asserts.BaseDeclaration) + c.Check(baseDecl.Series(), Equals, "16") + ts, err := time.Parse(time.RFC3339, "2016-09-29T19:50:49Z") + c.Assert(err, IsNil) + c.Check(baseDecl.Timestamp().Equal(ts), Equals, true) + + c.Check(baseDecl.PlugRule("interfaceX"), IsNil) + c.Check(baseDecl.SlotRule("interfaceX"), IsNil) + + plugRule1 := baseDecl.PlugRule("interface1") + c.Assert(plugRule1, NotNil) + c.Assert(plugRule1.DenyInstallation, HasLen, 1) + c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(plugRule1.AllowAutoConnection, HasLen, 1) + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"}) + c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"}) + c.Assert(plugRule1.DenyAutoConnection, HasLen, 1) + c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`) + plugRule2 := baseDecl.PlugRule("interface2") + c.Assert(plugRule2, NotNil) + c.Assert(plugRule2.AllowInstallation, HasLen, 1) + c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(plugRule2.AllowConnection, HasLen, 1) + c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`) + c.Assert(plugRule2.DenyConnection, HasLen, 1) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + + slotRule3 := baseDecl.SlotRule("interface3") + c.Assert(slotRule3, NotNil) + c.Assert(slotRule3.DenyInstallation, HasLen, 1) + c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(slotRule3.AllowAutoConnection, HasLen, 1) + c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"}) + c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"}) + c.Assert(slotRule3.DenyAutoConnection, HasLen, 1) + c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`) + slotRule4 := baseDecl.SlotRule("interface4") + c.Assert(slotRule4, NotNil) + c.Assert(slotRule4.AllowConnection, HasLen, 1) + c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`) + c.Assert(slotRule4.DenyConnection, HasLen, 1) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + c.Assert(slotRule4.AllowInstallation, HasLen, 1) + c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) + +} + +func (s *baseDeclSuite) TestBaseDeclarationCheckUntrustedAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := map[string]interface{}{ + "series": "16", + "timestamp": time.Now().Format(time.RFC3339), + } + baseDecl, err := otherDB.Sign(asserts.BaseDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(baseDecl) + c.Assert(err, ErrorMatches, `base-declaration assertion for series 16 is not signed by a directly trusted authority: other`) +} + +const ( + baseDeclErrPrefix = "assertion base-declaration: " +) + +func (s *baseDeclSuite) TestDecodeInvalid(c *C) { + tsLine := "timestamp: 2016-09-29T19:50:49Z\n" + + encoded := "type: base-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "plugs:\n interface1: true\n" + + "slots:\n interface2: true\n" + + tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series: 16\n", "", `"series" header is mandatory`}, + {"series: 16\n", "series: \n", `"series" header should not be empty`}, + {"plugs:\n interface1: true\n", "plugs: \n", `"plugs" header must be a map`}, + {"plugs:\n interface1: true\n", "plugs:\n intf1:\n foo: bar\n", `plug rule for interface "intf1" must specify at least one of.*`}, + {"slots:\n interface2: true\n", "slots: \n", `"slots" header must be a map`}, + {"slots:\n interface2: true\n", "slots:\n intf1:\n foo: bar\n", `slot rule for interface "intf1" must specify at least one of.*`}, + {tsLine, "", `"timestamp" header is mandatory`}, + {tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, baseDeclErrPrefix+test.expectedErr) + } + +} + +func (s *baseDeclSuite) TestBuiltin(c *C) { + baseDecl := asserts.BuiltinBaseDeclaration() + c.Check(baseDecl, IsNil) + + defer asserts.InitBuiltinBaseDeclaration(nil) + + const headers = ` +type: base-declaration +authority-id: canonical +series: 16 +revision: 0 +plugs: + network: true +slots: + network: + allow-installation: + slot-snap-type: + - core +` + + err := asserts.InitBuiltinBaseDeclaration([]byte(headers)) + c.Assert(err, IsNil) + + baseDecl = asserts.BuiltinBaseDeclaration() + c.Assert(baseDecl, NotNil) + + cont, _ := baseDecl.Signature() + c.Check(string(cont), Equals, strings.TrimSpace(headers)) + + c.Check(baseDecl.AuthorityID(), Equals, "canonical") + c.Check(baseDecl.Series(), Equals, "16") + c.Check(baseDecl.PlugRule("network").AllowAutoConnection[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Check(baseDecl.SlotRule("network").AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"core"}) + + enc := asserts.Encode(baseDecl) + // it's expected that it cannot be decoded + _, err = asserts.Decode(enc) + c.Check(err, NotNil) +} + +func (s *baseDeclSuite) TestBuiltinInitErrors(c *C) { + defer asserts.InitBuiltinBaseDeclaration(nil) + + tests := []struct { + headers string + err string + }{ + {"", `header entry missing ':' separator: ""`}, + {"type: foo\n", `the builtin base-declaration "type" header is not set to expected value "base-declaration"`}, + {"type: base-declaration", `the builtin base-declaration "authority-id" header is not set to expected value "canonical"`}, + {"type: base-declaration\nauthority-id: canonical", `the builtin base-declaration "series" header is not set to expected value "16"`}, + {"type: base-declaration\nauthority-id: canonical\nseries: 16\nrevision: zzz", `cannot assemble the builtin-base declaration: "revision" header is not an integer: zzz`}, + {"type: base-declaration\nauthority-id: canonical\nseries: 16\nplugs: foo", `cannot assemble the builtin base-declaration: "plugs" header must be a map`}, + } + + for _, t := range tests { + err := asserts.InitBuiltinBaseDeclaration([]byte(t.headers)) + c.Check(err, ErrorMatches, t.err, Commentf(t.headers)) + } +} diff --git a/asserts/snapasserts/snapasserts.go b/asserts/snapasserts/snapasserts.go new file mode 100644 index 00000000..a58b503e --- /dev/null +++ b/asserts/snapasserts/snapasserts.go @@ -0,0 +1,137 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +// Package snapasserts offers helpers to handle snap assertions and their checking for installation. +package snapasserts + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" +) + +func findSnapDeclaration(snapID, name string, db asserts.RODatabase) (*asserts.SnapDeclaration, error) { + a, err := db.Find(asserts.SnapDeclarationType, map[string]string{ + "series": release.Series, + "snap-id": snapID, + }) + if err != nil { + return nil, fmt.Errorf("internal error: cannot find snap declaration for %q: %s", name, snapID) + } + snapDecl := a.(*asserts.SnapDeclaration) + + if snapDecl.SnapName() == "" { + return nil, fmt.Errorf("cannot install snap %q with a revoked snap declaration", name) + } + + return snapDecl, nil +} + +// CrossCheck tries to cross check the name, hash digest and size of a snap plus its metadata in a SideInfo with the relevant snap assertions in a database that should have been populated with them. +func CrossCheck(name, snapSHA3_384 string, snapSize uint64, si *snap.SideInfo, db asserts.RODatabase) error { + // get relevant assertions and do cross checks + a, err := db.Find(asserts.SnapRevisionType, map[string]string{ + "snap-sha3-384": snapSHA3_384, + }) + if err != nil { + return fmt.Errorf("internal error: cannot find pre-populated snap-revision assertion for %q: %s", name, snapSHA3_384) + } + snapRev := a.(*asserts.SnapRevision) + + if snapRev.SnapSize() != snapSize { + return fmt.Errorf("snap %q file does not have expected size according to signatures (download is broken or tampered): %d != %d", name, snapSize, snapRev.SnapSize()) + } + + snapID := si.SnapID + + if snapRev.SnapID() != snapID || snapRev.SnapRevision() != si.Revision.N { + return fmt.Errorf("snap %q does not have expected ID or revision according to assertions (metadata is broken or tampered): %s / %s != %d / %s", name, si.Revision, snapID, snapRev.SnapRevision(), snapRev.SnapID()) + } + + snapDecl, err := findSnapDeclaration(snapID, name, db) + if err != nil { + return err + } + + if snapDecl.SnapName() != name { + return fmt.Errorf("cannot install snap %q that is undergoing a rename to %q", name, snapDecl.SnapName()) + } + + return nil +} + +// DeriveSideInfo tries to construct a SideInfo for the given snap using its digest to find the relevant snap assertions with the information in the given database. It will fail with asserts.ErrNotFound if it cannot find them. +func DeriveSideInfo(snapPath string, db asserts.RODatabase) (*snap.SideInfo, error) { + snapSHA3_384, snapSize, err := asserts.SnapFileSHA3_384(snapPath) + if err != nil { + return nil, err + } + + // get relevant assertions and reconstruct metadata + a, err := db.Find(asserts.SnapRevisionType, map[string]string{ + "snap-sha3-384": snapSHA3_384, + }) + if err != nil { + return nil, err + } + + snapRev := a.(*asserts.SnapRevision) + + if snapRev.SnapSize() != snapSize { + return nil, fmt.Errorf("snap %q does not have expected size according to signatures (broken or tampered): %d != %d", snapPath, snapSize, snapRev.SnapSize()) + } + + snapID := snapRev.SnapID() + + snapDecl, err := findSnapDeclaration(snapID, snapPath, db) + if err != nil { + return nil, err + } + + name := snapDecl.SnapName() + + return &snap.SideInfo{ + RealName: name, + SnapID: snapID, + Revision: snap.R(snapRev.SnapRevision()), + }, nil +} + +// FetchSnapAssertions fetches the assertions matching the snap file digest using the given fetcher. +func FetchSnapAssertions(f asserts.Fetcher, snapSHA3_384 string) error { + // for now starting from the snap-revision will get us all other relevant assertions + ref := &asserts.Ref{ + Type: asserts.SnapRevisionType, + PrimaryKey: []string{snapSHA3_384}, + } + + return f.Fetch(ref) +} + +// FetchSnapDeclaration fetches the snap declaration and its prerequisites for the given snap id using the given fetcher. +func FetchSnapDeclaration(f asserts.Fetcher, snapID string) error { + ref := &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{release.Series, snapID}, + } + + return f.Fetch(ref) +} diff --git a/asserts/snapasserts/snapasserts_test.go b/asserts/snapasserts/snapasserts_test.go new file mode 100644 index 00000000..6acc8212 --- /dev/null +++ b/asserts/snapasserts/snapasserts_test.go @@ -0,0 +1,317 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package snapasserts_test + +import ( + "crypto" + "fmt" + "io/ioutil" + "path/filepath" + "testing" + "time" + + "golang.org/x/crypto/sha3" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/snapasserts" + "github.com/snapcore/snapd/snap" +) + +func TestSnapasserts(t *testing.T) { TestingT(t) } + +type snapassertsSuite struct { + storeSigning *assertstest.StoreStack + dev1Acct *asserts.Account + + localDB *asserts.Database +} + +var _ = Suite(&snapassertsSuite{}) + +func (s *snapassertsSuite) SetUpTest(c *C) { + rootPrivKey, _ := assertstest.GenerateKey(1024) + storePrivKey, _ := assertstest.GenerateKey(752) + s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey) + + s.dev1Acct = assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + + localDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: s.storeSigning.Trusted, + }) + c.Assert(err, IsNil) + + s.localDB = localDB + + // add in prereqs assertions + err = s.localDB.Add(s.storeSigning.StoreAccountKey("")) + c.Assert(err, IsNil) + err = s.localDB.Add(s.dev1Acct) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) +} + +func fakeSnap(rev int) []byte { + fake := fmt.Sprintf("hsqs________________%d", rev) + return []byte(fake) +} + +func fakeHash(rev int) []byte { + h := sha3.Sum384(fakeSnap(rev)) + return h[:] +} + +func makeDigest(rev int) string { + d, err := asserts.EncodeDigest(crypto.SHA3_384, fakeHash(rev)) + if err != nil { + panic(err) + } + return string(d) +} + +func (s *snapassertsSuite) TestCrossCheckHappy(c *C) { + digest := makeDigest(12) + size := uint64(len(fakeSnap(12))) + headers := map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "12", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + // everything cross checks + err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB) + c.Check(err, IsNil) +} + +func (s *snapassertsSuite) TestCrossCheckErrors(c *C) { + digest := makeDigest(12) + size := uint64(len(fakeSnap(12))) + headers := map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "12", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + // different size + err = snapasserts.CrossCheck("foo", digest, size+1, si, s.localDB) + c.Check(err, ErrorMatches, fmt.Sprintf(`snap "foo" file does not have expected size according to signatures \(download is broken or tampered\): %d != %d`, size+1, size)) + + // mismatched revision vs what we got from store original info + err = snapasserts.CrossCheck("foo", digest, size, &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(21), + }, s.localDB) + c.Check(err, ErrorMatches, `snap "foo" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 21 / snap-id-1 != 12 / snap-id-1`) + + // mismatched snap id vs what we got from store original info + err = snapasserts.CrossCheck("foo", digest, size, &snap.SideInfo{ + SnapID: "snap-id-other", + Revision: snap.R(12), + }, s.localDB) + c.Check(err, ErrorMatches, `snap "foo" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 12 / snap-id-other != 12 / snap-id-1`) + + // changed name + err = snapasserts.CrossCheck("baz", digest, size, si, s.localDB) + c.Check(err, ErrorMatches, `cannot install snap "baz" that is undergoing a rename to "foo"`) + +} + +func (s *snapassertsSuite) TestCrossCheckRevokedSnapDecl(c *C) { + // revoked snap declaration (snap-name=="") ! + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "1", + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + digest := makeDigest(12) + size := uint64(len(fakeSnap(12))) + headers = map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "12", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB) + c.Check(err, ErrorMatches, `cannot install snap "foo" with a revoked snap declaration`) +} + +func (s *snapassertsSuite) TestDeriveSideInfoHappy(c *C) { + digest := makeDigest(42) + size := uint64(len(fakeSnap(42))) + headers := map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "42", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + tempdir := c.MkDir() + snapPath := filepath.Join(tempdir, "anon.snap") + err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644) + c.Assert(err, IsNil) + + si, err := snapasserts.DeriveSideInfo(snapPath, s.localDB) + c.Assert(err, IsNil) + c.Check(si, DeepEquals, &snap.SideInfo{ + RealName: "foo", + SnapID: "snap-id-1", + Revision: snap.R(42), + Channel: "", + }) +} + +func (s *snapassertsSuite) TestDeriveSideInfoNoSignatures(c *C) { + tempdir := c.MkDir() + snapPath := filepath.Join(tempdir, "anon.snap") + err := ioutil.WriteFile(snapPath, fakeSnap(42), 0644) + c.Assert(err, IsNil) + + _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB) + // cannot find signatures with metadata for snap + c.Assert(err, Equals, asserts.ErrNotFound) +} + +func (s *snapassertsSuite) TestDeriveSideInfoSizeMismatch(c *C) { + digest := makeDigest(42) + size := uint64(len(fakeSnap(42))) + headers := map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size+5), // broken + "snap-revision": "42", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + tempdir := c.MkDir() + snapPath := filepath.Join(tempdir, "anon.snap") + err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644) + c.Assert(err, IsNil) + + _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB) + c.Check(err, ErrorMatches, fmt.Sprintf(`snap %q does not have expected size according to signatures \(broken or tampered\): %d != %d`, snapPath, size, size+5)) +} + +func (s *snapassertsSuite) TestDeriveSideInfoRevokedSnapDecl(c *C) { + // revoked snap declaration (snap-name=="") ! + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "1", + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + digest := makeDigest(42) + size := uint64(len(fakeSnap(42))) + headers = map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "42", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + tempdir := c.MkDir() + snapPath := filepath.Join(tempdir, "anon.snap") + err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644) + c.Assert(err, IsNil) + + _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB) + c.Check(err, ErrorMatches, fmt.Sprintf(`cannot install snap %q with a revoked snap declaration`, snapPath)) +} diff --git a/asserts/sysdb/staging.go b/asserts/sysdb/staging.go new file mode 100644 index 00000000..05eeee32 --- /dev/null +++ b/asserts/sysdb/staging.go @@ -0,0 +1,94 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build withtestkeys withstagingkeys + +/* + * Copyright (C) 2016 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 . + * + */ + +package sysdb + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" +) + +const ( + encodedStagingTrustedAccount = `type: account +authority-id: canonical +account-id: canonical +display-name: Canonical +timestamp: 2016-04-01T00:00:00.0Z +username: canonical +validation: certified +sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu + +AcLBXAQAAQoABgUCV640ggAKCRAHKljtl9kuLrQtEADBji8VwAuislurkFORTmcXV/DOkvyvAYEN +mB/MLniK4MlLX+RDncDBmF38IK9SRkxbwwJuKgvsjwsYJ3w1P7SGvVfNyU2hLRFtdxDMVC7+A9g3 +N1VW9W+IOWmYeBgXiveqAlSJ9GUvLQiBgUWRBkbyAT6aLkSZrTSjxGRGW/uoNfjj+CbAR4HGbRnn +IOxDuQyw6rOXQZKfZvkD1NiH+0QzXLv0RivE8+V5uVN+ooUFRoVQmqbj7orvPS9iTY5AMVjCgfo0 +UiWiN6NyCfDBDz0bZhIZlBU4JF5W0I/sEwsuYCxIhFi5uPNmQXqqb5d9Y3bsxIUdMR0+pai1A3eI +HQmYX12wCnb276R5Adz4iol19oKAR2Qf3VJBvPccdIFU7Qu5FOOihQdMRxULBBXGn1HQF1uW+ue3 +ZQ3x6e8s3XjdDQE/kHCDUkmzhbk1SErgndg6Q1ipKJ+4G6dOc16s66bSFA4QzW53Y40NP0HRWxe2 +tK9VOJ+z9GvGYp5H1ZXbbs78t9bUwL7L6z/eXM6BRho6YY9X7nImpByIkdcV47dCyVFol6NrM5NS +NSpdtRStGqo7tjPaBf86p2vLOAbwFUuaE3rwf5g/agz4S/v5G5E2tKmfQs6vGYrfVtlOzr8gEoXH ++/hOEC3wYEJjpXmFRjUjJwr0Fbej2TpoITpfzbySpg== +` + encodedStagingRootAccountKey = `type: account-key +authority-id: canonical +revision: 3 +public-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu +account-id: canonical +name: staging-root +since: 2016-04-01T00:00:00.0Z +body-length: 717 +sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu + +AcbBTQRWhcGAARAA4wh+b9nyRdZj9gNKuHz8BTNZsLOVv2VJseHBoMNc4aA8EgmLwMF/aP+q1tAQ +VOeynhfSecIK/2aWKKX+dmU/rfAbnbdHX1NT8OnG2z3qdYdqw1EreN8LcY4DBDfa1RNKcjFvBu+Q +jxpU289m1yUjjc7yHie84BoYRgDl0icar8KF7vKx44wNhzbca+lw4xGSA5gpDZ1i1smdxdpOSsUY +WT70ZcJBN1oyKiiiCJUNLwCPzaPsH1i3WwDSaGsbjl8gjf2+LFNFPwdsWRbn3RLlFcFbET2bFe5y +v6UN+0cSh9qLJeLR2h0WDaVBp5Gx4PAYAfpIIF8EH3YbvI8uuTmBza8Ni0yozOZ2cXCSdezLGW2m +b6itOq/taBhgl8gzhKqki9jAOWmDBeBIbe2rUuNJrfHVH8+lWTzuzJIcHSHeAjFG1xid+HOOsw0e +Ag3JMjJaqCGCp0Oc9/WBtHV6jB30jLzht5QjJZ6izIKswRrvt0nCowp74FZ1l1ekXZPhhkA5MBMb +AoTiz9UvRZAWBPa5gX4R7eaekGjCPWI8NpJ7pT3Xh3NIHIsjyf0JcysoH2V1+A9qT1LOCyczf1Uc +9d8PXap1zhhQuczZcnw7vAwLEIwldfp08x6klsgiP6jqIB4XKJCjBDu/gn682ydWzfLT8echVHpg +uI62X67Ns1ZbFWMAEQEAAQ== + +AcLBXAQAAQoABgUCV86jSgAKCRAHKljtl9kuLpV6EADO8Q1WKJwoTfeIpBpQfDhdhqJLmW86Qrjq +P9ZsndN8eA4uSbo08yg9jxi6Q3J/A5QK6rhTz5Nu41frKVpgFr80BpIx8cHsY2dZNyKCm70Jjy4h +cxteK7mwdAzdWG/Dg7Nr4fhOmpepsh1gIXvjWhTkT226DIO6l45o6N2hMKKkWmqJYqVD6i7UE4Ed +xmC+IoluhnKGGwM6JpyOw0RViXbLjVDR58n4q1xmK7cFduMoLKszVY4/KGmKT8gA6D4pUOa62F84 +Ejh6akRum7uqygBibYT/DP+KA+MhHvpQ8XZem7IVIEnMJr7U2gde3brbVr0oiCl7FzfiBNy6qw92 +cTsE8o3JV0Lc106SWU28GuWPgyXjoH8imzSmWlpQtlPlKEDwMQt31XDKUKp0ZKiEax3cQ6VjMv1f +PV3bHNjD+tBq5e1xm/UWyGu7J2N4VPLgUK7F4TPUJk5lwKjmII8KD3KA/IeHnZVN6vmC2nKfhGvw ++rJllQQ0IWY9RfIdzFHpVvthe48g27ok5yEgovAc/s7xWZ6CBgyzYWLQMNFvENj04CzGvxirKwuJ +Fy5UJIEKB0j0R2qnCz6HZkyQrUsz5HiIIlks18FfOZwuIc4GGPbwwQBoXW7a6KQg0aa62BPj5Iww +3w60rtTSUsjINkZ/GXLodfzPglOl6VLF7bWx2hGesg== +` +) + +func init() { + stagingTrustedAccount, err := asserts.Decode([]byte(encodedStagingTrustedAccount)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + stagingRootAccountKey, err := asserts.Decode([]byte(encodedStagingRootAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + trustedStagingAssertions = []asserts.Assertion{stagingTrustedAccount, stagingRootAccountKey} +} diff --git a/asserts/sysdb/sysdb.go b/asserts/sysdb/sysdb.go new file mode 100644 index 00000000..3806e614 --- /dev/null +++ b/asserts/sysdb/sysdb.go @@ -0,0 +1,48 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +// Package sysdb supports the system-wide assertion database with ways to open it and to manage the trusted set of assertions founding it. +package sysdb + +import ( + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/dirs" +) + +func openDatabaseAt(path string, cfg *asserts.DatabaseConfig) (*asserts.Database, error) { + bs, err := asserts.OpenFSBackstore(path) + if err != nil { + return nil, err + } + keypairMgr, err := asserts.OpenFSKeypairManager(path) + if err != nil { + return nil, err + } + cfg.Backstore = bs + cfg.KeypairManager = keypairMgr + return asserts.OpenDatabase(cfg) +} + +// Open opens the system-wide assertion database with the trusted assertions set configured. +func Open() (*asserts.Database, error) { + cfg := &asserts.DatabaseConfig{ + Trusted: Trusted(), + } + return openDatabaseAt(dirs.SnapAssertsDBDir, cfg) +} diff --git a/asserts/sysdb/sysdb_test.go b/asserts/sysdb/sysdb_test.go new file mode 100644 index 00000000..18e912f4 --- /dev/null +++ b/asserts/sysdb/sysdb_test.go @@ -0,0 +1,147 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package sysdb_test + +import ( + "os" + "path/filepath" + "syscall" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/sysdb" +) + +func TestSysDB(t *testing.T) { TestingT(t) } + +type sysDBSuite struct { + extraTrusted []asserts.Assertion + probeAssert asserts.Assertion +} + +var _ = Suite(&sysDBSuite{}) + +func (sdbs *sysDBSuite) SetUpTest(c *C) { + tmpdir := c.MkDir() + + pk, _ := assertstest.GenerateKey(752) + + signingDB := assertstest.NewSigningDB("can0nical", pk) + + trustedAcct := assertstest.NewAccount(signingDB, "can0nical", map[string]interface{}{ + "account-id": "can0nical", + "validation": "certified", + "timestamp": "2015-11-20T15:04:00Z", + }, "") + + trustedAccKey := assertstest.NewAccountKey(signingDB, trustedAcct, map[string]interface{}{ + "account-id": "can0nical", + "since": "2015-11-20T15:04:00Z", + "until": "2500-11-20T15:04:00Z", + }, pk.PublicKey(), "") + + sdbs.extraTrusted = []asserts.Assertion{trustedAcct, trustedAccKey} + + fakeRoot := filepath.Join(tmpdir, "root") + err := os.Mkdir(fakeRoot, os.ModePerm) + c.Assert(err, IsNil) + dirs.SetRootDir(fakeRoot) + + sdbs.probeAssert = assertstest.NewAccount(signingDB, "probe", nil, "") +} + +func (sdbs *sysDBSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") +} + +func (sdbs *sysDBSuite) TestTrusted(c *C) { + trusted := sysdb.Trusted() + c.Check(trusted, HasLen, 2) + + restore := sysdb.InjectTrusted(sdbs.extraTrusted) + defer restore() + + trustedEx := sysdb.Trusted() + c.Check(trustedEx, HasLen, 4) +} + +func (sdbs *sysDBSuite) TestOpenSysDatabase(c *C) { + db, err := sysdb.Open() + c.Assert(err, IsNil) + c.Check(db, NotNil) + + // check trusted + _, err = db.Find(asserts.AccountKeyType, map[string]string{ + "account-id": "canonical", + "public-key-sha3-384": "-CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk", + }) + c.Assert(err, IsNil) + + trustedAcc, err := db.Find(asserts.AccountType, map[string]string{ + "account-id": "canonical", + }) + c.Assert(err, IsNil) + + err = db.Check(trustedAcc) + c.Check(err, IsNil) + + // extraneous + err = db.Check(sdbs.probeAssert) + c.Check(err, ErrorMatches, "no matching public key.*") +} + +func (sdbs *sysDBSuite) TestOpenSysDatabaseExtras(c *C) { + restore := sysdb.InjectTrusted(sdbs.extraTrusted) + defer restore() + + db, err := sysdb.Open() + c.Assert(err, IsNil) + c.Check(db, NotNil) + + err = db.Check(sdbs.probeAssert) + c.Check(err, IsNil) +} + +func (sdbs *sysDBSuite) TestOpenSysDatabaseBackstoreOpenFail(c *C) { + // make it not world-writeable + oldUmask := syscall.Umask(0) + os.MkdirAll(filepath.Join(dirs.SnapAssertsDBDir, "asserts-v0"), 0777) + syscall.Umask(oldUmask) + + db, err := sysdb.Open() + c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*") + c.Check(db, IsNil) +} + +func (sdbs *sysDBSuite) TestOpenSysDatabaseKeypairManagerOpenFail(c *C) { + // make it not world-writeable + oldUmask := syscall.Umask(0) + os.MkdirAll(filepath.Join(dirs.SnapAssertsDBDir, "private-keys-v1"), 0777) + syscall.Umask(oldUmask) + + db, err := sysdb.Open() + c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*") + c.Check(db, IsNil) +} diff --git a/asserts/sysdb/testkeys.go b/asserts/sysdb/testkeys.go new file mode 100644 index 00000000..7b615645 --- /dev/null +++ b/asserts/sysdb/testkeys.go @@ -0,0 +1,30 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build withtestkeys + +/* + * Copyright (C) 2016 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 . + * + */ + +package sysdb + +import ( + "github.com/snapcore/snapd/asserts/systestkeys" +) + +// init will inject the test trusted assertions when this module build tag "withtestkeys" is defined. +func init() { + InjectTrusted(systestkeys.Trusted) +} diff --git a/asserts/sysdb/trusted.go b/asserts/sysdb/trusted.go new file mode 100644 index 00000000..f623f269 --- /dev/null +++ b/asserts/sysdb/trusted.go @@ -0,0 +1,153 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package sysdb + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/osutil" +) + +const ( + encodedCanonicalAccount = `type: account +authority-id: canonical +account-id: canonical +display-name: Canonical +timestamp: 2016-04-01T00:00:00.0Z +username: canonical +validation: certified +sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk + +AcLDXAQAAQoABgUCV7UYzwAKCRDUpVvql9g3IK7uH/4udqNOurx5WYVknzXdwekp0ovHCQJ0iBPw +TSFxEVr9faZSzb7eqJ1WicHsShf97PYS3ClRYAiluFsjRA8Y03kkSVJHjC+sIwGFubsnkmgflt6D +WEmYIl0UBmeaEDS8uY4Xvp9NsLTzNEj2kvzy/52gKaTc1ZSl5RDL9ppMav+0V9iBYpiDPBWH2rJ+ +aDSD8Rkyygm0UscfAKyDKH4lrvZ0WkYyi1YVNPrjQ/AtBySh6Q4iJ3LifzKa9woIyAuJET/4/FPY +oirqHAfuvNod36yNQIyNqEc20AvTvZNH0PSsg4rq3DLjIPzv5KbJO9lhsasNJK1OdL6x8Yqrdsbk +ldZp4qkzfjV7VOMQKaadfcZPRaVVeJWOBnBiaukzkhoNlQi1sdCdkBB/AJHZF8QXw6c7vPDcfnCV +1lW7ddQ2p8IsJbT6LzpJu3GW/P4xhNgCjtCJ1AJm9a9RqLwQYgdLZwwDa9iCRtqTbRXBlfy3apps +1VjbQ3h5iCd0hNfwDBnGVm1rhLKHCD1DUdNE43oN2ZlE7XGyh0HFV6vKlpqoW3eoXCIxWu+HBY96 ++LSl/jQgCkb0nxYyzEYK4Reb31D0mYw1Nji5W+MIF5E09+DYZoOT0UvR05YMwMEOeSdI/hLWg/5P +k+GDK+/KopMmpd4D1+jjtF7ZvqDpmAV98jJGB2F88RyVb4gcjmFFyTi4Kv6vzz/oLpbm0qrizC0W +HLGDN/ymGA5sHzEgEx7U540vz/q9VX60FKqL2YZr/DcyY9GKX5kCG4sNqIIHbcJneZ4frM99oVDu +7Jv+DIx/Di6D1ULXol2XjxbbJLKHFtHksR97ceaFvcZwTogC61IYUBJCvvMoqdXAWMhEXCr0QfQ5 +Xbi31XW2d4/lF/zWlAkRnGTzufIXFni7+nEuOK0SQEzO3/WaRedK1SGOOtTDjB8/3OJeW96AUYK5 +oTIynkYkEyHWMNCXALg+WQW6L4/YO7aUjZ97zOWIugd7Xy63aT3r/EHafqaY2nacOhLfkeKZ830b +o/ezjoZQAxbh6ce7JnXRgE9ELxjdAhBTpGjmmmN2sYrJ7zP9bOgly0BnEPXGSQfFA+NNNw1FADx1 +MUY8q9DBjmVtgqY+1KGTV5X8KvQCBMODZIf/XJPHdCRAHxMd8COypcwgL2vDIIXpOFbi1J/B0GF+ +eklxk9wzBA8AecBMCwCzIRHDNpD1oa2we38bVFrOug6e/VId1k1jYFJjiLyLCDmV8IMYwEllHSXp +LQAdm3xZ7t4WnxYC8YSCk9mXf3CZg59SpmnV5Q5Z6A5Pl7Nc3sj7hcsMBZEsOMPzNC9dPsBnZvjs +WpPUffJzEdhHBFhvYMuD4Vqj6ejUv9l3oTrjQWVC +` + + encodedCanonicalRootAccountKey = `type: account-key +authority-id: canonical +revision: 2 +public-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk +account-id: canonical +name: root +since: 2016-04-01T00:00:00.0Z +body-length: 1406 +sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk + +AcbDTQRWhcGAASAA4Zdo3CVpKmTecjd3VDBiFbZTKKhcG0UV3FXxyGIe2UsdnJIks4NkVYO+qYk0 +zW26Svpa5OIOJGO2NcgN9bpCYWZOufO1xTmC7jW/fEtqJpX8Kcq20+X5AarqJ5RBVnGLrlz+ZT99 +aHdRZ4YQ2XUZvhbelzWTdK5+2eMSXNrFjO6WwGh9NRekE/NIBNwvULAtJ5nv1KwZaSpZ+klJrstU +EHPhs+NGGm1Aru01FFl3cWUm5Ao8i9y+pFcPoaRatgtpYU8mg9gP594lvyJqjFofXvHPwztmySqf +FVAp4gLLfLvRxbXkOfPUz8guidqvg6r4DUD+kCBjKYoT44PjK6l51MzEL2IEy6jdnFTgjHbaYML8 +/5NpuPu8XiSjCpOTeNR+XKzXC2tHRU7j09Xd44vKRhPk0Hc4XsPNBWqfrcbdWmwsFhjfxFDJajOq +hzWVoiRc5opB5socbRjLf+gYtncxe99oC2FDA2FcftlFoyztho0bAzeFer1IHJIMYWxKMESjvJUE +pnMMKpIMYY0QfWEo5hXR0TaT+NxW2Z9Jqclgyw13y5iY72ZparHS66J+C7dxCEOswlw1ypNic6MM +/OzpafIQ10yAT3HeRCJQQOOSSTaold+WpWsQweYCywPcu9S+wCo6CrPzJCCIxOAnXjLYv2ykTJje +pNJ2+GZ1WH2UeJdJ5sR8fpxxRupqHuEKNRZ+2CqLmFC5kHNszoGolLEvGcK4BJciO4KihnKtxrdX +dUJIOPBLktA8XiiHSOmLzs2CFjcvlDuPSpe64HIL5yCxO1/GRux4A1Kht1+DqTrL7DjyIW+vIPro +A1PQwkcAJyScNRxT4bPpUj8geAXWd3n212W+7QVHuQEFezvXC5GbMyR+Xj47FOFcFcSZID1hTZEu +uMD+AxaBHQKwPfBx1arVKE1OhkuKHeSFtZRP8K8l3qj5W0sIxxIW19W8aziu8ZeDMT+nIEJrJvhx +zGEdxwCrp3k2/93oDV7g+nb1ZGfIhtmcrKziijghzPLaYaiM9LggqwTARelk3xSzd8+uk3LPXuVl +fP8/xHApss6sCE3xk4+F3OGbL7HbGuCnoulf795XKLRTy+xU/78piOMNJJQu+G0lMZIO3cZrP6io +MYDa+jDZw4V4fBRWce/FA3Ot1eIDxCq5v+vfKw+HfUlWcjm6VUQIFZYbK+Lzj6mpXn81BugG3d+M +0WNFObXIrUbhnKcYkus3TSJ9M1oMEIMp0WfFGAVTd61u36fdi2e+/xbLN0kbYcFRZwd9CmtEeDZ0 +eYx/pvKKaNz/DfUr0piVCRwxuxQ0kVppklHPO4sOTFZUId8KLHg28LbszvupSsHP/nHlW8l5/VK6 +4+KxRV2XofsUnwARAQAB + +AcLDXAQAAQoABgUCV83kkgAKCRDUpVvql9g3IA9hIADAkn4VXnJIFblhMSBe6hbTy7z6AfOhZxXR +Ds/mHsiWfFT6ifGi9SpZowhRX+ff57YvFCjlBqMYLKYE0NsFQYEUc5uBWiFZwC0ENydNhO23DV1B +elTSs6mr9duPm1eJAozFrQETOD1kz5BIamqBUeaTczjM+9l5i485Ffknbc+EaGOrtMEap0GqjByQ +u+ykZGvryVQ447avgjvFsMtA0quFi+SoW9PT/9D26e5rD7RIICYWG8mzFRn5Isqs/X4W1uAiKQe9 +pqHMbdNr/FCWX5ws0/nMaOq+b0z4EIIXIfT0JmIlFDQsAgFVnKwYw+zs32cTw4XuzvMhgMDtCowD +YodhiO/5AOMsMMV0qBsYxbIPJIEz7b6gwTYEJoTVkqTit6o3UgWrAy+p4Y7t0ickYIHgwiuKRS9E +fu0Ue+32NFp0XFqZElfXLK/U2yjto+fJXu6uAELsXesfFGIOp/nbRbNavUt9jAJeO7ftQczgf39T +YfA0OKerP5gAOd4+aO3gATPUjfWPsJ9908XC7QqK2BwS1kh/fMrd95mxcmXdF1bBElszKwaToBVQ +1m52EYp06kkPyOu+fGKFAoIMafcV/2Ztz1WMo/Vp0iP/r0WAtBDw6sDJyWOfRjUEvP7BBdEzraHV +VblbSrKzhYeEGdMDi6kFC+KEzfPDPFJX1l3saPBkz9VDuESbktyObQp9VfkFKYBgBnw3msQJk+6k +G4t0o3/DZ7qz/kTJXMogG26Z/FsMhPERsaLTbWRJ3WRyXX8COaTladSf8bG0Oib19outnjuvpjQ0 +qEV9eeGRBlx9mbidSYH95cj0zD2DKpeSZ83M5K1pFg+8RKToGElGTTk8vtdTfDVbmi3+QntfLq+z +ZMgs2+SmCWrV/MPC04Dl00CXywdKPyf6toomqRP7A5fS7W8P9fdPn+a8JCblcleGj9nvJXBQjue7 +97rofCEszhKhoE9fMCIUcSoTU9YAm5Jr+qclSEbV1pzwTvZ8auMIXtzEZV5n4aK4WPDV+lYCadrL +DlvJSJRuXRvIMbmvU9b8NxgG8AS88BkX3L9vlOpkMculwG1/iooQvxuFaJDargt370wAQo0lCpG3 +MxnsSusymwnYegvvvr7Xp/KBLZK1+8Djzm3fwAryp4qNo29ciVw3O9lFKmmuiIcxSY0bauXaK6kv +pTnYkmx7XGPF7Ahb7Ov0/0FE2Lx3JZXSEKeW+VrCcpYQOY++t67b+jf0AV4rZExcLFJzP6MPMimP +ZCd383NzlzkXK+vAdvTi40HPiM9FYOp6g8JTs5TTdx2/qs/SWFC8AkahIQmH0IpFBJep2JKl2kyr +FZMvASkHA9bR/UuXDvbMzsUmT/xnERZosQaZgFEO +` +) + +var ( + trustedAssertions []asserts.Assertion + trustedStagingAssertions []asserts.Assertion + trustedExtraAssertions []asserts.Assertion +) + +func init() { + canonicalAccount, err := asserts.Decode([]byte(encodedCanonicalAccount)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + canonicalRootAccountKey, err := asserts.Decode([]byte(encodedCanonicalRootAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + trustedAssertions = []asserts.Assertion{canonicalAccount, canonicalRootAccountKey} +} + +// Trusted returns a copy of the current set of trusted assertions as used by Open. +func Trusted() []asserts.Assertion { + trusted := []asserts.Assertion(nil) + if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + trusted = append(trusted, trustedAssertions...) + } else { + trusted = append(trusted, trustedStagingAssertions...) + } + trusted = append(trusted, trustedExtraAssertions...) + return trusted +} + +// InjectTrusted injects further assertions into the trusted set for Open. +// Returns a restore function to reinstate the previous set. Useful +// for tests or called globally without worrying about restoring. +func InjectTrusted(extra []asserts.Assertion) (restore func()) { + prev := trustedExtraAssertions + trustedExtraAssertions = make([]asserts.Assertion, len(prev)+len(extra)) + copy(trustedExtraAssertions, prev) + copy(trustedExtraAssertions[len(prev):], extra) + return func() { + trustedExtraAssertions = prev + } +} diff --git a/asserts/systestkeys/trusted.go b/asserts/systestkeys/trusted.go new file mode 100644 index 00000000..eeb2f2a6 --- /dev/null +++ b/asserts/systestkeys/trusted.go @@ -0,0 +1,263 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +// Package systestkeys defines trusted assertions and keys to use in tests. +package systestkeys + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" +) + +const ( + TestRootPrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQcYBAAAAAEBEADx0Loc/418zmw2AIcf5uxC/hgshHyCU98n4cRfJph007X6gXJf +ifHsKlXlSa5NizsM9WlOgCI3eyekF088q7lQTORDo4YO5x/ZtmcAiePtbMrAac4D +9j+5Ax24jJ4VniYudQ1wX4x7wtXRpL+lCER0FS5HEQ6L3OW/SntfVtSzoshRO5u7 +r6yYW1t0EE04P7Squ+N/sK+xJytOxCzC2/BwugHgZf3jArpFCuWSZgk9QVmqR1a3 +tynSKrx35OzxSdPyyBa4XOQwKAEquK1Lv/njmYTwATR+zIUa3n7SNyOCz0sOTmBE +7sSCgUtc+wQF2It1Wazs4YDA8YbTTB8VgveGjg8J8qr6YfSQ6BQDKeUnvHwwJH3Z +5YSL/KUdeI7SOdFjxSy62szvp4s3jWJSVr/qPkNyxfFAH/HOViRR21e1iufov8NO +yeLFyW7eiA/OU8QXJXG/S9YiCQotZePYlFG3a6p7crfdO90XQf6bqydlNK2ftVje +J/1+/LHXj60qHXq5x1BrXPMmhMpOphZf0H5l8Q0YolSeFM/THsKbqWDcRQZrL9vm +GwDgMGipKG5/83SNUuiN2HGLcKT8ME2WoIPTPLi7O+KeNf5vhrL4soETc3XkCx8S +RYjDMj7U50OU5Zao7EmQzqWtDmFFDV8dmgKIaMduN4TVEgU7ZMDDa2nJRwARAQAB +AA/+PAQDZRYR/iNXXRHFd6f/BGN/CXF6W3hIfuP8MmdoWDqBRGKjSc35UpVxSx59 +2bYQGlfAYqDPnTh+Lq4wVs0CCcmDr7vilklLsOOh7dLLVI53RckcvgP8bcU1t6uC +wrfFHyujAbxdKAxDuCvs+p8yKiNloHK9yv2wscjhFNj+onToxayHKs5fhlLKQGSZ +XbgF9Yf7XyIxgMTJbVuoBlbC9p9bvt9hY1m2dFNPhgW4DlFtWSMqhR87DHPZ4eHZ +4srhhTSe2vQHGGKdY4aBUDcd5JyiD1UlO8Ez2ebV0AOqVxlutebC4ujlscQ4OaP9 +LBxCBIaUshgHthtbzI5sepDOMMYJKV0R0+gtW6+rrVaudeSdt62yLF6a8n5m41dP +6OxGmO84ejoyw/EMutrVeraoz2b5bb35gx9bLEMRFr8XL2x1Ckdx2epNTL9aOVmA +JiCMGC0zFyt/jbNXnoOjD8tzUj44jrJnY2PcnJHgDogXMoIRduPDnwYaQtXkffkW +zsVbdUHvMkZuKXUBfsxCwFYgGm2i9y0dGnTSzI03TevRJ1FM2+TN8uQ8h4/C0xfZ +snXgvVHAwAOJwE8onul8AiepE1ihSWmaQfq/2Hn+0u+wbIsdrpP9xKB88KvZtgVe +mXj1vbDHw1nbORH63vgzfT8tyIhvR1RfDutQoGKkrZ4ZCIkIAPgDABPYucbnUpv/ +e2OSKd+Z/RGwUqghtp6recs3+9IdIoz/XPQHr9eqmgMUSikRFHLD6s0unIUm1b5s +Q+98OvadsP0D5EaKjAo0Za2PQVi8Na3eoGDs+DpX2+lhq5lvYCezGNoo50awKhzs +vRE4RU91bohfNvfJ9bY0AwyrYHDg67Jl/JzWtPNBqfAMlRW5WM9NYvp+Brk8JJLU ++Ncf5w//7S4lH5qBf3rXk6ur8ittIq28MGalW7T8Uk2F7VkrvCDaKkWPP8jwux79 +u1F22ADPYbdHB2RUSv0FGPrOItUyl81V6qTpAqO8iYQVol+B0J95B7Z0DLa+QecH +vVfaVS8IAPmaokwf3mk36dmbHvDIaPjloD1Gw3PCPZ+dpmGLfvcPm4YcA/uTzbNV +E46QlTZCny8+5W4xDaetpdODXRvCciwnjJ/wcdpSaMe0R5Res8weIcV2RAM9UNNb +q6BiTDqyBwk/dmFYY71xus/tuAnxmhZnXrJYjcA1CEsO+cu3SkwYM6dp3d1W0Bfh +li4b6eT3bC7IRD+KW+3Vdti8bShoLUkK2UwXHhnz0yBBE+8vQc8PoxOwt29EcQDf +GGL1Tz31yxRF+EADH4SL5ypUZFUctLkJ76WP9vNHqx5Tzrbt2aHqqbtvkxfzcB/m +k6cm8XzLVxttNHvZkvjwtvl76+X8d2kH/34hjWibosJueZb7HoFuJIoXXtPJ+sY5 +MSnY9+uGW4FgzgyUjWd5bfBCcCOGIqJFj37YVJwPKXaXBr0CzgaeJfLNRqz9Mt6d +OyqYLdb4ojvFSvhfN7bjAiBbwTbGVsOVVKgiNYudWH5lBS9yqxKyDQeUmwSmgaWa +Y1zMmK7J/syCqMBlizox3NIjGUsV7JGHzatSGksblTdTHTts3D52yTphonZueYVz +f27546ta7Fk9uEts8XVrs8YiJgZw8DHEugmuD5ZFb5WrpF96jqpaAuEhUye0fkfA +GvRP9FpVShfxVockrCrLgCaaDs+/kg7cZS+PDU8uLlXnsKqXvkkH7ip/irQOICh0 +ZXN0cm9vdG9yZymJAjgEEwECACIFAgAAAAECGy8GCwkIBwMCBhUIAgkKCwQWAgMB +Ah4BAheAAAoJEExxmnn3gXGkIyAQAMmpCPsk3FjfH2wHMxDozPZJmgoPwFBj4VEi +Qg4pp1pWtTHWPm7qN2bUL0WaJkvdPvvana7T5iGSlQHAjQRgPQfS42+0Nz17AInR +QbpovdE3S/02UOWaF+VgFrF7IKHQhbxbfmjPBQAr/9mWfe/JGyUqlc14a8IwxOmf +k4qf3WVj48NI6PdtMYpBKtSpghc7rKQwFLyxEauoBtoF6VLyhha7TFBGGM3LJ5uU +SPr8oVCybkZ9xbWdfcodbe3Ix/gbG1rvX7Jp/pIlG+7DVKn/0xkR7zPPfDmZOBGd +VFdg9X8L9+QH00Rverp0cCZ+fN97W13/Mb2/E9Px0y86Omwyhg5SVbikemmybrK8 +JHelbZ2NMmN7YHq2TB1idii30aX/1PN9jGyHHFMWPj2BJmK2aWhN0QSX8sxCoS9O +NCXwYU5hfRX5RjyWnI51XDhhfpMikqXnLrxzmPme4htaIqMl332MiqusFZ0D6UVw +Br2jeRhncvRrsscvAibbUWgbN6u70xBGjZZksvT8vkBipkikXWJ8SPm5DBfbRe85 +NnAkj2flf8ZFtNwrCy93JPVqY7j4Ip5AHUqhlUhYyPEMlcPEiNIhqZFUZvMYAIRL +68Hgqm/HlvtVLR/P7H6mDd7XhVFT5Qxz3f+AD+hmQFf8NN4MDbhCxjkUBsq+eyGG +97WP6Yv2 +=gJ0v +-----END PGP PRIVATE KEY BLOCK----- +` + + encodedTestRootAccount = `type: account +authority-id: testrootorg +account-id: testrootorg +display-name: Testrootorg +timestamp: 2016-08-11T18:30:57+02:00 +username: testrootorg +validation: certified +sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR + +AcLBUgQAAQoABgUCV6yoQQAAelEQAEdSECpdmV5a2G5VMBzJFuHQUU1FzgZ7gPQjc3l0BibDWm8O +rDi7IT3L80OkqS2AoQgHS5KtEvKqEmhyfcdzcXgvCkHR5kucRBJJaPy8z6gGMhzZIPlc+EqY+Cvb +/MQPLvtYYvtAxq1vWz+aDGGwk2Z/dFUG+wofvNWodz400gYTZeFOCZwStBD84S7iY/3pMQgC3+SO +QMr/VI+bgmOukFqZL0cX4ReiuUs2W45V6EC81UGBjk+k7AVTEXMR1Xo8f0yiRzlLoEdKQMCOC45Q +n4eedjCToGRPFcktM0QhgfbcpPIQKHNqKGGvtQQXvW5PIZ7AS4rTfQScXTn1dqDsL/ZVdasvOpCP +5o4WvoWMoU8+Hm4n6ckw4sXn//PZIQrtnkp2DO+9JXXZasIPg4k1mvUQ5Kb9qCcBbaM+OO1izOoC +3PY8xHNQNfHNHwBMewhnU2NpdTS0mTepN/8iFsDT1vSZ28OE2hgbu1ltqx4AsRkCVyFFx6N6OYm2 +UDNozU9K5w0NY4u9HSTDz4KrBIalAaKY72CIUqeVsmAcYatXglbj7dVTZTw75M0v1thQiSoKFqHw +CHykZ6BJRgminY1FqOg7tvqTwzYM7lwaE3K8JpAyzie7v+OSLSxy1vlwUmT2lT+h1i28/w+r+R3Q +C0QC8xuHSvOv3YRtzKna3smAfRlB +` + + TestRootKeyID = "hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR" + + encodedTestRootAccountKey = `type: account-key +authority-id: testrootorg +public-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR +account-id: testrootorg +name: test-root +since: 2016-08-11T18:30:57+02:00 +body-length: 717 +sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR + +AcbBTQRWhcGAARAA8dC6HP+NfM5sNgCHH+bsQv4YLIR8glPfJ+HEXyaYdNO1+oFyX4nx7CpV5Umu +TYs7DPVpToAiN3snpBdPPKu5UEzkQ6OGDucf2bZnAInj7WzKwGnOA/Y/uQMduIyeFZ4mLnUNcF+M +e8LV0aS/pQhEdBUuRxEOi9zlv0p7X1bUs6LIUTubu6+smFtbdBBNOD+0qrvjf7CvsScrTsQswtvw +cLoB4GX94wK6RQrlkmYJPUFZqkdWt7cp0iq8d+Ts8UnT8sgWuFzkMCgBKritS7/545mE8AE0fsyF +Gt5+0jcjgs9LDk5gRO7EgoFLXPsEBdiLdVms7OGAwPGG00wfFYL3ho4PCfKq+mH0kOgUAynlJ7x8 +MCR92eWEi/ylHXiO0jnRY8UsutrM76eLN41iUla/6j5DcsXxQB/xzlYkUdtXtYrn6L/DTsnixclu +3ogPzlPEFyVxv0vWIgkKLWXj2JRRt2uqe3K33TvdF0H+m6snZTStn7VY3if9fvyx14+tKh16ucdQ +a1zzJoTKTqYWX9B+ZfENGKJUnhTP0x7Cm6lg3EUGay/b5hsA4DBoqShuf/N0jVLojdhxi3Ck/DBN +lqCD0zy4uzvinjX+b4ay+LKBE3N15AsfEkWIwzI+1OdDlOWWqOxJkM6lrQ5hRQ1fHZoCiGjHbjeE +1RIFO2TAw2tpyUcAEQEAAQ== + +AcLBXAQAAQoABgUCV8656QAKCRBMcZp594FxpNWlEADQgBlROdBTHpdZ3/9BbasxenUC3VXusMeK +0DmnsHrsAsyVk6xiHQQ3hWxvXKWoDkDsOhUqcQTsDBcIaZ18+qwpQciyItd+w3d7SSJ+MKSUpwsB +NOdgw1ykj7l1M/W7xAAPscFoV1xVSk9+rsLYFYDe23R+ecyotSmF+4QHj5b+hXeVIOUaqQTl5xPC +h0zVYNIUWv42q4Z+hiBS8+8UJ0G+7z/27XORkGHY6TXCt0aph7s5egr8Lm+/jq7c95HVsa7DwSpv +SqPajRnlyLiHFXUYAUPEU9oDgPwtLsqUkFfrv1WZ3ja1rDexgKBta+8BRyCAq3gPcMAjhiHXdjoW +90p893l9N6K82RiEOO9ic0pEezjQldg97oU+ajXNm3ryns+HX6hRd39rpzIsrbVdbCqun4RwMbCM +EVxgC/cuxMGcS40Co3O8wG3H/WIWOqcRQfolQTexmyzQljYt9WyWJdXmtPtaMzQGbOqE/dIjOK9j +xvrghVU4kX6fJFwPi+azMrluHV+WGSVxPCuLW8o2aipjOd1/bUQCL5OwRuaEWuLCiV01J8H/JjWV +hL4gGVqEM2KEPIDwY2yqX36jE7uN9O+mIPnS4Tdj0JQ5ZD1qh34wv+4QvhgNeyP120nuS1ykO9X0 +A806uPC5QK1+cgRMUz8zJ0afDNwE/DvpBQvE5CIi9A== +` + + TestStorePrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQcYBAAAAAEBEACYmqZm+xLnwg1Oz5RD6N+jzfq8FLm2RT+GTtzSG5l7dKjaBz2R +om+OSOFnqDTT+QaiJ3DeLZaR0wSn4m29T1m196782f86qRJzcCnUoCaovg6WU9Ug +jwfr3DbOq+aj49yofRK8cBUSg4LZOhc/TAQecBmxtW7noAqvCkcOmk8Qi9pLqCWu +wRfUBek54wdktVG1+wEHp2Ute66VrVStIAtEUISNe2peo62jlWj0LynreUsHLX2J +/Pg6uJYAYGpm8V0i2ajxUg9dIN2AwwcGW7YxI0kdV+jrrKlu6izlCzo+VUBEAIsm +DOCmUjmwNvNe1XHk71DxgmPPg19TRY5Zg9a+YA1cN4w2LFaha+6LFi+xdobHqZ0P +seH+CLymuRCZnuDFbUwQ5X0lOECpiOOzZrIZUPvcQjawpjFXASDeIlOhD9wTPc7Z +TUd2ZiNB9EMmJfcYQ8Fde20Ots8zjZIcSWi6V2Yn4+QkMt2QaYDznFhSgQod0QUi +SMVK1BzI7kKTI1k3tIeIAjADgOkYyYUnbqZqpXMm6Iu+JyuLYVH+wlpIDbg3wdsa +d7eBJLtatJBL6Mp7chk9XLrg0Kga+taj8e9N6qwh+KEo8SlebxBW2M2G2RWfdF0h +SA5o1bIB+dnh1bVNUgBN744cPDZM3IiZOMTTHvmcvoHX9Guf71U/1LCG/wARAQAB +AA//R+eWwK9NGSa2XowwsE7qEaTcoAKj/t5iMEa4hce7ahBt/02qFRUUu1Zb3xvC +yJ5uIbmz1PxmFg/4AaMPUkQxYSxzp3CQcnN33izbiPRtQtVKykp2AgFjGh+JM5iL +9G1Ja5qDWYb2ZuLQpMpaadjHmA/6C2IR/9HJNvEAykCrQIClO0DfgJg7QgwG+N+g +fDNzbOv4cELPyb6dZKlnXKvcozPNQV0FodI93vZnnacbeXiNgbRNktc/n2uaQlMr +z5Wq7ODiWdLwqlDyDdnXVYehMUYPDWR+u41/yGNPBB1mNDi3L1OSPTuUHspfpEhA +JE8ue1DIMwPdQ8oDAJmlmUglxpP1dnR3Q3XqUbsJMT6kAdqc4OSXF+L+E9j7EiA1 +UaXiiK+srj/GWFFdKlSf1JLYX3kOvrH/M1xMB6cmUshuWDfiJUGz9rPhPOIAvK11 ++Gog6kV+0JJXBe7oWEf8oewONLg7KtU1sSlHeuECpR+Pi652wXnAMeeHFjeCirp+ +jRPla+oKhrYMfLxk+x2YgMK4usoY6Q/KNTcHNs/FeRpzt50OFIaRbKL/I/CY1pB8 +oakl45D0+c38+6MZVkbPwDRN5ixUJfHwSBwl5qFyF3abP/N0gJVsdfPO0QyDbihm +1yo5Tvihd7aUkfTAF+E2BkZLIfuY5kREENxY/EHceST20gEIAMOjPOwYkN+V25o+ +MSIj9EBq9xEMpddHilpVXNkRHF2i89CFCUCKcIGe7wROvrqxQSqVrEDET4ZU6iqB +zsaA5RD4Fia3+eoZjvy4563H54XX2Wp89Qs2T0PREems5UMoeho/kCzSKdnYhhll +kbekWEqZAOzyCaBjzu7YowjrcUuceUbiDSsh6ds4/goS4h1AO/oroYawZQhvUfaf +W7ExpOsxuFa7S4N7mLywpeGaWcOuZt3r/EfM4gHpJaEntgqhjfiEtEkfO4dGKiAU ++hg+LmVPyBjQnVhK5NXSBc/zXaXOWqrVEkqTEQcZ5WsmpcB9hzqZIaFw9cAF4PKh +xm1ZOnkIAMewViBcogHUEzzn9ZxTXKi45po45g5qxsoifNlN3ZfShdrxOjXjYos2 +UujGfN+gZN8vV4bnD3Q6CbpioBT7lTZhweZVRwx/eQa/yQv20ZewL/CJduME8DZj +rQtyy4MRBhaNf3A8Gvx/CXJZaIHYfldRJYIrq9OuK4ael3Zf0uZwm9AleT5baFz8 +T8iRlojlzhT2+xi+Y/yLCCYFESkxgdXPkhUfYkh/O5NPWxSXnohDgKAtKj4gDe2c +Qs/zUI5Q+p8qucWbcbASZurDthTD80G6zGYNWX0e/6k45k/tatf0zJGLZVww02uc +Kq6MVafir1FzkOPxq41zmie8zPTe7zcIAL4m/lnWww+jPxM+LffdtgDqOeRxjgo6 +MV3576MqUakeIGVfnlW7SJCyjN2mnf0JbzrVgv7XxEcZIJrIePutMqdKm1YAt2YR +1TuU/rsKpUQt+d8t9rWfCYd1xeSn6IdNtoBaMeu6vI13pV1dghPAnQyovUK0xzI6 +seLeVhTU3wG9zZHJBycyE8PDTqE3awEetYLGFkz6DruIjYwylYRPZwSC1xpPcirf +nkSAeE2U9nmnxDWUQNhWzFTazYr7QQAUzghX3Mf2ZYeoDBBqDg9lQMy2oUJrJtfv +vqmejP39c3+fJiXlT2k2o0V6B8aZTNVaRn00E3hE+e1Obaa1lV1EWxaDcrQUICh0 +ZXN0cm9vdG9yZyBzdG9yZSmJAjgEEwECACIFAgAAAAECGy8GCwkIBwMCBhUIAgkK +CwQWAgMBAh4BAheAAAoJEN2glF+93m+NRIEP/2AxZS9tmJ6l7oltpYTEhAQdytAE +eqahcBYIARSTgvy3YJlOzdKdIoYsGogVvNZ7ashaFCpQtNaNezI7Mhz5cuVoHyYl +hEctEXSeTNUmxNekdksoBm2QHfnxFHbKLV4Kvj7dlvMhNVbpaMe/qI1SykddGBvh +woEp2HnHe3lGhlU84+XopEijphI8BXQ2so8bA0jEcuDJOAEXtVzj14miP6nZCsDD +EKHriukohhCQQUZVm0VOKLfdoi4QuAWbehBmlrhcvRDLvcr6p7jY00803jvaGBjD +XmS0DT51tNg6W2COQ5xlM9+hjK5n6nyZdT/OYeu+TqtdnpHcZxsF7qKsUBbKeQtA +Abh0wqtD58Kqp9UTovMVho/+/VEH9+gpfpvrieQvjrpZki2ZVnEhqlINOVwCYH0j +wC5qKcFeUmHHGhE1ShMKypZvLgqfc0soK8vaz+njN4IYrsWaI0iCQmr6FfV7Q8Ih +XAcSt/73baWnQsiBWWgl+FOxChDfwEWZaGFgtzyjexLpbi1V+Usuwd0+pX3U/+A6 +uXw5t77PXE4nW73a8EDM2nkG5ru+KswmOC0G7ULB2Cs9UOWqN+XChdii+VC68MMK +O0gyQlMQf+OPtU18Nff7hfKGY1ZCUbCwvb/+bHBvzpjmtWEuIOwPC0CBgU9G9FcX +o7ZSZ/h/bUY1EjE2 +=Nc2M +-----END PGP PRIVATE KEY BLOCK----- +` + + TestStoreKeyID = "XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y" + + encodedTestStoreAccountKey = `type: account-key +authority-id: testrootorg +public-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y +account-id: testrootorg +name: test-store +since: 2016-08-11T18:42:22+02:00 +body-length: 717 +sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR + +AcbBTQRWhcGAARAAmJqmZvsS58INTs+UQ+jfo836vBS5tkU/hk7c0huZe3So2gc9kaJvjkjhZ6g0 +0/kGoidw3i2WkdMEp+JtvU9Ztfeu/Nn/OqkSc3Ap1KAmqL4OllPVII8H69w2zqvmo+PcqH0SvHAV +EoOC2ToXP0wEHnAZsbVu56AKrwpHDppPEIvaS6glrsEX1AXpOeMHZLVRtfsBB6dlLXuula1UrSAL +RFCEjXtqXqOto5Vo9C8p63lLBy19ifz4OriWAGBqZvFdItmo8VIPXSDdgMMHBlu2MSNJHVfo66yp +buos5Qs6PlVARACLJgzgplI5sDbzXtVx5O9Q8YJjz4NfU0WOWYPWvmANXDeMNixWoWvuixYvsXaG +x6mdD7Hh/gi8prkQmZ7gxW1MEOV9JThAqYjjs2ayGVD73EI2sKYxVwEg3iJToQ/cEz3O2U1HdmYj +QfRDJiX3GEPBXXttDrbPM42SHElouldmJ+PkJDLdkGmA85xYUoEKHdEFIkjFStQcyO5CkyNZN7SH +iAIwA4DpGMmFJ26maqVzJuiLvicri2FR/sJaSA24N8HbGne3gSS7WrSQS+jKe3IZPVy64NCoGvrW +o/HvTeqsIfihKPEpXm8QVtjNhtkVn3RdIUgOaNWyAfnZ4dW1TVIATe+OHDw2TNyImTjE0x75nL6B +1/Rrn+9VP9Swhv8AEQEAAQ== + +AcLBXAQAAQoABgUCV866kwAKCRBMcZp594FxpHWHD/9AaZXqyT/Zsmq/VzmAMpd9JvCH4PHQKtAP +bXfP2Dnpa2wk2wuzQuSWunR8NDRyVh/aNVeTEZ9dFm/B8LR+U2O4rsHmFSeicmsTmo9u/HouRdEU +zeSc6cbAxMPpfNSjr5J+URLjGRT6oX5fEBmRPx/OC9pEIScMx7uKmTKEnuyMzLRNN/6HiGWKrFCo +nJdKkwRXrkCHyXWAOv1GumT7NDuyFcjAqt/UdHliTZkDBImKOsBmBVXMUjg7HCSS2uq/5WjStJ+B +JHQ4GSsXBvVINs6BncNWcvV6mCQ73D57MzGhqo997Zb4tSrn7UNGWK7GLCzV3e/pFlG7pw6HbgnQ ++rxU2Oj/TPVw0tcnUiRl2ttKpm+nua0Cl+MD+Gx0KXLAVp0ZGOQ9yGyP9AePFzcOR8SlRIgxi0EI +iJkSeYilqoKo3AJhnICRiqvAca2TGJoiJUryEgZ8jbTOElfaF2p+y0xvXGlWbKZm1gzGyvFM5fV5 +hJTlp/am+2uVn6U8wPACir4PrbuXYo7L4MIXww2OEO0ruBIaLARbc5IutSWmw6AEYQUxtsa9bdHV +Zin7LGbEj6lZm8GycWQwh4B6Vnt6dJRIyPc/9G7uM8Ds/2Wa7+yAxhiPqm8DwlbOYh1npw4X4TLD +IMGnTv5N3zllI+Xz4rqJzNTzEbvOIcrqWxCedQe79A== +` +) + +var ( + TestRootAccount asserts.Assertion + TestRootAccountKey asserts.Assertion + // here for convenience, does not need to be in the trusted set + TestStoreAccountKey asserts.Assertion + // Testing-only trusted assertions for injecting in the the system trusted set. + Trusted []asserts.Assertion +) + +func init() { + acct, err := asserts.Decode([]byte(encodedTestRootAccount)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + accKey, err := asserts.Decode([]byte(encodedTestRootAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + storeAccKey, err := asserts.Decode([]byte(encodedTestStoreAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode test store assertion: %v", err)) + } + + TestRootAccount = acct + TestRootAccountKey = accKey + TestStoreAccountKey = storeAccKey + Trusted = []asserts.Assertion{TestRootAccount, TestRootAccountKey} +} diff --git a/asserts/user.go b/asserts/user.go new file mode 100644 index 00000000..25e66487 --- /dev/null +++ b/asserts/user.go @@ -0,0 +1,263 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts + +import ( + "fmt" + "net/mail" + "regexp" + "strconv" + "strings" + "time" +) + +var validSystemUserUsernames = regexp.MustCompile(`^[a-z0-9][-a-z0-9+.-_]*$`) + +// SystemUser holds a system-user assertion which allows creating local +// system users. +type SystemUser struct { + assertionBase + series []string + models []string + sshKeys []string + since time.Time + until time.Time +} + +// BrandID returns the brand identifier that signed this assertion. +func (su *SystemUser) BrandID() string { + return su.HeaderString("brand-id") +} + +// Email returns the email address that this assertion is valid for. +func (su *SystemUser) Email() string { + return su.HeaderString("email") +} + +// Series returns the series that this assertion is valid for. +func (su *SystemUser) Series() []string { + return su.series +} + +// Models returns the models that this assertion is valid for. +func (su *SystemUser) Models() []string { + return su.models +} + +// Name returns the full name of the user (e.g. Random Guy). +func (su *SystemUser) Name() string { + return su.HeaderString("name") +} + +// Username returns the system user name that should be created (e.g. "foo"). +func (su *SystemUser) Username() string { + return su.HeaderString("username") +} + +// Password returns the crypt(3) compatible password for the user. +// Note that only ID: $6$ or stronger is supported (sha512crypt). +func (su *SystemUser) Password() string { + return su.HeaderString("password") +} + +// SSHKeys returns the ssh keys for the user. +func (su *SystemUser) SSHKeys() []string { + return su.sshKeys +} + +// Since returns the time since the assertion is valid. +func (su *SystemUser) Since() time.Time { + return su.since +} + +// Until returns the time until the assertion is valid. +func (su *SystemUser) Until() time.Time { + return su.until +} + +// ValidAt returns whether the system-user is valid at 'when' time. +func (su *SystemUser) ValidAt(when time.Time) bool { + valid := when.After(su.since) || when.Equal(su.since) + if valid { + valid = when.Before(su.until) + } + return valid +} + +// Implement further consistency checks. +func (su *SystemUser) checkConsistency(db RODatabase, acck *AccountKey) error { + // Do the cross-checks when this assertion is actually used, + // i.e. in the create-user code. See also Model.checkConsitency + + return nil +} + +// sanity +var _ consistencyChecker = (*SystemUser)(nil) + +type shadow struct { + ID string + Rounds string + Salt string + Hash string +} + +// crypt(3) compatible hashes have the forms: +// - $id$salt$hash +// - $id$rounds=N$salt$hash +func parseShadowLine(line string) (*shadow, error) { + l := strings.SplitN(line, "$", 5) + if len(l) != 4 && len(l) != 5 { + return nil, fmt.Errorf(`hashed password must be of the form "$integer-id$salt$hash", see crypt(3)`) + } + + // if rounds is the second field, the line must consist of 4 + if strings.HasPrefix(l[2], "rounds=") && len(l) == 4 { + return nil, fmt.Errorf(`missing hash field`) + } + + // shadow line without $rounds=N$ + if len(l) == 4 { + return &shadow{ + ID: l[1], + Salt: l[2], + Hash: l[3], + }, nil + } + // shadow line with rounds + return &shadow{ + ID: l[1], + Rounds: l[2], + Salt: l[3], + Hash: l[4], + }, nil +} + +func checkHashedPassword(headers map[string]interface{}, name string) (string, error) { + pw, err := checkOptionalString(headers, name) + if err != nil { + return "", err + } + // the pw string is optional, so just return if its empty + if pw == "" { + return "", nil + } + + // parse the shadow line + shd, err := parseShadowLine(pw) + if err != nil { + return "", fmt.Errorf(`%q header invalid: %s`, name, err) + } + + // and verify it + + // see crypt(3), ID 6 means SHA-512 (since glibc 2.7) + ID, err := strconv.Atoi(shd.ID) + if err != nil { + return "", fmt.Errorf(`%q header must start with "$integer-id$", got %q`, name, shd.ID) + } + // double check that we only allow modern hashes + if ID < 6 { + return "", fmt.Errorf("%q header only supports $id$ values of 6 (sha512crypt) or higher", name) + } + + // the $rounds=N$ part is optional + if strings.HasPrefix(shd.Rounds, "rounds=") { + rounds, err := strconv.Atoi(strings.SplitN(shd.Rounds, "=", 2)[1]) + if err != nil { + return "", fmt.Errorf("%q header has invalid number of rounds: %s", name, err) + } + if rounds < 5000 || rounds > 999999999 { + return "", fmt.Errorf("%q header rounds parameter out of bounds: %d", name, rounds) + } + } + + // see crypt(3) for the legal chars + validSaltAndHash := regexp.MustCompile(`^[a-zA-Z0-9./]+$`) + if !validSaltAndHash.MatchString(shd.Salt) { + return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt) + } + if !validSaltAndHash.MatchString(shd.Hash) { + return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash) + } + + return pw, nil +} + +func assembleSystemUser(assert assertionBase) (Assertion, error) { + // brand-id here can be different from authority-id, + // the code using the assertion must use the policy set + // by the model assertion system-user-authority header + email, err := checkNotEmptyString(assert.headers, "email") + if err != nil { + return nil, err + } + if _, err := mail.ParseAddress(email); err != nil { + return nil, fmt.Errorf(`"email" header must be a RFC 5322 compliant email address: %s`, err) + } + + series, err := checkStringList(assert.headers, "series") + if err != nil { + return nil, err + } + models, err := checkStringList(assert.headers, "models") + if err != nil { + return nil, err + } + if _, err := checkOptionalString(assert.headers, "name"); err != nil { + return nil, err + } + if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil { + return nil, err + } + if _, err := checkHashedPassword(assert.headers, "password"); err != nil { + return nil, err + } + + sshKeys, err := checkStringList(assert.headers, "ssh-keys") + if err != nil { + return nil, err + } + since, err := checkRFC3339Date(assert.headers, "since") + if err != nil { + return nil, err + } + until, err := checkRFC3339Date(assert.headers, "until") + if err != nil { + return nil, err + } + if until.Before(since) { + return nil, fmt.Errorf("'until' time cannot be before 'since' time") + } + + // "global" system-user assertion can only be valid for 1y + if len(models) == 0 && until.After(since.AddDate(1, 0, 0)) { + return nil, fmt.Errorf("'until' time cannot be more than 365 days in the future when no models are specified") + } + + return &SystemUser{ + assertionBase: assert, + series: series, + models: models, + sshKeys: sshKeys, + since: since, + until: until, + }, nil +} diff --git a/asserts/user_test.go b/asserts/user_test.go new file mode 100644 index 00000000..a2d86246 --- /dev/null +++ b/asserts/user_test.go @@ -0,0 +1,200 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package asserts_test + +import ( + "fmt" + "strings" + "time" + + "github.com/snapcore/snapd/asserts" + . "gopkg.in/check.v1" +) + +var ( + _ = Suite(&systemUserSuite{}) +) + +type systemUserSuite struct { + until time.Time + untilLine string + since time.Time + sinceLine string + + modelsLine string + + systemUserStr string +} + +const systemUserExample = "type: system-user\n" + + "authority-id: canonical\n" + + "brand-id: canonical\n" + + "email: foo@example.com\n" + + "series:\n" + + " - 16\n" + + "MODELSLINE\n" + + "name: Nice Guy\n" + + "username: guy\n" + + "password: $6$salt$hash\n" + + "ssh-keys:\n" + + " - ssh-rsa AAAABcdefg\n" + + "SINCELINE\n" + + "UNTILLINE\n" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + +func (s *systemUserSuite) SetUpTest(c *C) { + s.since = time.Now().Truncate(time.Second) + s.sinceLine = fmt.Sprintf("since: %s\n", s.since.Format(time.RFC3339)) + s.until = time.Now().AddDate(0, 1, 0).Truncate(time.Second) + s.untilLine = fmt.Sprintf("until: %s\n", s.until.Format(time.RFC3339)) + s.modelsLine = "models:\n - frobinator\n" + s.systemUserStr = strings.Replace(systemUserExample, "UNTILLINE\n", s.untilLine, 1) + s.systemUserStr = strings.Replace(s.systemUserStr, "SINCELINE\n", s.sinceLine, 1) + s.systemUserStr = strings.Replace(s.systemUserStr, "MODELSLINE\n", s.modelsLine, 1) +} + +func (s *systemUserSuite) TestDecodeOK(c *C) { + a, err := asserts.Decode([]byte(s.systemUserStr)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SystemUserType) + systemUser := a.(*asserts.SystemUser) + c.Check(systemUser.BrandID(), Equals, "canonical") + c.Check(systemUser.Email(), Equals, "foo@example.com") + c.Check(systemUser.Series(), DeepEquals, []string{"16"}) + c.Check(systemUser.Models(), DeepEquals, []string{"frobinator"}) + c.Check(systemUser.Name(), Equals, "Nice Guy") + c.Check(systemUser.Username(), Equals, "guy") + c.Check(systemUser.Password(), Equals, "$6$salt$hash") + c.Check(systemUser.SSHKeys(), DeepEquals, []string{"ssh-rsa AAAABcdefg"}) + c.Check(systemUser.Since().Equal(s.since), Equals, true) + c.Check(systemUser.Until().Equal(s.until), Equals, true) +} + +func (s *systemUserSuite) TestDecodePasswd(c *C) { + validTests := []struct{ original, valid string }{ + {"password: $6$salt$hash\n", "password: $6$rounds=9999$salt$hash\n"}, + {"password: $6$salt$hash\n", ""}, + } + for _, test := range validTests { + valid := strings.Replace(s.systemUserStr, test.original, test.valid, 1) + _, err := asserts.Decode([]byte(valid)) + c.Check(err, IsNil) + } +} + +func (s *systemUserSuite) TestValidAt(c *C) { + a, err := asserts.Decode([]byte(s.systemUserStr)) + c.Assert(err, IsNil) + su := a.(*asserts.SystemUser) + + c.Check(su.ValidAt(su.Since()), Equals, true) + c.Check(su.ValidAt(su.Since().AddDate(0, 0, -1)), Equals, false) + c.Check(su.ValidAt(su.Since().AddDate(0, 0, 1)), Equals, true) + + c.Check(su.ValidAt(su.Until()), Equals, false) + c.Check(su.ValidAt(su.Until().AddDate(0, -1, 0)), Equals, true) + c.Check(su.ValidAt(su.Until().AddDate(0, 1, 0)), Equals, false) +} + +func (s *systemUserSuite) TestValidAtRevoked(c *C) { + // With since == until, i.e. system-user has been revoked. + revoked := strings.Replace(s.systemUserStr, s.sinceLine, fmt.Sprintf("since: %s\n", s.until.Format(time.RFC3339)), 1) + a, err := asserts.Decode([]byte(revoked)) + c.Assert(err, IsNil) + su := a.(*asserts.SystemUser) + + c.Check(su.ValidAt(su.Since()), Equals, false) + c.Check(su.ValidAt(su.Since().AddDate(0, 0, -1)), Equals, false) + c.Check(su.ValidAt(su.Since().AddDate(0, 0, 1)), Equals, false) + + c.Check(su.ValidAt(su.Until()), Equals, false) + c.Check(su.ValidAt(su.Until().AddDate(0, -1, 0)), Equals, false) + c.Check(su.ValidAt(su.Until().AddDate(0, 1, 0)), Equals, false) +} + +const ( + systemUserErrPrefix = "assertion system-user: " +) + +func (s *systemUserSuite) TestDecodeInvalid(c *C) { + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"brand-id: canonical\n", "", `"brand-id" header is mandatory`}, + {"brand-id: canonical\n", "brand-id: \n", `"brand-id" header should not be empty`}, + {"email: foo@example.com\n", "", `"email" header is mandatory`}, + {"email: foo@example.com\n", "email: \n", `"email" header should not be empty`}, + {"email: foo@example.com\n", "email: \n", `"email" header must be a RFC 5322 compliant email address: mail: missing @ in addr-spec`}, + {"email: foo@example.com\n", "email: no-mail\n", `"email" header must be a RFC 5322 compliant email address:.*`}, + {"series:\n - 16\n", "series: \n", `"series" header must be a list of strings`}, + {"series:\n - 16\n", "series: something\n", `"series" header must be a list of strings`}, + {"models:\n - frobinator\n", "models: \n", `"models" header must be a list of strings`}, + {"models:\n - frobinator\n", "models: something\n", `"models" header must be a list of strings`}, + {"ssh-keys:\n - ssh-rsa AAAABcdefg\n", "ssh-keys: \n", `"ssh-keys" header must be a list of strings`}, + {"ssh-keys:\n - ssh-rsa AAAABcdefg\n", "ssh-keys: something\n", `"ssh-keys" header must be a list of strings`}, + {"name: Nice Guy\n", "name:\n - foo\n", `"name" header must be a string`}, + {"username: guy\n", "username:\n - foo\n", `"username" header must be a string`}, + {"username: guy\n", "username: bäää\n", `"username" header contains invalid characters: "bäää"`}, + {"username: guy\n", "", `"username" header is mandatory`}, + {"password: $6$salt$hash\n", "password:\n - foo\n", `"password" header must be a string`}, + {"password: $6$salt$hash\n", "password: cleartext\n", `"password" header invalid: hashed password must be of the form "\$integer-id\$salt\$hash", see crypt\(3\)`}, + {"password: $6$salt$hash\n", "password: $ni!$salt$hash\n", `"password" header must start with "\$integer-id\$", got "ni!"`}, + {"password: $6$salt$hash\n", "password: $3$salt$hash\n", `"password" header only supports \$id\$ values of 6 \(sha512crypt\) or higher`}, + {"password: $6$salt$hash\n", "password: $7$invalid-salt$hash\n", `"password" header has invalid chars in salt "invalid-salt"`}, + {"password: $6$salt$hash\n", "password: $8$salt$invalid-hash\n", `"password" header has invalid chars in hash "invalid-hash"`}, + {"password: $6$salt$hash\n", "password: $8$rounds=9999$hash\n", `"password" header invalid: missing hash field`}, + {"password: $6$salt$hash\n", "password: $8$rounds=xxx$salt$hash\n", `"password" header has invalid number of rounds:.*`}, + {"password: $6$salt$hash\n", "password: $8$rounds=1$salt$hash\n", `"password" header rounds parameter out of bounds: 1`}, + {"password: $6$salt$hash\n", "password: $8$rounds=1999999999$salt$hash\n", `"password" header rounds parameter out of bounds: 1999999999`}, + {s.sinceLine, "since: \n", `"since" header should not be empty`}, + {s.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`}, + {s.untilLine, "until: \n", `"until" header should not be empty`}, + {s.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`}, + {s.untilLine, "until: 1002-11-01T22:08:41+00:00\n", `'until' time cannot be before 'since' time`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(s.systemUserStr, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, systemUserErrPrefix+test.expectedErr) + } +} + +func (s *systemUserSuite) TestUntilNoModels(c *C) { + // no models is good for <1y + su := strings.Replace(s.systemUserStr, s.modelsLine, "", -1) + _, err := asserts.Decode([]byte(su)) + c.Check(err, IsNil) + + // but invalid for more than one year + oneYearPlusOne := time.Now().AddDate(1, 0, 1).Truncate(time.Second) + su = strings.Replace(su, s.untilLine, fmt.Sprintf("until: %s\n", oneYearPlusOne.Format(time.RFC3339)), -1) + _, err = asserts.Decode([]byte(su)) + c.Check(err, ErrorMatches, systemUserErrPrefix+"'until' time cannot be more than 365 days in the future when no models are specified") +} + +func (s *systemUserSuite) TestUntilWithModels(c *C) { + // with models it can be valid forever + oneYearPlusOne := time.Now().AddDate(10, 0, 1).Truncate(time.Second) + su := strings.Replace(s.systemUserStr, s.untilLine, fmt.Sprintf("until: %s\n", oneYearPlusOne.Format(time.RFC3339)), -1) + _, err := asserts.Decode([]byte(su)) + c.Check(err, IsNil) +} diff --git a/boot/boottest/mockbootloader.go b/boot/boottest/mockbootloader.go new file mode 100644 index 00000000..47de7ee0 --- /dev/null +++ b/boot/boottest/mockbootloader.go @@ -0,0 +1,72 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package boottest + +import ( + "path/filepath" +) + +// MockBootloader mocks the bootloader interface and records all +// set/get calls. +type MockBootloader struct { + BootVars map[string]string + SetErr error + GetErr error + + name string + bootdir string +} + +func NewMockBootloader(name, bootdir string) *MockBootloader { + return &MockBootloader{ + name: name, + bootdir: bootdir, + + BootVars: make(map[string]string), + } +} + +func (b *MockBootloader) SetBootVars(values map[string]string) error { + for k, v := range values { + b.BootVars[k] = v + } + return b.SetErr +} + +func (b *MockBootloader) GetBootVars(keys ...string) (map[string]string, error) { + out := map[string]string{} + for _, k := range keys { + out[k] = b.BootVars[k] + } + + return out, b.GetErr +} + +func (b *MockBootloader) Dir() string { + return b.bootdir +} + +func (b *MockBootloader) Name() string { + return b.name +} + +func (b *MockBootloader) ConfigFile() string { + return filepath.Join(b.bootdir, "mockboot/mockboot.cfg") +} diff --git a/boot/kernel_os.go b/boot/kernel_os.go new file mode 100644 index 00000000..c1e3f74e --- /dev/null +++ b/boot/kernel_os.go @@ -0,0 +1,203 @@ +// -*- 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 . + * + */ + +package boot + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/partition" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" +) + +// RemoveKernelAssets removes the unpacked kernel/initrd for the given +// kernel snap. +func RemoveKernelAssets(s snap.PlaceInfo) error { + bootloader, err := partition.FindBootloader() + if err != nil { + return fmt.Errorf("no not remove kernel assets: %s", err) + } + + // remove the kernel blob + blobName := filepath.Base(s.MountFile()) + dstDir := filepath.Join(bootloader.Dir(), blobName) + if err := os.RemoveAll(dstDir); err != nil { + return err + } + + return nil +} + +func copyAll(src, dst string) error { + if output, err := exec.Command("cp", "-aLv", src, dst).CombinedOutput(); err != nil { + return fmt.Errorf("cannot copy %q -> %q: %s (%s)", src, dst, err, output) + } + return nil +} + +// ExtractKernelAssets extracts kernel/initrd/dtb data from the given +// kernel snap, if required, to a versioned bootloader directory so +// that the bootloader can use it. +func ExtractKernelAssets(s *snap.Info, snapf snap.Container) error { + if s.Type != snap.TypeKernel { + return fmt.Errorf("cannot extract kernel assets from snap type %q", s.Type) + } + + bootloader, err := partition.FindBootloader() + if err != nil { + return fmt.Errorf("cannot extract kernel assets: %s", err) + } + + if bootloader.Name() == "grub" { + return nil + } + + // now do the kernel specific bits + blobName := filepath.Base(s.MountFile()) + dstDir := filepath.Join(bootloader.Dir(), blobName) + if err := os.MkdirAll(dstDir, 0755); err != nil { + return err + } + dir, err := os.Open(dstDir) + if err != nil { + return err + } + defer dir.Close() + + for _, src := range []string{"kernel.img", "initrd.img"} { + if err := snapf.Unpack(src, dstDir); err != nil { + return err + } + if err := dir.Sync(); err != nil { + return err + } + } + if err := snapf.Unpack("dtbs/*", dstDir); err != nil { + return err + } + + return dir.Sync() +} + +// SetNextBoot will schedule the given OS or kernel snap to be used in +// the next boot +func SetNextBoot(s *snap.Info) error { + if release.OnClassic { + return nil + } + if s.Type != snap.TypeOS && s.Type != snap.TypeKernel { + return nil + } + + bootloader, err := partition.FindBootloader() + if err != nil { + return fmt.Errorf("cannot set next boot: %s", err) + } + + var nextBoot, goodBoot string + switch s.Type { + case snap.TypeOS: + nextBoot = "snap_try_core" + goodBoot = "snap_core" + case snap.TypeKernel: + nextBoot = "snap_try_kernel" + goodBoot = "snap_kernel" + } + blobName := filepath.Base(s.MountFile()) + + // check if we actually need to do anything, i.e. the exact same + // kernel/core revision got installed again (e.g. firstboot) + m, err := bootloader.GetBootVars(goodBoot) + if err != nil { + return err + } + if m[goodBoot] == blobName { + return nil + } + + return bootloader.SetBootVars(map[string]string{ + nextBoot: blobName, + "snap_mode": "try", + }) +} + +// KernelOrOsRebootRequired returns whether a reboot is required to swith to the given OS or kernel snap. +func KernelOrOsRebootRequired(s *snap.Info) bool { + if s.Type != snap.TypeKernel && s.Type != snap.TypeOS { + return false + } + + bootloader, err := partition.FindBootloader() + if err != nil { + logger.Noticef("cannot get boot settings: %s", err) + return false + } + + var nextBoot, goodBoot string + switch s.Type { + case snap.TypeKernel: + nextBoot = "snap_try_kernel" + goodBoot = "snap_kernel" + case snap.TypeOS: + nextBoot = "snap_try_core" + goodBoot = "snap_core" + } + + m, err := bootloader.GetBootVars(nextBoot, goodBoot) + if err != nil { + return false + } + + squashfsName := filepath.Base(s.MountFile()) + if m[nextBoot] == squashfsName && m[goodBoot] != m[nextBoot] { + return true + } + + return false +} + +// InUse checks if the given name/revision is used in the +// boot environment +func InUse(name string, rev snap.Revision) bool { + bootloader, err := partition.FindBootloader() + if err != nil { + logger.Noticef("cannot get boot settings: %s", err) + return false + } + + bootVars, err := bootloader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_core", "snap_try_core") + if err != nil { + logger.Noticef("cannot get boot vars: %s", err) + return false + } + + snapFile := filepath.Base(snap.MountFile(name, rev)) + for _, bootVar := range bootVars { + if bootVar == snapFile { + return true + } + } + + return false +} diff --git a/boot/kernel_os_test.go b/boot/kernel_os_test.go new file mode 100644 index 00000000..d37ca865 --- /dev/null +++ b/boot/kernel_os_test.go @@ -0,0 +1,251 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package boot_test + +import ( + "io/ioutil" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/partition" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" +) + +func TestBoot(t *testing.T) { TestingT(t) } + +type kernelOSSuite struct { + bootloader *boottest.MockBootloader +} + +var _ = Suite(&kernelOSSuite{}) + +func (s *kernelOSSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) + s.bootloader = boottest.NewMockBootloader("mock", c.MkDir()) + partition.ForceBootloader(s.bootloader) +} + +func (s *kernelOSSuite) TearDownTest(c *C) { + dirs.SetRootDir("") + partition.ForceBootloader(nil) +} + +const packageKernel = ` +name: ubuntu-kernel +version: 4.0-1 +type: kernel +vendor: Someone +` + +func (s *kernelOSSuite) TestExtractKernelAssetsAndRemove(c *C) { + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"dtbs/foo.dtb", "g'day, I'm foo.dtb"}, + {"dtbs/bar.dtb", "hello, I'm bar.dtb"}, + // must be last + {"meta/kernel.yaml", "version: 4.2"}, + } + + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = boot.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // this is where the kernel/initrd is unpacked + bootdir := s.bootloader.Dir() + + kernelAssetsDir := filepath.Join(bootdir, "ubuntu-kernel_42.snap") + + for _, def := range files { + if def[0] == "meta/kernel.yaml" { + break + } + + fullFn := filepath.Join(kernelAssetsDir, def[0]) + content, err := ioutil.ReadFile(fullFn) + c.Assert(err, IsNil) + c.Assert(string(content), Equals, def[1]) + } + + // remove + err = boot.RemoveKernelAssets(info) + c.Assert(err, IsNil) + + c.Check(osutil.FileExists(kernelAssetsDir), Equals, false) +} + +func (s *kernelOSSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) { + // pretend to be a grub system + mockGrub := boottest.NewMockBootloader("grub", c.MkDir()) + partition.ForceBootloader(mockGrub) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = boot.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is *not* here + kernimg := filepath.Join(mockGrub.Dir(), "ubuntu-kernel_42.snap", "kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, false) +} + +func (s *kernelOSSuite) TestExtractKernelAssetsError(c *C) { + info := &snap.Info{} + info.Type = snap.TypeApp + + err := boot.ExtractKernelAssets(info, nil) + c.Assert(err, ErrorMatches, `cannot extract kernel assets from snap type "app"`) +} + +// SetNextBoot should do nothing on classic LP: #1580403 +func (s *kernelOSSuite) TestSetNextBootOnClassic(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + // Create a fake OS snap that we try to update + snapInfo := snaptest.MockSnap(c, "name: os\ntype: os", "SNAP", &snap.SideInfo{Revision: snap.R(42)}) + err := boot.SetNextBoot(snapInfo) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, HasLen, 0) +} + +func (s *kernelOSSuite) TestSetNextBootForCore(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + info := &snap.Info{} + info.Type = snap.TypeOS + info.RealName = "core" + info.Revision = snap.R(100) + + err := boot.SetNextBoot(info) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snap_try_core": "core_100.snap", + "snap_mode": "try", + }) + + c.Check(boot.KernelOrOsRebootRequired(info), Equals, true) +} + +func (s *kernelOSSuite) TestSetNextBootForKernel(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + info := &snap.Info{} + info.Type = snap.TypeKernel + info.RealName = "krnl" + info.Revision = snap.R(42) + + err := boot.SetNextBoot(info) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snap_try_kernel": "krnl_42.snap", + "snap_mode": "try", + }) + + s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" + s.bootloader.BootVars["snap_try_kernel"] = "krnl_42.snap" + c.Check(boot.KernelOrOsRebootRequired(info), Equals, true) + + // simulate good boot + s.bootloader.BootVars["snap_kernel"] = "krnl_42.snap" + c.Check(boot.KernelOrOsRebootRequired(info), Equals, false) +} + +func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernel(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + info := &snap.Info{} + info.Type = snap.TypeKernel + info.RealName = "krnl" + info.Revision = snap.R(40) + + s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" + + err := boot.SetNextBoot(info) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snap_kernel": "krnl_40.snap", + }) +} + +func (s *kernelOSSuite) TestInUse(c *C) { + for _, t := range []struct { + bootVarKey string + bootVarValue string + + snapName string + snapRev snap.Revision + + inUse bool + }{ + // in use + {"snap_kernel", "kernel_41.snap", "kernel", snap.R(41), true}, + {"snap_try_kernel", "kernel_82.snap", "kernel", snap.R(82), true}, + {"snap_core", "core_21.snap", "core", snap.R(21), true}, + {"snap_try_core", "core_42.snap", "core", snap.R(42), true}, + // not in use + {"snap_core", "core_111.snap", "core", snap.R(21), false}, + {"snap_try_core", "core_111.snap", "core", snap.R(21), false}, + {"snap_kernel", "kernel_111.snap", "kernel", snap.R(1), false}, + {"snap_try_kernel", "kernel_111.snap", "kernel", snap.R(1), false}, + } { + s.bootloader.BootVars[t.bootVarKey] = t.bootVarValue + c.Assert(boot.InUse(t.snapName, t.snapRev), Equals, t.inUse, Commentf("unexpected result: %s %s %v", t.snapName, t.snapRev, t.inUse)) + } +} diff --git a/client/aliases.go b/client/aliases.go new file mode 100644 index 00000000..5b591034 --- /dev/null +++ b/client/aliases.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" +) + +// aliasAction represents an action performed on aliases. +type aliasAction struct { + Action string `json:"action"` + Snap string `json:"snap"` + Aliases []string `json:"aliases"` +} + +// performAliasAction performs a single action on aliases. +func (client *Client) performAliasAction(sa *aliasAction) (changeID string, err error) { + b, err := json.Marshal(sa) + if err != nil { + return "", err + } + return client.doAsync("POST", "/v2/aliases", nil, nil, bytes.NewReader(b)) +} + +// Alias enables the provided aliases for the snap with snapName. +func (client *Client) Alias(snapName string, aliases []string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "alias", + Snap: snapName, + Aliases: aliases, + }) +} + +// Unalias disables explicitly the provided aliases for the snap with snapName. +func (client *Client) Unalias(snapName string, aliases []string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "unalias", + Snap: snapName, + Aliases: aliases, + }) +} + +// ResetAliases resets the provided aliases for the snap with snapName +// to their default state, enabled for auto-aliases, disabled otherwise. +func (client *Client) ResetAliases(snapName string, aliases []string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "reset", + Snap: snapName, + Aliases: aliases, + }) +} + +// AliasStatus represents the status of an alias. +type AliasStatus struct { + App string `json:"app,omitempty"` + Status string `json:"status,omitempty"` +} + +// Aliases returns a map snap -> alias -> AliasStatus for all snaps and aliases in the system. +func (client *Client) Aliases() (allStatuses map[string]map[string]AliasStatus, err error) { + _, err = client.doSync("GET", "/v2/aliases", nil, nil, nil, &allStatuses) + return +} diff --git a/client/aliases_test.go b/client/aliases_test.go new file mode 100644 index 00000000..12ce0e77 --- /dev/null +++ b/client/aliases_test.go @@ -0,0 +1,144 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client_test + +import ( + "encoding/json" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func (cs *clientSuite) TestClientAliasCallsEndpoint(c *check.C) { + cs.cli.Alias("alias-snap", []string{"alias1", "alias2"}) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientAlias(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.Alias("alias-snap", []string{"alias1", "alias2"}) + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "chgid") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "alias", + "snap": "alias-snap", + "aliases": []interface{}{"alias1", "alias2"}, + }) +} + +func (cs *clientSuite) TestClientUnaliasCallsEndpoint(c *check.C) { + cs.cli.Unalias("alias-snap", []string{"alias1", "alias2"}) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientUnalias(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.Unalias("alias-snap", []string{"alias1", "alias2"}) + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "chgid") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "unalias", + "snap": "alias-snap", + "aliases": []interface{}{"alias1", "alias2"}, + }) +} + +func (cs *clientSuite) TestClientRestAliasesCallsEndpoint(c *check.C) { + cs.cli.ResetAliases("alias-snap", []string{"alias1", "alias2"}) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientResetAliases(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.ResetAliases("alias-snap", []string{"alias1", "alias2"}) + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "chgid") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "reset", + "snap": "alias-snap", + "aliases": []interface{}{"alias1", "alias2"}, + }) +} + +func (cs *clientSuite) TestClientAliasesCallsEndpoint(c *check.C) { + _, _ = cs.cli.Aliases() + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientAliases(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": { + "foo": { + "foo0": {"app": "foo", "status": "auto"}, + "foo_reset": {"app": "foo.reset"} + }, + "bar": { + "bar_dump": {"app": "bar.dump", "status": "enabled"}, + "bar_dump.1": {"status": "disabled"} + } + + } + }` + allStatuses, err := cs.cli.Aliases() + c.Assert(err, check.IsNil) + c.Check(allStatuses, check.DeepEquals, map[string]map[string]client.AliasStatus{ + "foo": { + "foo0": {App: "foo", Status: "auto"}, + "foo_reset": {App: "foo.reset", Status: ""}, + }, + "bar": { + "bar_dump": {App: "bar.dump", Status: "enabled"}, + "bar_dump.1": {App: "", Status: "disabled"}, + }, + }) +} diff --git a/client/asserts.go b/client/asserts.go new file mode 100644 index 00000000..6e2da0d1 --- /dev/null +++ b/client/asserts.go @@ -0,0 +1,92 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + + "github.com/snapcore/snapd/asserts" // for parsing +) + +// Ack tries to add an assertion to the system assertion +// database. To succeed the assertion must be valid, its signature +// verified with a known public key and the assertion consistent with +// and its prerequisite in the database. +func (client *Client) Ack(b []byte) error { + var rsp interface{} + if _, err := client.doSync("POST", "/v2/assertions", nil, nil, bytes.NewReader(b), &rsp); err != nil { + return err + } + + return nil +} + +// Known queries assertions with type assertTypeName and matching assertion headers. +func (client *Client) Known(assertTypeName string, headers map[string]string) ([]asserts.Assertion, error) { + path := fmt.Sprintf("/v2/assertions/%s", assertTypeName) + q := url.Values{} + + if len(headers) > 0 { + for k, v := range headers { + q.Set(k, v) + } + } + + response, err := client.raw("GET", path, q, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to query assertions: %v", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, parseError(response) + } + + sanityCount, err := strconv.Atoi(response.Header.Get("X-Ubuntu-Assertions-Count")) + if err != nil { + return nil, fmt.Errorf("invalid assertions count") + } + + dec := asserts.NewDecoder(response.Body) + + asserts := []asserts.Assertion{} + + // TODO: make sure asserts can decode and deal with unknown types + for { + a, err := dec.Decode() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("failed to decode assertions: %v", err) + } + asserts = append(asserts, a) + } + + if len(asserts) != sanityCount { + return nil, fmt.Errorf("response did not have the expected number of assertions") + } + + return asserts, nil +} diff --git a/client/asserts_test.go b/client/asserts_test.go new file mode 100644 index 00000000..b98f8963 --- /dev/null +++ b/client/asserts_test.go @@ -0,0 +1,145 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client_test + +import ( + "errors" + "io/ioutil" + "net/http" + "net/url" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +func (cs *clientSuite) TestClientAssert(c *C) { + cs.rsp = `{ + "type": "sync", + "result": {} + }` + a := []byte("Assertion.") + err := cs.cli.Ack(a) + c.Assert(err, IsNil) + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, IsNil) + c.Check(body, DeepEquals, a) + c.Check(cs.req.Method, Equals, "POST") + c.Check(cs.req.URL.Path, Equals, "/v2/assertions") +} + +func (cs *clientSuite) TestClientAssertsCallsEndpoint(c *C) { + _, _ = cs.cli.Known("snap-revision", nil) + c.Check(cs.req.Method, Equals, "GET") + c.Check(cs.req.URL.Path, Equals, "/v2/assertions/snap-revision") +} + +func (cs *clientSuite) TestClientAssertsCallsEndpointWithFilter(c *C) { + _, _ = cs.cli.Known("snap-revision", map[string]string{ + "snap-id": "snap-id-1", + "snap-sha3-384": "sha3-384...", + }) + u, err := url.ParseRequestURI(cs.req.URL.String()) + c.Assert(err, IsNil) + c.Check(u.Path, Equals, "/v2/assertions/snap-revision") + c.Check(u.Query(), DeepEquals, url.Values{ + "snap-sha3-384": []string{"sha3-384..."}, + "snap-id": []string{"snap-id-1"}, + }) +} + +func (cs *clientSuite) TestClientAssertsHttpError(c *C) { + cs.err = errors.New("fail") + _, err := cs.cli.Known("snap-build", nil) + c.Assert(err, ErrorMatches, "failed to query assertions: cannot communicate with server: fail") +} + +func (cs *clientSuite) TestClientAssertsJSONError(c *C) { + cs.status = http.StatusBadRequest + cs.header = http.Header{} + cs.header.Add("Content-type", "application/json") + cs.rsp = `{ + "status-code": 400, + "type": "error", + "result": { + "message": "invalid" + } + }` + _, err := cs.cli.Known("snap-build", nil) + c.Assert(err, ErrorMatches, "invalid") +} + +func (cs *clientSuite) TestClientAsserts(c *C) { + cs.header = http.Header{} + cs.header.Add("X-Ubuntu-Assertions-Count", "2") + cs.rsp = `type: snap-revision +authority-id: store-id1 +snap-sha3-384: P1wNUk5O_5tO5spqOLlqUuAk7gkNYezIMHp5N9hMUg1a6YEjNeaCc4T0BaYz7IWs +snap-id: snap-id-1 +snap-size: 123 +snap-revision: 1 +developer-id: dev-id1 +revision: 1 +timestamp: 2015-11-25T20:00:00Z +body-length: 0 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +openpgp ... + +type: snap-revision +authority-id: store-id1 +snap-sha3-384: 0Yt6-GXQeTZWUAHo1IKDpS9kqO6zMaizY6vGEfGM-aSfpghPKir1Ic7teQ5Zadaj +snap-id: snap-id-2 +snap-size: 456 +snap-revision: 1 +developer-id: dev-id1 +revision: 1 +timestamp: 2015-11-30T20:00:00Z +body-length: 0 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +openpgp ... +` + + a, err := cs.cli.Known("snap-revision", nil) + c.Assert(err, IsNil) + c.Check(a, HasLen, 2) + + c.Check(a[0].Type(), Equals, asserts.SnapRevisionType) +} + +func (cs *clientSuite) TestClientAssertsNoAssertions(c *C) { + cs.header = http.Header{} + cs.header.Add("X-Ubuntu-Assertions-Count", "0") + cs.rsp = "" + cs.status = http.StatusOK + a, err := cs.cli.Known("snap-revision", nil) + c.Assert(err, IsNil) + c.Check(a, HasLen, 0) +} + +func (cs *clientSuite) TestClientAssertsMissingAssertions(c *C) { + cs.header = http.Header{} + cs.header.Add("X-Ubuntu-Assertions-Count", "4") + cs.rsp = "" + cs.status = http.StatusOK + _, err := cs.cli.Known("snap-build", nil) + c.Assert(err, ErrorMatches, "response did not have the expected number of assertions") +} diff --git a/client/buy.go b/client/buy.go new file mode 100644 index 00000000..d53b850d --- /dev/null +++ b/client/buy.go @@ -0,0 +1,53 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + + "github.com/snapcore/snapd/store" +) + +func (client *Client) Buy(opts *store.BuyOptions) (*store.BuyResult, error) { + if opts == nil { + opts = &store.BuyOptions{} + } + + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(opts); err != nil { + return nil, err + } + + var result store.BuyResult + _, err := client.doSync("POST", "/v2/buy", nil, nil, &body, &result) + + if err != nil { + return nil, err + } + + return &result, nil +} + +func (client *Client) ReadyToBuy() error { + var result bool + _, err := client.doSync("GET", "/v2/buy/ready", nil, nil, nil, &result) + return err +} diff --git a/client/change.go b/client/change.go new file mode 100644 index 00000000..98eed315 --- /dev/null +++ b/client/change.go @@ -0,0 +1,164 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/url" + "time" +) + +// A Change is a modification to the system state. +type Change struct { + ID string `json:"id"` + Kind string `json:"kind"` + Summary string `json:"summary"` + Status string `json:"status"` + Tasks []*Task `json:"tasks,omitempty"` + Ready bool `json:"ready"` + Err string `json:"err,omitempty"` + + SpawnTime time.Time `json:"spawn-time,omitempty"` + ReadyTime time.Time `json:"ready-time,omitempty"` + + data map[string]*json.RawMessage +} + +var ErrNoData = fmt.Errorf("data entry not found") + +// Get unmarshals into value the kind-specific data with the provided key. +func (c *Change) Get(key string, value interface{}) error { + raw := c.data[key] + if raw == nil { + return ErrNoData + } + return json.Unmarshal([]byte(*raw), value) +} + +// A Task is an operation done to change the system's state. +type Task struct { + ID string `json:"id"` + Kind string `json:"kind"` + Summary string `json:"summary"` + Status string `json:"status"` + Log []string `json:"log,omitempty"` + Progress TaskProgress `json:"progress"` + + SpawnTime time.Time `json:"spawn-time,omitempty"` + ReadyTime time.Time `json:"ready-time,omitempty"` +} + +type TaskProgress struct { + Label string `json:"label"` + Done int `json:"done"` + Total int `json:"total"` +} + +type changeAndData struct { + Change + Data map[string]*json.RawMessage `json:"data"` +} + +// Change fetches information about a Change given its ID +func (client *Client) Change(id string) (*Change, error) { + var chgd changeAndData + _, err := client.doSync("GET", "/v2/changes/"+id, nil, nil, nil, &chgd) + if err != nil { + return nil, err + } + + chgd.Change.data = chgd.Data + return &chgd.Change, nil +} + +// Abort attempts to abort a change that is in not yet ready. +func (client *Client) Abort(id string) (*Change, error) { + var postData struct { + Action string `json:"action"` + } + postData.Action = "abort" + + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(postData); err != nil { + return nil, err + } + + var chg Change + if _, err := client.doSync("POST", "/v2/changes/"+id, nil, nil, &body, &chg); err != nil { + return nil, err + } + + return &chg, nil +} + +type ChangeSelector uint8 + +func (c ChangeSelector) String() string { + switch c { + case ChangesInProgress: + return "in-progress" + case ChangesReady: + return "ready" + case ChangesAll: + return "all" + } + + panic(fmt.Sprintf("unknown ChangeSelector %d", c)) +} + +const ( + ChangesInProgress ChangeSelector = 1 << iota + ChangesReady + ChangesAll = ChangesReady | ChangesInProgress +) + +type ChangesOptions struct { + SnapName string // if empty, no filtering by name is done + Selector ChangeSelector +} + +func (client *Client) Changes(opts *ChangesOptions) ([]*Change, error) { + query := url.Values{} + if opts != nil { + if opts.Selector != 0 { + query.Set("select", opts.Selector.String()) + } + if opts.SnapName != "" { + query.Set("for", opts.SnapName) + } + } + + var chgds []changeAndData + _, err := client.doSync("GET", "/v2/changes", query, nil, nil, &chgds) + if err != nil { + return nil, err + } + + var chgs []*Change + for i := range chgds { + chgd := &chgds[i] + chgd.Change.data = chgd.Data + chgs = append(chgs, &chgd.Change) + } + + return chgs, err +} diff --git a/client/change_test.go b/client/change_test.go new file mode 100644 index 00000000..79dd9b3b --- /dev/null +++ b/client/change_test.go @@ -0,0 +1,215 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client_test + +import ( + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "io/ioutil" + "time" +) + +func (cs *clientSuite) TestClientChange(c *check.C) { + cs.rsp = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "...", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] +}}` + + chg, err := cs.cli.Change("uno") + c.Assert(err, check.IsNil) + c.Check(chg, check.DeepEquals, &client.Change{ + ID: "uno", + Kind: "foo", + Summary: "...", + Status: "Do", + Tasks: []*client.Task{{ + Kind: "bar", + Summary: "...", + Status: "Do", + Progress: client.TaskProgress{Done: 0, Total: 1}, + SpawnTime: time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC), + ReadyTime: time.Date(2016, 04, 21, 1, 2, 4, 0, time.UTC), + }}, + + SpawnTime: time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC), + ReadyTime: time.Date(2016, 04, 21, 1, 2, 4, 0, time.UTC), + }) +} + +func (cs *clientSuite) TestClientChangeData(c *check.C) { + cs.rsp = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "data": {"n": 42} +}}` + + chg, err := cs.cli.Change("uno") + c.Assert(err, check.IsNil) + var n int + err = chg.Get("n", &n) + c.Assert(err, check.IsNil) + c.Assert(n, check.Equals, 42) + + err = chg.Get("missing", &n) + c.Assert(err, check.Equals, client.ErrNoData) +} + +func (cs *clientSuite) TestClientChangeError(c *check.C) { + cs.rsp = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Error", + "ready": true, + "tasks": [{"kind": "bar", "summary": "...", "status": "Error", "progress": {"done": 1, "total": 1}, "log": ["ERROR: something broke"]}], + "err": "error message" +}}` + + chg, err := cs.cli.Change("uno") + c.Assert(err, check.IsNil) + c.Check(chg, check.DeepEquals, &client.Change{ + ID: "uno", + Kind: "foo", + Summary: "...", + Status: "Error", + Tasks: []*client.Task{{ + Kind: "bar", + Summary: "...", + Status: "Error", + Progress: client.TaskProgress{Done: 1, Total: 1}, + Log: []string{"ERROR: something broke"}, + }}, + Err: "error message", + Ready: true, + }) +} + +func (cs *clientSuite) TestClientChangesString(c *check.C) { + for k, v := range map[client.ChangeSelector]string{ + client.ChangesAll: "all", + client.ChangesReady: "ready", + client.ChangesInProgress: "in-progress", + } { + c.Check(k.String(), check.Equals, v) + } +} + +func (cs *clientSuite) TestClientChanges(c *check.C) { + cs.rsp = `{"type": "sync", "result": [{ + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "tasks": [{"kind": "bar", "summary": "...", "status": "Do", "progress": {"done": 0, "total": 1}}] +}]}` + + for _, i := range []*client.ChangesOptions{ + {Selector: client.ChangesAll}, + {Selector: client.ChangesReady}, + {Selector: client.ChangesInProgress}, + {SnapName: "foo"}, + nil, + } { + chg, err := cs.cli.Changes(i) + c.Assert(err, check.IsNil) + c.Check(chg, check.DeepEquals, []*client.Change{{ + ID: "uno", + Kind: "foo", + Summary: "...", + Status: "Do", + Tasks: []*client.Task{{Kind: "bar", Summary: "...", Status: "Do", Progress: client.TaskProgress{Done: 0, Total: 1}}}, + }}) + if i == nil { + c.Check(cs.req.URL.RawQuery, check.Equals, "") + } else { + if i.Selector != 0 { + c.Check(cs.req.URL.RawQuery, check.Equals, "select="+i.Selector.String()) + } else { + c.Check(cs.req.URL.RawQuery, check.Equals, "for="+i.SnapName) + } + } + } + +} + +func (cs *clientSuite) TestClientChangesData(c *check.C) { + cs.rsp = `{"type": "sync", "result": [{ + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "data": {"n": 42} +}]}` + + chgs, err := cs.cli.Changes(&client.ChangesOptions{Selector: client.ChangesAll}) + c.Assert(err, check.IsNil) + + chg := chgs[0] + var n int + err = chg.Get("n", &n) + c.Assert(err, check.IsNil) + c.Assert(n, check.Equals, 42) + + err = chg.Get("missing", &n) + c.Assert(err, check.Equals, client.ErrNoData) +} + +func (cs *clientSuite) TestClientAbort(c *check.C) { + cs.rsp = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Hold", + "ready": true, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z" +}}` + + chg, err := cs.cli.Abort("uno") + c.Assert(err, check.IsNil) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(chg, check.DeepEquals, &client.Change{ + ID: "uno", + Kind: "foo", + Summary: "...", + Status: "Hold", + Ready: true, + + SpawnTime: time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC), + ReadyTime: time.Date(2016, 04, 21, 1, 2, 4, 0, time.UTC), + }) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + + c.Assert(string(body), check.Equals, "{\"action\":\"abort\"}\n") +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 00000000..806d1840 --- /dev/null +++ b/client/client.go @@ -0,0 +1,495 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "path" + "syscall" + "time" + + "github.com/snapcore/snapd/dirs" +) + +func unixDialer() func(string, string) (net.Conn, error) { + // We have two sockets available: the SnapdSocket (which provides + // administrative access), and the SnapSocket (which doesn't). Use the most + // powerful one available (e.g. from within snaps, SnapdSocket is hidden by + // apparmor unless the snap has the snapd-control interface). + socketPath := dirs.SnapdSocket + file, err := os.OpenFile(socketPath, os.O_RDWR, 0666) + if err == nil { + file.Close() + } else if e, ok := err.(*os.PathError); ok && (e.Err == syscall.ENOENT || e.Err == syscall.EACCES) { + socketPath = dirs.SnapSocket + } + + return func(_, _ string) (net.Conn, error) { + return net.Dial("unix", socketPath) + } +} + +type doer interface { + Do(*http.Request) (*http.Response, error) +} + +// Config allows to customize client behavior. +type Config struct { + // BaseURL contains the base URL where snappy daemon is expected to be. + // It can be empty for a default behavior of talking over a unix socket. + BaseURL string +} + +// A Client knows how to talk to the snappy daemon. +type Client struct { + baseURL url.URL + doer doer +} + +// New returns a new instance of Client +func New(config *Config) *Client { + // By default talk over an UNIX socket. + if config == nil || config.BaseURL == "" { + return &Client{ + baseURL: url.URL{ + Scheme: "http", + Host: "localhost", + }, + doer: &http.Client{ + Transport: &http.Transport{Dial: unixDialer()}, + }, + } + } + baseURL, err := url.Parse(config.BaseURL) + if err != nil { + panic(fmt.Sprintf("cannot parse server base URL: %q (%v)", config.BaseURL, err)) + } + return &Client{ + baseURL: *baseURL, + doer: &http.Client{}, + } +} + +func (client *Client) setAuthorization(req *http.Request) error { + user, err := readAuthData() + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + + var buf bytes.Buffer + fmt.Fprintf(&buf, `Macaroon root="%s"`, user.Macaroon) + for _, discharge := range user.Discharges { + fmt.Fprintf(&buf, `, discharge="%s"`, discharge) + } + req.Header.Set("Authorization", buf.String()) + return nil +} + +type RequestError struct{ error } + +func (e RequestError) Error() string { + return fmt.Sprintf("cannot build request: %v", e.error) +} + +type AuthorizationError struct{ error } + +func (e AuthorizationError) Error() string { + return fmt.Sprintf("cannot add authorization: %v", e.error) +} + +type ConnectionError struct{ error } + +func (e ConnectionError) Error() string { + return fmt.Sprintf("cannot communicate with server: %v", e.error) +} + +// raw performs a request and returns the resulting http.Response and +// error you usually only need to call this directly if you expect the +// response to not be JSON, otherwise you'd call Do(...) instead. +func (client *Client) raw(method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) { + // fake a url to keep http.Client happy + u := client.baseURL + u.Path = path.Join(client.baseURL.Path, urlpath) + u.RawQuery = query.Encode() + req, err := http.NewRequest(method, u.String(), body) + if err != nil { + return nil, RequestError{err} + } + + for key, value := range headers { + req.Header.Set(key, value) + } + + // set Authorization header if there are user's credentials + err = client.setAuthorization(req) + if err != nil { + return nil, AuthorizationError{err} + } + + rsp, err := client.doer.Do(req) + if err != nil { + return nil, ConnectionError{err} + } + + return rsp, nil +} + +var ( + doRetry = 250 * time.Millisecond + doTimeout = 5 * time.Second +) + +// MockDoRetry mocks the delays used by the do retry loop. +func MockDoRetry(retry, timeout time.Duration) (restore func()) { + oldRetry := doRetry + oldTimeout := doTimeout + doRetry = retry + doTimeout = timeout + return func() { + doRetry = oldRetry + doTimeout = oldTimeout + } +} + +// do performs a request and decodes the resulting json into the given +// value. It's low-level, for testing/experimenting only; you should +// usually use a higher level interface that builds on this. +func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) error { + retry := time.NewTicker(doRetry) + defer retry.Stop() + timeout := time.After(doTimeout) + var rsp *http.Response + var err error + for { + rsp, err = client.raw(method, path, query, headers, body) + if err == nil || method != "GET" { + break + } + select { + case <-retry.C: + continue + case <-timeout: + } + break + } + if err != nil { + return err + } + defer rsp.Body.Close() + + if v != nil { + dec := json.NewDecoder(rsp.Body) + if err := dec.Decode(v); err != nil { + r := dec.Buffered() + buf, err1 := ioutil.ReadAll(r) + if err1 != nil { + buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1)) + } + return fmt.Errorf("cannot decode %q: %s", buf, err) + } + } + + return nil +} + +// doSync performs a request to the given path using the specified HTTP method. +// It expects a "sync" response from the API and on success decodes the JSON +// response payload into the given value. +func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) { + var rsp response + if err := client.do(method, path, query, headers, body, &rsp); err != nil { + return nil, err + } + if err := rsp.err(); err != nil { + return nil, err + } + if rsp.Type != "sync" { + return nil, fmt.Errorf("expected sync response, got %q", rsp.Type) + } + + if v != nil { + if err := json.Unmarshal(rsp.Result, v); err != nil { + return nil, fmt.Errorf("cannot unmarshal: %v", err) + } + } + + return &rsp.ResultInfo, nil +} + +func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { + var rsp response + + if err := client.do(method, path, query, headers, body, &rsp); err != nil { + return "", err + } + if err := rsp.err(); err != nil { + return "", err + } + if rsp.Type != "async" { + return "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type) + } + if rsp.StatusCode != http.StatusAccepted { + return "", fmt.Errorf("operation not accepted") + } + if rsp.Change == "" { + return "", fmt.Errorf("async response without change reference") + } + + return rsp.Change, nil +} + +type ServerVersion struct { + Version string + Series string + OSID string + OSVersionID string + OnClassic bool +} + +func (client *Client) ServerVersion() (*ServerVersion, error) { + sysInfo, err := client.SysInfo() + if err != nil { + return nil, err + } + + return &ServerVersion{ + Version: sysInfo.Version, + Series: sysInfo.Series, + OSID: sysInfo.OSRelease.ID, + OSVersionID: sysInfo.OSRelease.VersionID, + OnClassic: sysInfo.OnClassic, + }, nil +} + +// A response produced by the REST API will usually fit in this +// (exceptions are the icons/ endpoints obvs) +type response struct { + Result json.RawMessage `json:"result"` + Status string `json:"status"` + StatusCode int `json:"status-code"` + Type string `json:"type"` + Change string `json:"change"` + + ResultInfo +} + +// Error is the real value of response.Result when an error occurs. +type Error struct { + Kind string `json:"kind"` + Message string `json:"message"` + + StatusCode int +} + +func (e *Error) Error() string { + return e.Message +} + +const ( + ErrorKindTwoFactorRequired = "two-factor-required" + ErrorKindTwoFactorFailed = "two-factor-failed" + ErrorKindLoginRequired = "login-required" + ErrorKindTermsNotAccepted = "terms-not-accepted" + ErrorKindNoPaymentMethods = "no-payment-methods" + ErrorKindPaymentDeclined = "payment-declined" + + ErrorKindSnapAlreadyInstalled = "snap-already-installed" + ErrorKindSnapNotInstalled = "snap-not-installed" + ErrorKindNoUpdateAvailable = "snap-no-update-available" +) + +// IsTwoFactorError returns whether the given error is due to problems +// in two-factor authentication. +func IsTwoFactorError(err error) bool { + e, ok := err.(*Error) + if !ok || e == nil { + return false + } + + return e.Kind == ErrorKindTwoFactorFailed || e.Kind == ErrorKindTwoFactorRequired +} + +// OSRelease contains information about the system extracted from /etc/os-release. +type OSRelease struct { + ID string `json:"id"` + VersionID string `json:"version-id,omitempty"` +} + +// SysInfo holds system information +type SysInfo struct { + Series string `json:"series,omitempty"` + Version string `json:"version,omitempty"` + OSRelease OSRelease `json:"os-release"` + OnClassic bool `json:"on-classic"` + Managed bool `json:"managed"` +} + +func (rsp *response) err() error { + if rsp.Type != "error" { + return nil + } + var resultErr Error + err := json.Unmarshal(rsp.Result, &resultErr) + if err != nil || resultErr.Message == "" { + return fmt.Errorf("server error: %q", rsp.Status) + } + resultErr.StatusCode = rsp.StatusCode + + return &resultErr +} + +func parseError(r *http.Response) error { + var rsp response + if r.Header.Get("Content-Type") != "application/json" { + return fmt.Errorf("server error: %q", r.Status) + } + + dec := json.NewDecoder(r.Body) + if err := dec.Decode(&rsp); err != nil { + return fmt.Errorf("cannot unmarshal error: %v", err) + } + + err := rsp.err() + if err == nil { + return fmt.Errorf("server error: %q", r.Status) + } + return err +} + +// SysInfo gets system information from the REST API. +func (client *Client) SysInfo() (*SysInfo, error) { + var sysInfo SysInfo + + if _, err := client.doSync("GET", "/v2/system-info", nil, nil, nil, &sysInfo); err != nil { + return nil, fmt.Errorf("cannot obtain system details: %v", err) + } + + return &sysInfo, nil +} + +// CreateUserResult holds the result of a user creation. +type CreateUserResult struct { + Username string `json:"username"` + SSHKeys []string `json:"ssh-keys"` +} + +// CreateUserOptions holds options for creating a local system user. +// +// If Known is false, the provided email is used to query the store for +// username and SSH key details. +// +// If Known is true, the user will be created by looking through existing +// system-user assertions and looking for a matching email. If Email is +// empty then all such assertions are considered and multiple users may +// be created. +type CreateUserOptions struct { + Email string `json:"email,omitempty"` + Sudoer bool `json:"sudoer,omitempty"` + Known bool `json:"known,omitempty"` + ForceManaged bool `json:"force-managed,omitempty"` +} + +// CreateUser creates a local system user. See CreateUserOptions for details. +func (client *Client) CreateUser(options *CreateUserOptions) (*CreateUserResult, error) { + if options.Email == "" { + return nil, fmt.Errorf("cannot create a user without providing an email") + } + + var result CreateUserResult + data, err := json.Marshal(options) + if err != nil { + return nil, err + } + + if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { + return nil, fmt.Errorf("while creating user: %v", err) + } + return &result, nil +} + +// CreateUsers creates multiple local system users. See CreateUserOptions for details. +// +// Results may be provided even if there are errors. +func (client *Client) CreateUsers(options []*CreateUserOptions) ([]*CreateUserResult, error) { + for _, opts := range options { + if opts.Email == "" && !opts.Known { + return nil, fmt.Errorf("cannot create user from store details without an email to query for") + } + } + + var results []*CreateUserResult + var errs []error + + for _, opts := range options { + data, err := json.Marshal(opts) + if err != nil { + return nil, err + } + + if opts.Email == "" { + var result []*CreateUserResult + if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { + errs = append(errs, err) + } else { + results = append(results, result...) + } + } else { + var result *CreateUserResult + if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { + errs = append(errs, err) + } else { + results = append(results, result) + } + } + } + + if len(errs) == 1 { + return results, errs[0] + } + if len(errs) > 1 { + var buf bytes.Buffer + for _, err := range errs { + fmt.Fprintf(&buf, "\n- %s", err) + } + return results, fmt.Errorf("while creating users:%s", buf.Bytes()) + } + return results, nil +} + +// Users returns the local users. +func (client *Client) Users() ([]*User, error) { + var result []*User + + if _, err := client.doSync("GET", "/v2/users", nil, nil, nil, &result); err != nil { + return nil, fmt.Errorf("while getting users: %v", err) + } + return result, nil +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 00000000..f3740122 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,423 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package client_test + +import ( + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type clientSuite struct { + cli *client.Client + req *http.Request + reqs []*http.Request + rsp string + rsps []string + err error + doCalls int + header http.Header + status int +} + +var _ = Suite(&clientSuite{}) + +func (cs *clientSuite) SetUpTest(c *C) { + os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "auth.json")) + cs.cli = client.New(nil) + cs.cli.SetDoer(cs) + cs.err = nil + cs.req = nil + cs.reqs = nil + cs.rsp = "" + cs.rsps = nil + cs.req = nil + cs.header = nil + cs.status = http.StatusOK + cs.doCalls = 0 + + dirs.SetRootDir(c.MkDir()) +} + +func (cs *clientSuite) TearDownTest(c *C) { + os.Unsetenv(client.TestAuthFileEnvKey) +} + +func (cs *clientSuite) Do(req *http.Request) (*http.Response, error) { + cs.req = req + cs.reqs = append(cs.reqs, req) + body := cs.rsp + if cs.doCalls < len(cs.rsps) { + body = cs.rsps[cs.doCalls] + } + rsp := &http.Response{ + Body: ioutil.NopCloser(strings.NewReader(body)), + Header: cs.header, + StatusCode: cs.status, + } + cs.doCalls++ + return rsp, cs.err +} + +func (cs *clientSuite) TestNewPanics(c *C) { + c.Assert(func() { + client.New(&client.Config{BaseURL: ":"}) + }, PanicMatches, `cannot parse server base URL: ":" \(parse :: missing protocol scheme\)`) +} + +func (cs *clientSuite) TestClientDoReportsErrors(c *C) { + restore := client.MockDoRetry(10*time.Millisecond, 100*time.Millisecond) + defer restore() + cs.err = errors.New("ouchie") + err := cs.cli.Do("GET", "/", nil, nil, nil) + c.Check(err, ErrorMatches, "cannot communicate with server: ouchie") + if cs.doCalls < 2 { + c.Fatalf("do did not retry") + } +} + +func (cs *clientSuite) TestClientWorks(c *C) { + var v []int + cs.rsp = `[1,2]` + reqBody := ioutil.NopCloser(strings.NewReader("")) + err := cs.cli.Do("GET", "/this", nil, reqBody, &v) + c.Check(err, IsNil) + c.Check(v, DeepEquals, []int{1, 2}) + c.Assert(cs.req, NotNil) + c.Assert(cs.req.URL, NotNil) + c.Check(cs.req.Method, Equals, "GET") + c.Check(cs.req.Body, Equals, reqBody) + c.Check(cs.req.URL.Path, Equals, "/this") +} + +func (cs *clientSuite) TestClientDefaultsToNoAuthorization(c *C) { + os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "json")) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + var v string + _ = cs.cli.Do("GET", "/this", nil, nil, &v) + c.Assert(cs.req, NotNil) + authorization := cs.req.Header.Get("Authorization") + c.Check(authorization, Equals, "") +} + +func (cs *clientSuite) TestClientSetsAuthorization(c *C) { + os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "json")) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + mockUserData := client.User{ + Macaroon: "macaroon", + Discharges: []string{"discharge"}, + } + err := client.TestWriteAuth(mockUserData) + c.Assert(err, IsNil) + + var v string + _ = cs.cli.Do("GET", "/this", nil, nil, &v) + authorization := cs.req.Header.Get("Authorization") + c.Check(authorization, Equals, `Macaroon root="macaroon", discharge="discharge"`) +} + +func (cs *clientSuite) TestClientSysInfo(c *C) { + cs.rsp = `{"type": "sync", "result": + {"series": "16", + "version": "2", + "os-release": {"id": "ubuntu", "version-id": "16.04"}, + "on-classic": true}}` + sysInfo, err := cs.cli.SysInfo() + c.Check(err, IsNil) + c.Check(sysInfo, DeepEquals, &client.SysInfo{ + Version: "2", + Series: "16", + OSRelease: client.OSRelease{ + ID: "ubuntu", + VersionID: "16.04", + }, + OnClassic: true, + }) +} + +func (cs *clientSuite) TestServerVersion(c *C) { + cs.rsp = `{"type": "sync", "result": + {"series": "16", + "version": "2", + "os-release": {"id": "zyggy", "version-id": "123"}}}` + version, err := cs.cli.ServerVersion() + c.Check(err, IsNil) + c.Check(version, DeepEquals, &client.ServerVersion{ + Version: "2", + Series: "16", + OSID: "zyggy", + OSVersionID: "123", + }) +} + +func (cs *clientSuite) TestSnapdClientIntegration(c *C) { + c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdSocket), 0755), IsNil) + l, err := net.Listen("unix", dirs.SnapdSocket) + if err != nil { + c.Fatalf("unable to listen on %q: %v", dirs.SnapdSocket, err) + } + + f := func(w http.ResponseWriter, r *http.Request) { + c.Check(r.URL.Path, Equals, "/v2/system-info") + c.Check(r.URL.RawQuery, Equals, "") + + fmt.Fprintln(w, `{"type":"sync", "result":{"series":"42"}}`) + } + + srv := &httptest.Server{ + Listener: l, + Config: &http.Server{Handler: http.HandlerFunc(f)}, + } + srv.Start() + defer srv.Close() + + cli := client.New(nil) + si, err := cli.SysInfo() + c.Check(err, IsNil) + c.Check(si.Series, Equals, "42") +} + +func (cs *clientSuite) TestSnapClientIntegration(c *C) { + c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapSocket), 0755), IsNil) + l, err := net.Listen("unix", dirs.SnapSocket) + if err != nil { + c.Fatalf("unable to listen on %q: %v", dirs.SnapSocket, err) + } + + f := func(w http.ResponseWriter, r *http.Request) { + c.Check(r.URL.Path, Equals, "/v2/snapctl") + c.Check(r.URL.RawQuery, Equals, "") + + fmt.Fprintln(w, `{"type":"sync", "result":{"stdout":"test stdout","stderr":"test stderr"}}`) + } + + srv := &httptest.Server{ + Listener: l, + Config: &http.Server{Handler: http.HandlerFunc(f)}, + } + srv.Start() + defer srv.Close() + + cli := client.New(nil) + options := &client.SnapCtlOptions{ + ContextID: "foo", + Args: []string{"bar", "--baz"}, + } + + stdout, stderr, err := cli.RunSnapctl(options) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "test stdout") + c.Check(string(stderr), Equals, "test stderr") +} + +func (cs *clientSuite) TestClientReportsOpError(c *C) { + cs.rsp = `{"type": "error", "status": "potatoes"}` + _, err := cs.cli.SysInfo() + c.Check(err, ErrorMatches, `.*server error: "potatoes"`) +} + +func (cs *clientSuite) TestClientReportsOpErrorStr(c *C) { + cs.rsp = `{ + "result": {}, + "status": "Bad Request", + "status-code": 400, + "type": "error" + }` + _, err := cs.cli.SysInfo() + c.Check(err, ErrorMatches, `.*server error: "Bad Request"`) +} + +func (cs *clientSuite) TestClientReportsBadType(c *C) { + cs.rsp = `{"type": "what"}` + _, err := cs.cli.SysInfo() + c.Check(err, ErrorMatches, `.*expected sync response, got "what"`) +} + +func (cs *clientSuite) TestClientReportsOuterJSONError(c *C) { + cs.rsp = "this isn't really json is it" + _, err := cs.cli.SysInfo() + c.Check(err, ErrorMatches, `.*invalid character .*`) +} + +func (cs *clientSuite) TestClientReportsInnerJSONError(c *C) { + cs.rsp = `{"type": "sync", "result": "this isn't really json is it"}` + _, err := cs.cli.SysInfo() + c.Check(err, ErrorMatches, `.*cannot unmarshal.*`) +} + +func (cs *clientSuite) TestParseError(c *C) { + resp := &http.Response{ + Status: "404 Not Found", + } + err := client.ParseErrorInTest(resp) + c.Check(err, ErrorMatches, `server error: "404 Not Found"`) + + h := http.Header{} + h.Add("Content-Type", "application/json") + resp = &http.Response{ + Status: "400 Bad Request", + Header: h, + Body: ioutil.NopCloser(strings.NewReader(`{ + "status-code": 400, + "type": "error", + "result": { + "message": "invalid" + } + }`)), + } + err = client.ParseErrorInTest(resp) + c.Check(err, ErrorMatches, "invalid") + + resp = &http.Response{ + Status: "400 Bad Request", + Header: h, + Body: ioutil.NopCloser(strings.NewReader("{}")), + } + err = client.ParseErrorInTest(resp) + c.Check(err, ErrorMatches, `server error: "400 Bad Request"`) +} + +func (cs *clientSuite) TestIsTwoFactor(c *C) { + c.Check(client.IsTwoFactorError(&client.Error{Kind: client.ErrorKindTwoFactorRequired}), Equals, true) + c.Check(client.IsTwoFactorError(&client.Error{Kind: client.ErrorKindTwoFactorFailed}), Equals, true) + c.Check(client.IsTwoFactorError(&client.Error{Kind: "some other kind"}), Equals, false) + c.Check(client.IsTwoFactorError(errors.New("test")), Equals, false) + c.Check(client.IsTwoFactorError(nil), Equals, false) + c.Check(client.IsTwoFactorError((*client.Error)(nil)), Equals, false) +} + +func (cs *clientSuite) TestClientCreateUser(c *C) { + _, err := cs.cli.CreateUser(&client.CreateUserOptions{}) + c.Assert(err, ErrorMatches, "cannot create a user without providing an email") + + cs.rsp = `{ + "type": "sync", + "result": { + "username": "karl", + "ssh-keys": ["one", "two"] + } + }` + rsp, err := cs.cli.CreateUser(&client.CreateUserOptions{Email: "one@email.com", Sudoer: true, Known: true}) + c.Assert(cs.req.Method, Equals, "POST") + c.Assert(cs.req.URL.Path, Equals, "/v2/create-user") + c.Assert(err, IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, IsNil) + c.Assert(string(body), Equals, `{"email":"one@email.com","sudoer":true,"known":true}`) + + c.Assert(rsp, DeepEquals, &client.CreateUserResult{ + Username: "karl", + SSHKeys: []string{"one", "two"}, + }) +} + +var createUsersTests = []struct { + options []*client.CreateUserOptions + bodies []string + responses []string + results []*client.CreateUserResult + error string +}{{ + options: []*client.CreateUserOptions{{}}, + error: "cannot create user from store details without an email to query for", +}, { + options: []*client.CreateUserOptions{{ + Email: "one@example.com", + Sudoer: true, + }, { + Known: true, + }}, + bodies: []string{ + `{"email":"one@example.com","sudoer":true}`, + `{"known":true}`, + }, + responses: []string{ + `{"type": "sync", "result": {"username": "one", "ssh-keys":["a", "b"]}}`, + `{"type": "sync", "result": [{"username": "two"}, {"username": "three"}]}`, + }, + results: []*client.CreateUserResult{{ + Username: "one", + SSHKeys: []string{"a", "b"}, + }, { + Username: "two", + }, { + Username: "three", + }}, +}} + +func (cs *clientSuite) TestClientCreateUsers(c *C) { + for _, test := range createUsersTests { + cs.rsps = test.responses + + results, err := cs.cli.CreateUsers(test.options) + if test.error != "" { + c.Assert(err, ErrorMatches, test.error) + } + c.Assert(results, DeepEquals, test.results) + + var bodies []string + for _, req := range cs.reqs { + c.Assert(req.Method, Equals, "POST") + c.Assert(req.URL.Path, Equals, "/v2/create-user") + data, err := ioutil.ReadAll(req.Body) + c.Assert(err, IsNil) + bodies = append(bodies, string(data)) + } + + c.Assert(bodies, DeepEquals, test.bodies) + } +} + +func (cs *clientSuite) TestClientJSONError(c *C) { + cs.rsp = `some non-json error message` + _, err := cs.cli.SysInfo() + c.Assert(err, ErrorMatches, `cannot obtain system details: cannot decode "some non-json error message": invalid char.*`) +} + +func (cs *clientSuite) TestUsers(c *C) { + cs.rsp = `{"type": "sync", "result": + [{"username": "foo","email":"foo@example.com"}, + {"username": "bar","email":"bar@example.com"}]}` + sysInfo, err := cs.cli.Users() + c.Check(err, IsNil) + c.Check(sysInfo, DeepEquals, []*client.User{ + {Username: "foo", Email: "foo@example.com"}, + {Username: "bar", Email: "bar@example.com"}, + }) +} diff --git a/client/conf.go b/client/conf.go new file mode 100644 index 00000000..1fd60944 --- /dev/null +++ b/client/conf.go @@ -0,0 +1,50 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "net/url" + "strings" +) + +// SetConf requests a snap to apply the provided patch to the configuration. +func (client *Client) SetConf(snapName string, patch map[string]interface{}) (changeID string, err error) { + b, err := json.Marshal(patch) + if err != nil { + return "", err + } + return client.doAsync("PUT", "/v2/snaps/"+snapName+"/conf", nil, nil, bytes.NewReader(b)) +} + +// Conf asks for a snap's current configuration. +func (client *Client) Conf(snapName string, keys []string) (configuration map[string]interface{}, err error) { + // Prepare query + query := url.Values{} + query.Set("keys", strings.Join(keys, ",")) + + _, err = client.doSync("GET", "/v2/snaps/"+snapName+"/conf", query, nil, nil, &configuration) + if err != nil { + return nil, err + } + + return configuration, nil +} diff --git a/client/conf_test.go b/client/conf_test.go new file mode 100644 index 00000000..386150e2 --- /dev/null +++ b/client/conf_test.go @@ -0,0 +1,93 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client_test + +import ( + "encoding/json" + + "gopkg.in/check.v1" +) + +func (cs *clientSuite) TestClientSetConfCallsEndpoint(c *check.C) { + cs.cli.SetConf("snap-name", map[string]interface{}{"key": "value"}) + c.Check(cs.req.Method, check.Equals, "PUT") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps/snap-name/conf") +} + +func (cs *clientSuite) TestClientGetConfCallsEndpoint(c *check.C) { + cs.cli.Conf("snap-name", []string{"test-key"}) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps/snap-name/conf") + c.Check(cs.req.URL.Query().Get("keys"), check.Equals, "test-key") +} + +func (cs *clientSuite) TestClientGetConfCallsEndpointMultipleKeys(c *check.C) { + cs.cli.Conf("snap-name", []string{"test-key1", "test-key2"}) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps/snap-name/conf") + c.Check(cs.req.URL.Query().Get("keys"), check.Equals, "test-key1,test-key2") +} + +func (cs *clientSuite) TestClientSetConf(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "foo" + }` + id, err := cs.cli.SetConf("snap-name", map[string]interface{}{"key": "value"}) + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "foo") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "key": "value", + }) +} + +func (cs *clientSuite) TestClientGetConf(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {"test-key": "test-value"} + }` + value, err := cs.cli.Conf("snap-name", []string{"test-key"}) + c.Assert(err, check.IsNil) + c.Check(value, check.DeepEquals, map[string]interface{}{"test-key": "test-value"}) +} + +func (cs *clientSuite) TestClientGetConfMultipleKeys(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": { + "test-key1": "test-value1", + "test-key2": "test-value2" + } + }` + value, err := cs.cli.Conf("snap-name", []string{"test-key1", "test-key2"}) + c.Assert(err, check.IsNil) + c.Check(value, check.DeepEquals, map[string]interface{}{ + "test-key1": "test-value1", + "test-key2": "test-value2", + }) +} diff --git a/client/export_test.go b/client/export_test.go new file mode 100644 index 00000000..ff750c86 --- /dev/null +++ b/client/export_test.go @@ -0,0 +1,45 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package client + +import ( + "io" + "net/url" +) + +// SetDoer sets the client's doer to the given one +func (client *Client) SetDoer(d doer) { + client.doer = d +} + +// Do does do. +func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}) error { + return client.do(method, path, query, nil, body, v) +} + +// expose parseError for testing +var ParseErrorInTest = parseError + +// expose read and write auth helpers for testing +var TestWriteAuth = writeAuthData +var TestReadAuth = readAuthData +var TestStoreAuthFilename = storeAuthDataFilename + +var TestAuthFileEnvKey = authFileEnvKey diff --git a/client/icons.go b/client/icons.go new file mode 100644 index 00000000..e5584b65 --- /dev/null +++ b/client/icons.go @@ -0,0 +1,67 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client + +import ( + "fmt" + "io/ioutil" + "net/http" + "regexp" +) + +// Icon represents the icon of an installed snap +type Icon struct { + Filename string + Content []byte +} + +// Icon returns the Icon belonging to an installed snap +func (c *Client) Icon(pkgID string) (*Icon, error) { + const errPrefix = "cannot retrieve icon" + + response, err := c.raw("GET", fmt.Sprintf("/v2/icons/%s/icon", pkgID), nil, nil, nil) + if err != nil { + return nil, fmt.Errorf("%s: failed to communicate with server: %s", errPrefix, err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: Not Found", errPrefix) + } + + re := regexp.MustCompile(`attachment; filename=(.+)`) + matches := re.FindStringSubmatch(response.Header.Get("Content-Disposition")) + + if matches == nil || matches[1] == "" { + return nil, fmt.Errorf("%s: cannot determine filename", errPrefix) + } + + content, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("%s: %s", errPrefix, err) + } + + icon := &Icon{ + Filename: matches[1], + Content: content, + } + + return icon, nil +} diff --git a/client/icons_test.go b/client/icons_test.go new file mode 100644 index 00000000..981f6fa9 --- /dev/null +++ b/client/icons_test.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client_test + +import ( + "errors" + "fmt" + "net/http" + + . "gopkg.in/check.v1" +) + +const ( + pkgID = "chatroom.ogra" +) + +func (cs *clientSuite) TestClientIconCallsEndpoint(c *C) { + _, _ = cs.cli.Icon(pkgID) + c.Assert(cs.req.Method, Equals, "GET") + c.Assert(cs.req.URL.Path, Equals, fmt.Sprintf("/v2/icons/%s/icon", pkgID)) +} + +func (cs *clientSuite) TestClientIconHttpError(c *C) { + cs.err = errors.New("fail") + _, err := cs.cli.Icon(pkgID) + c.Assert(err, ErrorMatches, ".*server: fail") +} + +func (cs *clientSuite) TestClientIconResponseNotFound(c *C) { + cs.status = http.StatusNotFound + _, err := cs.cli.Icon(pkgID) + c.Assert(err, ErrorMatches, `.*Not Found`) +} + +func (cs *clientSuite) TestClientIconInvalidContentDisposition(c *C) { + cs.header = http.Header{"Content-Disposition": {"invalid"}} + _, err := cs.cli.Icon(pkgID) + c.Assert(err, ErrorMatches, `.*cannot determine filename`) +} + +func (cs *clientSuite) TestClientIcon(c *C) { + cs.rsp = "pixels" + cs.header = http.Header{"Content-Disposition": {"attachment; filename=myicon.png"}} + icon, err := cs.cli.Icon(pkgID) + c.Assert(err, IsNil) + c.Assert(icon.Filename, Equals, "myicon.png") + c.Assert(icon.Content, DeepEquals, []byte("pixels")) +} diff --git a/client/interfaces.go b/client/interfaces.go new file mode 100644 index 00000000..6ae2148e --- /dev/null +++ b/client/interfaces.go @@ -0,0 +1,106 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" +) + +// Plug represents the potential of a given snap to connect to a slot. +type Plug struct { + Snap string `json:"snap"` + Name string `json:"plug"` + Interface string `json:"interface,omitempty"` + Attrs map[string]interface{} `json:"attrs,omitempty"` + Apps []string `json:"apps,omitempty"` + Label string `json:"label,omitempty"` + Connections []SlotRef `json:"connections,omitempty"` +} + +// PlugRef is a reference to a plug. +type PlugRef struct { + Snap string `json:"snap"` + Name string `json:"plug"` +} + +// Slot represents a capacity offered by a snap. +type Slot struct { + Snap string `json:"snap"` + Name string `json:"slot"` + Interface string `json:"interface,omitempty"` + Attrs map[string]interface{} `json:"attrs,omitempty"` + Apps []string `json:"apps,omitempty"` + Label string `json:"label,omitempty"` + Connections []PlugRef `json:"connections,omitempty"` +} + +// SlotRef is a reference to a slot. +type SlotRef struct { + Snap string `json:"snap"` + Name string `json:"slot"` +} + +// Interfaces contains information about all plugs, slots and their connections +type Interfaces struct { + Plugs []Plug `json:"plugs"` + Slots []Slot `json:"slots"` +} + +// InterfaceAction represents an action performed on the interface system. +type InterfaceAction struct { + Action string `json:"action"` + Plugs []Plug `json:"plugs,omitempty"` + Slots []Slot `json:"slots,omitempty"` +} + +// Interfaces returns all plugs, slots and their connections. +func (client *Client) Interfaces() (interfaces Interfaces, err error) { + _, err = client.doSync("GET", "/v2/interfaces", nil, nil, nil, &interfaces) + return +} + +// performInterfaceAction performs a single action on the interface system. +func (client *Client) performInterfaceAction(sa *InterfaceAction) (changeID string, err error) { + b, err := json.Marshal(sa) + if err != nil { + return "", err + } + return client.doAsync("POST", "/v2/interfaces", nil, nil, bytes.NewReader(b)) +} + +// Connect establishes a connection between a plug and a slot. +// The plug and the slot must have the same interface. +func (client *Client) Connect(plugSnapName, plugName, slotSnapName, slotName string) (changeID string, err error) { + return client.performInterfaceAction(&InterfaceAction{ + Action: "connect", + Plugs: []Plug{{Snap: plugSnapName, Name: plugName}}, + Slots: []Slot{{Snap: slotSnapName, Name: slotName}}, + }) +} + +// Disconnect breaks the connection between a plug and a slot. +func (client *Client) Disconnect(plugSnapName, plugName, slotSnapName, slotName string) (changeID string, err error) { + return client.performInterfaceAction(&InterfaceAction{ + Action: "disconnect", + Plugs: []Plug{{Snap: plugSnapName, Name: plugName}}, + Slots: []Slot{{Snap: slotSnapName, Name: slotName}}, + }) +} diff --git a/client/interfaces_test.go b/client/interfaces_test.go new file mode 100644 index 00000000..9487a093 --- /dev/null +++ b/client/interfaces_test.go @@ -0,0 +1,170 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client_test + +import ( + "encoding/json" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func (cs *clientSuite) TestClientInterfacesCallsEndpoint(c *check.C) { + _, _ = cs.cli.Interfaces() + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") +} + +func (cs *clientSuite) TestClientInterfaces(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": { + "plugs": [ + { + "snap": "canonical-pi2", + "plug": "pin-13", + "interface": "bool-file", + "label": "Pin 13", + "connections": [ + {"snap": "keyboard-lights", "slot": "capslock-led"} + ] + } + ], + "slots": [ + { + "snap": "keyboard-lights", + "slot": "capslock-led", + "interface": "bool-file", + "label": "Capslock indicator LED", + "connections": [ + {"snap": "canonical-pi2", "plug": "pin-13"} + ] + } + ] + } + }` + interfaces, err := cs.cli.Interfaces() + c.Assert(err, check.IsNil) + c.Check(interfaces, check.DeepEquals, client.Interfaces{ + Plugs: []client.Plug{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.SlotRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }, + Slots: []client.Slot{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + Interface: "bool-file", + Label: "Capslock indicator LED", + Connections: []client.PlugRef{ + { + Snap: "canonical-pi2", + Name: "pin-13", + }, + }, + }, + }, + }) +} + +func (cs *clientSuite) TestClientConnectCallsEndpoint(c *check.C) { + cs.cli.Connect("producer", "plug", "consumer", "slot") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") +} + +func (cs *clientSuite) TestClientConnect(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "foo" + }` + id, err := cs.cli.Connect("producer", "plug", "consumer", "slot") + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "foo") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "connect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) +} + +func (cs *clientSuite) TestClientDisconnectCallsEndpoint(c *check.C) { + cs.cli.Disconnect("producer", "plug", "consumer", "slot") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") +} + +func (cs *clientSuite) TestClientDisconnect(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "42" + }` + id, err := cs.cli.Disconnect("producer", "plug", "consumer", "slot") + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "42") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "disconnect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) +} diff --git a/client/login.go b/client/login.go new file mode 100644 index 00000000..d6dc4edf --- /dev/null +++ b/client/login.go @@ -0,0 +1,161 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/snapcore/snapd/osutil" +) + +// User holds logged in user information. +type User struct { + ID int `json:"id,omitempty"` + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + + Macaroon string `json:"macaroon,omitempty"` + Discharges []string `json:"discharges,omitempty"` +} + +type loginData struct { + Email string `json:"email,omitempty"` + Password string `json:"password,omitempty"` + Otp string `json:"otp,omitempty"` +} + +// Login logs user in. +func (client *Client) Login(email, password, otp string) (*User, error) { + postData := loginData{ + Email: email, + Password: password, + Otp: otp, + } + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(postData); err != nil { + return nil, err + } + + var user User + if _, err := client.doSync("POST", "/v2/login", nil, nil, &body, &user); err != nil { + return nil, err + } + + if err := writeAuthData(user); err != nil { + return nil, fmt.Errorf("cannot persist login information: %v", err) + } + return &user, nil +} + +// Logout logs the user out. +func (client *Client) Logout() error { + _, err := client.doSync("POST", "/v2/logout", nil, nil, nil, nil) + if err != nil { + return err + } + return removeAuthData() +} + +// LoggedInUser returns the logged in User or nil +func (client *Client) LoggedInUser() *User { + u, err := readAuthData() + if err != nil { + return nil + } + return u +} + +const authFileEnvKey = "SNAPPY_STORE_AUTH_DATA_FILENAME" + +func storeAuthDataFilename(homeDir string) string { + if fn := os.Getenv(authFileEnvKey); fn != "" { + return fn + } + + if homeDir == "" { + real, err := osutil.RealUser() + if err != nil { + panic(err) + } + homeDir = real.HomeDir + } + + return filepath.Join(homeDir, ".snap", "auth.json") +} + +// writeAuthData saves authentication details for later reuse through ReadAuthData +func writeAuthData(user User) error { + real, err := osutil.RealUser() + if err != nil { + return err + } + + uid, err := strconv.Atoi(real.Uid) + if err != nil { + return err + } + + gid, err := strconv.Atoi(real.Gid) + if err != nil { + return err + } + + targetFile := storeAuthDataFilename(real.HomeDir) + + if err := osutil.MkdirAllChown(filepath.Dir(targetFile), 0700, uid, gid); err != nil { + return err + } + + outStr, err := json.Marshal(user) + if err != nil { + return nil + } + + return osutil.AtomicWriteFileChown(targetFile, []byte(outStr), 0600, 0, uid, gid) +} + +// readAuthData reads previously written authentication details +func readAuthData() (*User, error) { + sourceFile := storeAuthDataFilename("") + f, err := os.Open(sourceFile) + if err != nil { + return nil, err + } + defer f.Close() + + var user User + dec := json.NewDecoder(f) + if err := dec.Decode(&user); err != nil { + return nil, err + } + + return &user, nil +} + +// removeAuthData removes any previously written authentication details. +func removeAuthData() error { + filename := storeAuthDataFilename("") + return os.Remove(filename) +} diff --git a/client/login_test.go b/client/login_test.go new file mode 100644 index 00000000..c7cc6515 --- /dev/null +++ b/client/login_test.go @@ -0,0 +1,132 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/osutil" +) + +func (cs *clientSuite) TestClientLogin(c *check.C) { + cs.rsp = `{"type": "sync", "result": + {"username": "the-user-name", + "macaroon": "the-root-macaroon", + "discharges": ["discharge-macaroon"]}}` + + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + c.Assert(cs.cli.LoggedInUser(), check.IsNil) + + user, err := cs.cli.Login("username", "pass", "") + c.Check(err, check.IsNil) + c.Check(user, check.DeepEquals, &client.User{ + Username: "the-user-name", + Macaroon: "the-root-macaroon", + Discharges: []string{"discharge-macaroon"}}) + + c.Assert(cs.cli.LoggedInUser(), check.Not(check.IsNil)) + + c.Check(osutil.FileExists(outfile), check.Equals, true) + content, err := ioutil.ReadFile(outfile) + c.Check(err, check.IsNil) + c.Check(string(content), check.Equals, `{"username":"the-user-name","macaroon":"the-root-macaroon","discharges":["discharge-macaroon"]}`) +} + +func (cs *clientSuite) TestClientLoginError(c *check.C) { + cs.rsp = `{ + "result": {}, + "status": "Bad Request", + "status-code": 400, + "type": "error" + }` + + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + user, err := cs.cli.Login("username", "pass", "") + + c.Check(user, check.IsNil) + c.Check(err, check.NotNil) + + c.Check(osutil.FileExists(outfile), check.Equals, false) +} + +func (cs *clientSuite) TestClientLogout(c *check.C) { + cs.rsp = `{"type": "sync", "result": {}}` + + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + err := ioutil.WriteFile(outfile, []byte(`{"macaroon":"macaroon","discharges":["discharged"]}`), 0600) + c.Assert(err, check.IsNil) + + err = cs.cli.Logout() + c.Assert(err, check.IsNil) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/logout")) + + c.Check(osutil.FileExists(outfile), check.Equals, false) +} + +func (cs *clientSuite) TestWriteAuthData(c *check.C) { + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + authData := client.User{ + Macaroon: "macaroon", + Discharges: []string{"discharge"}, + } + err := client.TestWriteAuth(authData) + c.Assert(err, check.IsNil) + + c.Check(osutil.FileExists(outfile), check.Equals, true) + content, err := ioutil.ReadFile(outfile) + c.Check(err, check.IsNil) + c.Check(string(content), check.Equals, `{"macaroon":"macaroon","discharges":["discharge"]}`) +} + +func (cs *clientSuite) TestReadAuthData(c *check.C) { + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + authData := client.User{ + Macaroon: "macaroon", + Discharges: []string{"discharge"}, + } + err := client.TestWriteAuth(authData) + c.Assert(err, check.IsNil) + + readUser, err := client.TestReadAuth() + c.Assert(err, check.IsNil) + c.Check(readUser, check.DeepEquals, &authData) +} diff --git a/client/packages.go b/client/packages.go new file mode 100644 index 00000000..543cb0e1 --- /dev/null +++ b/client/packages.go @@ -0,0 +1,227 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package client + +import ( + "errors" + "fmt" + "net/url" + "time" + + "github.com/snapcore/snapd/snap" +) + +// Snap holds the data for a snap as obtained from snapd. +type Snap struct { + ID string `json:"id"` + Summary string `json:"summary"` + Description string `json:"description"` + DownloadSize int64 `json:"download-size"` + Icon string `json:"icon"` + InstalledSize int64 `json:"installed-size"` + InstallDate time.Time `json:"install-date"` + Name string `json:"name"` + Developer string `json:"developer"` + Status string `json:"status"` + Type string `json:"type"` + Version string `json:"version"` + Channel string `json:"channel"` + TrackingChannel string `json:"tracking-channel"` + Revision snap.Revision `json:"revision"` + Confinement string `json:"confinement"` + Private bool `json:"private"` + DevMode bool `json:"devmode"` + JailMode bool `json:"jailmode"` + TryMode bool `json:"trymode"` + Apps []AppInfo `json:"apps"` + Broken string `json:"broken"` + + Prices map[string]float64 `json:"prices"` + Screenshots []Screenshot `json:"screenshots"` + + Channels map[string]*snap.ChannelSnapInfo `json:"channels"` +} + +type AppInfo struct { + Name string `json:"name"` + Daemon string `json:"daemon"` + Aliases []string `json:"aliases"` +} + +type Screenshot struct { + URL string `json:"url"` + Width int64 `json:"width,omitempty"` + Height int64 `json:"height,omitempty"` +} + +// Statuses and types a snap may have. +const ( + StatusAvailable = "available" + StatusInstalled = "installed" + StatusActive = "active" + StatusRemoved = "removed" + StatusPriced = "priced" + + TypeApp = "app" + TypeKernel = "kernel" + TypeGadget = "gadget" + TypeOS = "os" + + StrictConfinement = "strict" + DevModeConfinement = "devmode" + ClassicConfinement = "classic" +) + +type ResultInfo struct { + SuggestedCurrency string `json:"suggested-currency"` +} + +// FindOptions supports exactly one of the following options: +// - Refresh: only return snaps that are refreshable +// - Private: return snaps that are private +// - Query: only return snaps that match the query string +type FindOptions struct { + Refresh bool + Private bool + Prefix bool + Query string + Section string +} + +var ErrNoSnapsInstalled = errors.New("no snaps installed") + +type ListOptions struct { + All bool +} + +// List returns the list of all snaps installed on the system +// with names in the given list; if the list is empty, all snaps. +func (client *Client) List(names []string, opts *ListOptions) ([]*Snap, error) { + if opts == nil { + opts = &ListOptions{} + } + + q := make(url.Values) + if opts.All { + q.Add("select", "all") + } + + snaps, _, err := client.snapsFromPath("/v2/snaps", q) + if err != nil { + return nil, err + } + + if len(snaps) == 0 { + return nil, ErrNoSnapsInstalled + } + + if len(names) == 0 { + return snaps, nil + } + + wanted := make(map[string]bool, len(names)) + for _, name := range names { + wanted[name] = true + } + + var result []*Snap + for _, snap := range snaps { + if wanted[snap.Name] { + result = append(result, snap) + } + } + + return result, nil +} + +// Sections returns the list of existing snap sections in the store +func (client *Client) Sections() ([]string, error) { + var sections []string + _, err := client.doSync("GET", "/v2/sections", nil, nil, nil, §ions) + if err != nil { + return nil, fmt.Errorf("cannot get snap sections: %s", err) + } + return sections, nil +} + +// Find returns a list of snaps available for install from the +// store for this system and that match the query +func (client *Client) Find(opts *FindOptions) ([]*Snap, *ResultInfo, error) { + if opts == nil { + opts = &FindOptions{} + } + + q := url.Values{} + if opts.Prefix { + q.Set("name", opts.Query+"*") + } else { + q.Set("q", opts.Query) + } + switch { + case opts.Refresh && opts.Private: + return nil, nil, fmt.Errorf("cannot specify refresh and private together") + case opts.Refresh: + q.Set("select", "refresh") + case opts.Private: + q.Set("select", "private") + } + if opts.Section != "" { + q.Set("section", opts.Section) + } + + return client.snapsFromPath("/v2/find", q) +} + +func (client *Client) FindOne(name string) (*Snap, *ResultInfo, error) { + q := url.Values{} + q.Set("name", name) + + snaps, ri, err := client.snapsFromPath("/v2/find", q) + if err != nil { + return nil, nil, fmt.Errorf("cannot find snap %q: %s", name, err) + } + + if len(snaps) == 0 { + return nil, nil, fmt.Errorf("cannot find snap %q", name) + } + + return snaps[0], ri, nil +} + +func (client *Client) snapsFromPath(path string, query url.Values) ([]*Snap, *ResultInfo, error) { + var snaps []*Snap + ri, err := client.doSync("GET", path, query, nil, nil, &snaps) + if err != nil { + return nil, nil, fmt.Errorf("cannot list snaps: %s", err) + } + return snaps, ri, nil +} + +// Snap returns the most recently published revision of the snap with the +// provided name. +func (client *Client) Snap(name string) (*Snap, *ResultInfo, error) { + var snap *Snap + path := fmt.Sprintf("/v2/snaps/%s", name) + ri, err := client.doSync("GET", path, nil, nil, nil, &snap) + if err != nil { + return nil, nil, fmt.Errorf("cannot retrieve snap %q: %s", name, err) + } + return snap, ri, nil +} diff --git a/client/packages_test.go b/client/packages_test.go new file mode 100644 index 00000000..d5a54238 --- /dev/null +++ b/client/packages_test.go @@ -0,0 +1,225 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package client_test + +import ( + "fmt" + "net/url" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func (cs *clientSuite) TestClientSnapsCallsEndpoint(c *check.C) { + _, _ = cs.cli.List(nil, nil) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{}) +} + +func (cs *clientSuite) TestClientFindRefreshSetsQuery(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{ + Refresh: true, + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ + "q": []string{""}, "select": []string{"refresh"}, + }) +} + +func (cs *clientSuite) TestClientFindRefreshSetsQueryWithSec(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{ + Refresh: true, + Section: "mysection", + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ + "q": []string{""}, "section": []string{"mysection"}, "select": []string{"refresh"}, + }) +} + +func (cs *clientSuite) TestClientFindWithSectionSetsQuery(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{ + Section: "mysection", + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ + "q": []string{""}, "section": []string{"mysection"}, + }) +} + +func (cs *clientSuite) TestClientFindPrivateSetsQuery(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{ + Private: true, + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + + c.Check(cs.req.URL.Query().Get("select"), check.Equals, "private") +} + +func (cs *clientSuite) TestClientSnapsInvalidSnapsJSON(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": "not a list of snaps" + }` + _, err := cs.cli.List(nil, nil) + c.Check(err, check.ErrorMatches, `.*cannot unmarshal.*`) +} + +func (cs *clientSuite) TestClientNoSnaps(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": [], + "suggested-currency": "GBP" + }` + _, err := cs.cli.List(nil, nil) + c.Check(err, check.Equals, client.ErrNoSnapsInstalled) + _, err = cs.cli.List([]string{"foo"}, nil) + c.Check(err, check.Equals, client.ErrNoSnapsInstalled) +} + +func (cs *clientSuite) TestClientSnaps(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": [{ + "id": "funky-snap-id", + "summary": "salutation snap", + "description": "hello-world", + "download-size": 22212, + "icon": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", + "installed-size": -1, + "name": "hello-world", + "developer": "canonical", + "resource": "/v2/snaps/hello-world.canonical", + "status": "available", + "type": "app", + "version": "1.0.18", + "confinement": "strict", + "private": true + }], + "suggested-currency": "GBP" + }` + applications, err := cs.cli.List(nil, nil) + c.Check(err, check.IsNil) + c.Check(applications, check.DeepEquals, []*client.Snap{{ + ID: "funky-snap-id", + Summary: "salutation snap", + Description: "hello-world", + DownloadSize: 22212, + Icon: "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", + InstalledSize: -1, + Name: "hello-world", + Developer: "canonical", + Status: client.StatusAvailable, + Type: client.TypeApp, + Version: "1.0.18", + Confinement: client.StrictConfinement, + Private: true, + DevMode: false, + }}) + otherApps, err := cs.cli.List([]string{"foo"}, nil) + c.Check(err, check.IsNil) + c.Check(otherApps, check.HasLen, 0) +} + +func (cs *clientSuite) TestClientFilterSnaps(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{Query: "foo"}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.RawQuery, check.Equals, "q=foo") +} + +func (cs *clientSuite) TestClientFindPrefix(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{Query: "foo", Prefix: true}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.RawQuery, check.Equals, "name=foo%2A") // 2A is `*` +} + +func (cs *clientSuite) TestClientFindOne(c *check.C) { + _, _, _ = cs.cli.FindOne("foo") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.RawQuery, check.Equals, "name=foo") +} + +const ( + pkgName = "chatroom" +) + +func (cs *clientSuite) TestClientSnap(c *check.C) { + // example data obtained via + // printf "GET /v2/find?name=test-snapd-tools HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket|grep '{'|python3 -m json.tool + cs.rsp = `{ + "type": "sync", + "result": { + "id": "funky-snap-id", + "summary": "bla bla", + "description": "WebRTC Video chat server for Snappy", + "download-size": 6930947, + "icon": "/v2/icons/chatroom.ogra/icon", + "installed-size": 18976651, + "install-date": "2016-01-02T15:04:05Z", + "name": "chatroom", + "developer": "ogra", + "resource": "/v2/snaps/chatroom.ogra", + "status": "active", + "type": "app", + "version": "0.1-8", + "confinement": "strict", + "private": true, + "devmode": true, + "trymode": true, + "screenshots": [ + {"url":"http://example.com/shot1.png", "width":640, "height":480}, + {"url":"http://example.com/shot2.png"} + ] + } + }` + pkg, _, err := cs.cli.Snap(pkgName) + c.Assert(cs.req.Method, check.Equals, "GET") + c.Assert(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps/%s", pkgName)) + c.Assert(err, check.IsNil) + c.Assert(pkg, check.DeepEquals, &client.Snap{ + ID: "funky-snap-id", + Summary: "bla bla", + Description: "WebRTC Video chat server for Snappy", + DownloadSize: 6930947, + Icon: "/v2/icons/chatroom.ogra/icon", + InstalledSize: 18976651, + InstallDate: time.Date(2016, 1, 2, 15, 4, 5, 0, time.UTC), + Name: "chatroom", + Developer: "ogra", + Status: client.StatusActive, + Type: client.TypeApp, + Version: "0.1-8", + Confinement: client.StrictConfinement, + Private: true, + DevMode: true, + TryMode: true, + Screenshots: []client.Screenshot{ + {URL: "http://example.com/shot1.png", Width: 640, Height: 480}, + {URL: "http://example.com/shot2.png"}, + }, + }) +} diff --git a/client/snap_op.go b/client/snap_op.go new file mode 100644 index 00000000..a31b7d26 --- /dev/null +++ b/client/snap_op.go @@ -0,0 +1,226 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" + "strconv" +) + +type SnapOptions struct { + Channel string `json:"channel,omitempty"` + Revision string `json:"revision,omitempty"` + DevMode bool `json:"devmode,omitempty"` + JailMode bool `json:"jailmode,omitempty"` + Classic bool `json:"classic,omitempty"` + Dangerous bool `json:"dangerous,omitempty"` + IgnoreValidation bool `json:"ignore-validation,omitempty"` +} + +type actionData struct { + Action string `json:"action"` + Name string `json:"name,omitempty"` + SnapPath string `json:"snap-path,omitempty"` + *SnapOptions +} + +type multiActionData struct { + Action string `json:"action"` + Snaps []string `json:"snaps,omitempty"` +} + +// Install adds the snap with the given name from the given channel (or +// the system default channel if not). +func (client *Client) Install(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("install", name, options) +} + +func (client *Client) InstallMany(names []string, options *SnapOptions) (changeID string, err error) { + return client.doMultiSnapAction("install", names, options) +} + +// Remove removes the snap with the given name. +func (client *Client) Remove(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("remove", name, options) +} + +func (client *Client) RemoveMany(names []string, options *SnapOptions) (changeID string, err error) { + return client.doMultiSnapAction("remove", names, options) +} + +// Refresh refreshes the snap with the given name (switching it to track +// the given channel if given). +func (client *Client) Refresh(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("refresh", name, options) +} + +func (client *Client) RefreshMany(names []string, options *SnapOptions) (changeID string, err error) { + return client.doMultiSnapAction("refresh", names, options) +} + +func (client *Client) Enable(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("enable", name, options) +} + +func (client *Client) Disable(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("disable", name, options) +} + +// Revert rolls the snap back to the previous on-disk state +func (client *Client) Revert(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("revert", name, options) +} + +var ErrDangerousNotApplicable = fmt.Errorf("dangerous option only meaningful when installing from a local file") + +func (client *Client) doSnapAction(actionName string, snapName string, options *SnapOptions) (changeID string, err error) { + if options != nil && options.Dangerous { + return "", ErrDangerousNotApplicable + } + action := actionData{ + Action: actionName, + SnapOptions: options, + } + data, err := json.Marshal(&action) + if err != nil { + return "", fmt.Errorf("cannot marshal snap action: %s", err) + } + path := fmt.Sprintf("/v2/snaps/%s", snapName) + + headers := map[string]string{ + "Content-Type": "application/json", + } + + return client.doAsync("POST", path, nil, headers, bytes.NewBuffer(data)) +} + +func (client *Client) doMultiSnapAction(actionName string, snaps []string, options *SnapOptions) (changeID string, err error) { + if options != nil { + return "", fmt.Errorf("cannot use options for multi-action") // (yet) + } + action := multiActionData{ + Action: actionName, + Snaps: snaps, + } + data, err := json.Marshal(&action) + if err != nil { + return "", fmt.Errorf("cannot marshal multi-snap action: %s", err) + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + + return client.doAsync("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data)) +} + +// InstallPath sideloads the snap with the given path, returning the UUID +// of the background operation upon success. +func (client *Client) InstallPath(path string, options *SnapOptions) (changeID string, err error) { + f, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("cannot open: %q", path) + } + + action := actionData{ + Action: "install", + SnapPath: path, + SnapOptions: options, + } + + pr, pw := io.Pipe() + mw := multipart.NewWriter(pw) + go sendSnapFile(path, f, pw, mw, &action) + + headers := map[string]string{ + "Content-Type": mw.FormDataContentType(), + } + + return client.doAsync("POST", "/v2/snaps", nil, headers, pr) +} + +// Try +func (client *Client) Try(path string, options *SnapOptions) (changeID string, err error) { + if options == nil { + options = &SnapOptions{} + } + if options.Dangerous { + return "", ErrDangerousNotApplicable + } + + buf := bytes.NewBuffer(nil) + mw := multipart.NewWriter(buf) + mw.WriteField("action", "try") + mw.WriteField("snap-path", path) + mw.WriteField("devmode", strconv.FormatBool(options.DevMode)) + mw.WriteField("jailmode", strconv.FormatBool(options.JailMode)) + mw.Close() + + headers := map[string]string{ + "Content-Type": mw.FormDataContentType(), + } + + return client.doAsync("POST", "/v2/snaps", nil, headers, buf) +} + +func sendSnapFile(snapPath string, snapFile *os.File, pw *io.PipeWriter, mw *multipart.Writer, action *actionData) { + defer snapFile.Close() + + if action.SnapOptions == nil { + action.SnapOptions = &SnapOptions{} + } + errs := []error{ + mw.WriteField("action", action.Action), + mw.WriteField("name", action.Name), + mw.WriteField("snap-path", action.SnapPath), + mw.WriteField("channel", action.Channel), + mw.WriteField("devmode", strconv.FormatBool(action.DevMode)), + mw.WriteField("jailmode", strconv.FormatBool(action.JailMode)), + mw.WriteField("classic", strconv.FormatBool(action.Classic)), + mw.WriteField("dangerous", strconv.FormatBool(action.Dangerous)), + } + for _, err := range errs { + if err != nil { + pw.CloseWithError(err) + return + } + } + + fw, err := mw.CreateFormFile("snap", filepath.Base(snapPath)) + if err != nil { + pw.CloseWithError(err) + return + } + + _, err = io.Copy(fw, snapFile) + if err != nil { + pw.CloseWithError(err) + return + } + + mw.Close() + pw.Close() +} diff --git a/client/snap_op_test.go b/client/snap_op_test.go new file mode 100644 index 00000000..62fdd4ec --- /dev/null +++ b/client/snap_op_test.go @@ -0,0 +1,295 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client_test + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime" + "mime/multipart" + "path/filepath" + "strconv" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +var chanName = "achan" + +var ops = []struct { + op func(*client.Client, string, *client.SnapOptions) (string, error) + action string +}{ + {(*client.Client).Install, "install"}, + {(*client.Client).Refresh, "refresh"}, + {(*client.Client).Remove, "remove"}, + {(*client.Client).Revert, "revert"}, + {(*client.Client).Enable, "enable"}, + {(*client.Client).Disable, "disable"}, +} + +var multiOps = []struct { + op func(*client.Client, []string, *client.SnapOptions) (string, error) + action string +}{ + {(*client.Client).RefreshMany, "refresh"}, + {(*client.Client).InstallMany, "install"}, + {(*client.Client).RemoveMany, "remove"}, +} + +func (cs *clientSuite) TestClientOpSnapServerError(c *check.C) { + cs.err = errors.New("fail") + for _, s := range ops { + _, err := s.op(cs.cli, pkgName, nil) + c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientMultiOpSnapServerError(c *check.C) { + cs.err = errors.New("fail") + for _, s := range multiOps { + _, err := s.op(cs.cli, nil, nil) + c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpSnapResponseError(c *check.C) { + cs.rsp = `{"type": "error", "status": "potatoes"}` + for _, s := range ops { + _, err := s.op(cs.cli, pkgName, nil) + c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientMultiOpSnapResponseError(c *check.C) { + cs.rsp = `{"type": "error", "status": "potatoes"}` + for _, s := range multiOps { + _, err := s.op(cs.cli, nil, nil) + c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpSnapBadType(c *check.C) { + cs.rsp = `{"type": "what"}` + for _, s := range ops { + _, err := s.op(cs.cli, pkgName, nil) + c.Check(err, check.ErrorMatches, `.*expected async response for "POST" on "/v2/snaps/`+pkgName+`", got "what"`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpSnapNotAccepted(c *check.C) { + cs.rsp = `{ + "status-code": 200, + "type": "async" + }` + for _, s := range ops { + _, err := s.op(cs.cli, pkgName, nil) + c.Check(err, check.ErrorMatches, `.*operation not accepted`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpSnapNoChange(c *check.C) { + cs.rsp = `{ + "status-code": 202, + "type": "async" + }` + for _, s := range ops { + _, err := s.op(cs.cli, pkgName, nil) + c.Assert(err, check.ErrorMatches, `.*response without change reference.*`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpSnap(c *check.C) { + cs.rsp = `{ + "change": "d728", + "status-code": 202, + "type": "async" + }` + for _, s := range ops { + id, err := s.op(cs.cli, pkgName, &client.SnapOptions{Channel: chanName}) + c.Assert(err, check.IsNil) + + c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action)) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil, check.Commentf(s.action)) + jsonBody := make(map[string]string) + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil, check.Commentf(s.action)) + c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action)) + c.Check(jsonBody["channel"], check.Equals, chanName, check.Commentf(s.action)) + c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action)) + + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps/%s", pkgName), check.Commentf(s.action)) + c.Check(id, check.Equals, "d728", check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientMultiOpSnap(c *check.C) { + cs.rsp = `{ + "change": "d728", + "status-code": 202, + "type": "async" + }` + for _, s := range multiOps { + id, err := s.op(cs.cli, []string{pkgName}, nil) + c.Assert(err, check.IsNil) + + c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action)) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil, check.Commentf(s.action)) + jsonBody := make(map[string]interface{}) + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil, check.Commentf(s.action)) + c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action)) + c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName}, check.Commentf(s.action)) + c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action)) + + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps", check.Commentf(s.action)) + c.Check(id, check.Equals, "d728", check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpInstallPath(c *check.C) { + cs.rsp = `{ + "change": "66b3", + "status-code": 202, + "type": "async" + }` + bodyData := []byte("snap-data") + + snap := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snap, bodyData, 0644) + c.Assert(err, check.IsNil) + + id, err := cs.cli.InstallPath(snap, nil) + c.Assert(err, check.IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + + c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*") + c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") + c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\nfalse\r\n.*") + + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps")) + c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") + c.Check(id, check.Equals, "66b3") +} + +func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) { + cs.rsp = `{ + "change": "66b3", + "status-code": 202, + "type": "async" + }` + bodyData := []byte("snap-data") + + snap := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snap, bodyData, 0644) + c.Assert(err, check.IsNil) + + opts := client.SnapOptions{ + Dangerous: true, + } + + // InstallPath takes Dangerous + _, err = cs.cli.InstallPath(snap, &opts) + c.Assert(err, check.IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + + c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\ntrue\r\n.*") + + // Install does not (and gives us a clear error message) + _, err = cs.cli.Install("foo", &opts) + c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) + + // nor does InstallMany (whether it fails because any option + // at all was provided, or because dangerous was provided, is + // unimportant) + _, err = cs.cli.InstallMany([]string{"foo"}, &opts) + c.Assert(err, check.NotNil) +} + +func formToMap(c *check.C, mr *multipart.Reader) map[string]string { + formData := map[string]string{} + for { + p, err := mr.NextPart() + if err == io.EOF { + break + } + c.Assert(err, check.IsNil) + slurp, err := ioutil.ReadAll(p) + c.Assert(err, check.IsNil) + formData[p.FormName()] = string(slurp) + } + return formData +} + +func (cs *clientSuite) TestClientOpTryMode(c *check.C) { + cs.rsp = `{ + "change": "66b3", + "status-code": 202, + "type": "async" + }` + snapdir := filepath.Join(c.MkDir(), "/some/path") + + for _, opts := range []*client.SnapOptions{ + {DevMode: false, JailMode: false}, + {DevMode: false, JailMode: true}, + {DevMode: true, JailMode: true}, + {DevMode: true, JailMode: false}, + } { + id, err := cs.cli.Try(snapdir, opts) + c.Assert(err, check.IsNil) + + // ensure we send the right form-data + _, params, err := mime.ParseMediaType(cs.req.Header.Get("Content-Type")) + c.Assert(err, check.IsNil) + mr := multipart.NewReader(cs.req.Body, params["boundary"]) + formData := formToMap(c, mr) + c.Check(formData, check.DeepEquals, map[string]string{ + "action": "try", + "snap-path": snapdir, + "devmode": strconv.FormatBool(opts.DevMode), + "jailmode": strconv.FormatBool(opts.JailMode), + }) + + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps")) + c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") + c.Check(id, check.Equals, "66b3") + } +} + +func (cs *clientSuite) TestClientOpTryModeDangerous(c *check.C) { + snapdir := filepath.Join(c.MkDir(), "/some/path") + + _, err := cs.cli.Try(snapdir, &client.SnapOptions{Dangerous: true}) + c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) +} diff --git a/client/snapctl.go b/client/snapctl.go new file mode 100644 index 00000000..ae36e18f --- /dev/null +++ b/client/snapctl.go @@ -0,0 +1,57 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// SnapCtlOptions holds the various options with which snapctl is invoked. +type SnapCtlOptions struct { + // ContextID is a string used to determine the context of this call (e.g. + // which context and handler should be used, etc.) + ContextID string `json:"context-id"` + + // Args contains a list of parameters to use for this invocation. + Args []string `json:"args"` +} + +type snapctlOutput struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} + +// RunSnapctl requests a snapctl run for the given options. +func (client *Client) RunSnapctl(options *SnapCtlOptions) (stdout, stderr []byte, err error) { + b, err := json.Marshal(options) + if err != nil { + return nil, nil, fmt.Errorf("cannot marshal options: %s", err) + } + + var output snapctlOutput + _, err = client.doSync("POST", "/v2/snapctl", nil, nil, bytes.NewReader(b), &output) + if err != nil { + return nil, nil, err + } + + return []byte(output.Stdout), []byte(output.Stderr), nil +} diff --git a/client/snapctl_test.go b/client/snapctl_test.go new file mode 100644 index 00000000..9ddb1a81 --- /dev/null +++ b/client/snapctl_test.go @@ -0,0 +1,68 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package client_test + +import ( + "encoding/json" + + "github.com/snapcore/snapd/client" + + "gopkg.in/check.v1" +) + +func (cs *clientSuite) TestClientRunSnapctlCallsEndpoint(c *check.C) { + options := &client.SnapCtlOptions{ + ContextID: "1234ABCD", + Args: []string{"foo", "bar"}, + } + cs.cli.RunSnapctl(options) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snapctl") +} + +func (cs *clientSuite) TestClientRunSnapctl(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": { + "stdout": "test stdout", + "stderr": "test stderr" + } + }` + + options := &client.SnapCtlOptions{ + ContextID: "1234ABCD", + Args: []string{"foo", "bar"}, + } + + stdout, stderr, err := cs.cli.RunSnapctl(options) + c.Assert(err, check.IsNil) + c.Check(string(stdout), check.Equals, "test stdout") + c.Check(string(stderr), check.Equals, "test stderr") + + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "context-id": "1234ABCD", + "args": []interface{}{"foo", "bar"}, + }) +} diff --git a/cmd/Makefile.am b/cmd/Makefile.am new file mode 100644 index 00000000..af002243 --- /dev/null +++ b/cmd/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = snap-confine +EXTRA_DIST = VERSION + +.PHONY: check-syntax +check-syntax: + shellcheck --format=gcc snap-confine/spread-tests/spread-prepare.sh diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 00000000..44a6dcb2 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,109 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package cmd + +import ( + "io/ioutil" + "os" + "path/filepath" + "regexp" + "syscall" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/strutil" +) + +// The SNAP_REEXEC environment variable controls whether the command +// will attempt to re-exec itself from inside an ubuntu-core snap +// present on the system. If not present in the environ it's assumed +// to be set to 1 (do re-exec); that is: set it to 0 to disable. +const key = "SNAP_REEXEC" + +// newCore is the place to look for the core snap; everything in this +// location will be new enough to re-exec into. +const newCore = "/snap/core/current" + +// oldCore is the previous location of the core snap. Only things +// newer than minOldRevno will be ok to re-exec into. +const oldCore = "/snap/ubuntu-core/current" + +// ExecInCoreSnap makes sure you're executing the binary that ships in +// the core snap. +func ExecInCoreSnap() { + if !release.OnClassic { + // you're already the real deal, natch + return + } + + // should we re-exec? no option in the environment means yes + if !osutil.GetenvBool(key, true) { + return + } + + exe, err := os.Readlink("/proc/self/exe") + if err != nil { + return + } + + corePath := newCore + full := filepath.Join(newCore, exe) + if !osutil.FileExists(full) { + corePath = oldCore + full = filepath.Join(oldCore, exe) + if !osutil.FileExists(full) { + return + } + } + + // ensure we do not re-exec into an older version of snapd, look + // for info file and ignore version of core that do not yet have + // it + fullInfo := filepath.Join(corePath, "/usr/lib/snapd/info") + if !osutil.FileExists(fullInfo) { + logger.Debugf("not restarting into %q (no version info): older than %q (%s)", full, exe, Version) + return + } + content, err := ioutil.ReadFile(fullInfo) + if err != nil { + logger.Noticef("cannot read info file %q: %s", fullInfo, err) + return + } + ver := regexp.MustCompile("(?m)^VERSION=(.*)$").FindStringSubmatch(string(content)) + if len(ver) != 2 { + logger.Noticef("cannot find version information in %q", content) + } + // > 0 means our Version is bigger than the version of snapd in core + res, err := strutil.VersionCompare(Version, ver[1]) + if err != nil { + logger.Debugf("cannot version compare %q and %q: %s", Version, ver[1], res) + return + } + if res > 0 { + logger.Debugf("not restarting into %q (%s): older than %q (%s)", full, ver, exe, Version) + return + } + + logger.Debugf("restarting into %q", full) + + env := append(os.Environ(), key+"=0") + panic(syscall.Exec(full, os.Args, env)) +} diff --git a/cmd/configure.ac b/cmd/configure.ac new file mode 100644 index 00000000..abe85a72 --- /dev/null +++ b/cmd/configure.ac @@ -0,0 +1,169 @@ +AC_PREREQ([2.69]) +AC_INIT([snap-confine], m4_esyscmd_s([cat VERSION]), [snapcraft@lists.ubuntu.com]) +AC_CONFIG_SRCDIR([snap-confine/snap-confine.c]) +AC_CONFIG_HEADERS([config.h]) +AC_USE_SYSTEM_EXTENSIONS +AM_INIT_AUTOMAKE([foreign]) +AM_MAINTAINER_MODE([enable]) + +# Checks for programs. +AC_PROG_CC_C99 +AC_PROG_CPP +AC_PROG_INSTALL +AC_PROG_MAKE_SET +AC_PROG_RANLIB + +AC_LANG([C]) +# Checks for libraries. + +# Checks for header files. +AC_CHECK_HEADERS([fcntl.h limits.h stdlib.h string.h sys/mount.h unistd.h]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_CHECK_HEADER_STDBOOL +AC_TYPE_UID_T +AC_TYPE_MODE_T +AC_TYPE_PID_T +AC_TYPE_SIZE_T + +# Checks for library functions. +AC_FUNC_CHOWN +AC_FUNC_ERROR_AT_LINE +AC_FUNC_FORK +AC_FUNC_STRNLEN +AC_CHECK_FUNCS([mkdir regcomp setenv strdup strerror secure_getenv]) + +AC_ARG_WITH([unit-tests], + AC_HELP_STRING([--without-unit-tests], [do not build unit test programs]), + [case "${withval}" in + yes) with_unit_tests=yes ;; + no) with_unit_tests=no ;; + *) AC_MSG_ERROR([bad value ${withval} for --without-unit-tests]) + esac], [with_unit_tests=yes]) +AM_CONDITIONAL([WITH_UNIT_TESTS], [test "x$with_unit_tests" = "xyes"]) + +# Allow to build without apparmor support by calling: +# ./configure --disable-apparmor +# This makes it possible to run snaps in devmode on almost any host, +# regardless of the kernel version. +AC_ARG_ENABLE([apparmor], + AS_HELP_STRING([--disable-apparmor], [Disable apparmor support]), + [case "${enableval}" in + yes) enable_apparmor=yes ;; + no) enable_apparmor=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --disable-apparmor]) + esac], [enable_apparmor=yes]) +AM_CONDITIONAL([APPARMOR], [test "x$enable_apparmor" = "xyes"]) + +# Allow to build without seccomp support by calling: +# ./configure --disable-seccomp +# This is separate because seccomp support is generally very good and it +# provides useful confinement for unsafe system calls. +AC_ARG_ENABLE([seccomp], + AS_HELP_STRING([--disable-seccomp], [Disable seccomp support]), + [case "${enableval}" in + yes) enable_seccomp=yes ;; + no) enable_seccomp=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --disable-seccomp]) + esac], [enable_seccomp=yes]) +AM_CONDITIONAL([SECCOMP], [test "x$enable_seccomp" = "xyes"]) + +# Enable older tests only when confinement is enabled and we're building for PC +# The tests are of smaller value as we port more and more tests to spread. +AM_CONDITIONAL([CONFINEMENT_TESTS], [test "x$enable_apparmor" = "xyes" && test "x$enable_seccomp" = "xyes" && ((test "x$host_cpu" = "xx86_64" && test "x$build_cpu" = "xx86_64") || (test "x$host_cpu" = "xi686" && test "x$build_cpu" = "xi686"))]) + +# Check for glib that we use for unit testing +AS_IF([test "x$with_unit_tests" = "xyes"], [ + PKG_CHECK_MODULES([GLIB], [glib-2.0]) +]) + +# Check if seccomp userspace library is available +AS_IF([test "x$enable_seccomp" = "xyes"], [ + PKG_CHECK_MODULES([SECCOMP], [libseccomp], [ + AC_DEFINE([HAVE_SECCOMP], [1], [Build with seccomp support])]) +]) + +# Check if apparmor userspace library is available. +AS_IF([test "x$enable_apparmor" = "xyes"], [ + PKG_CHECK_MODULES([APPARMOR], [libapparmor], [ + AC_DEFINE([HAVE_APPARMOR], [1], [Build with apparmor support])]) +], [ + AC_MSG_WARN([ + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + X X + X Apparmor is disabled, all snaps will run in devmode X + X X + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]) +]) + +# Check if udev and libudev are available. +# Those are now used unconditionally even if apparmor is disabled. +PKG_CHECK_MODULES([LIBUDEV], [libudev]) +PKG_CHECK_MODULES([UDEV], [udev]) + +# Enable special support for hosts with proprietary nvidia drivers on Ubuntu. +AC_ARG_ENABLE([nvidia-ubuntu], + AS_HELP_STRING([--enable-nvidia-ubuntu], [Support for proprietary nvidia drivers (Ubuntu)]), + [case "${enableval}" in + yes) enable_nvidia_ubuntu=yes ;; + no) enable_nvidia_ubuntu=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-nvidia-ubuntu]) + esac], [enable_nvidia_ubuntu=no]) +AM_CONDITIONAL([NVIDIA_UBUNTU], [test "x$enable_nvidia_ubuntu" = "xyes"]) + +AS_IF([test "x$enable_nvidia_ubuntu" = "xyes"], [ + AC_DEFINE([NVIDIA_UBUNTU], [1], + [Support for proprietary nvidia drivers (Ubuntu)])]) + +# Enable special support for hosts with proprietary nvidia drivers on Arch. +AC_ARG_ENABLE([nvidia-arch], + AS_HELP_STRING([--enable-nvidia-arch], [Support for proprietary nvidia drivers (Arch)]), + [case "${enableval}" in + yes) enable_nvidia_arch=yes ;; + no) enable_nvidia_arch=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-nvidia-arch]) + esac], [enable_nvidia_arch=no]) +AM_CONDITIONAL([NVIDIA_ARCH], [test "x$enable_nvidia_arch" = "xyes"]) + +AS_IF([test "x$enable_nvidia_arch" = "xyes"], [ + AC_DEFINE([NVIDIA_ARCH], [1], + [Support for proprietary nvidia drivers (Arch)])]) + +AC_ARG_ENABLE([merged-usr], + AS_HELP_STRING([--enable-merged-usr], [Enable support for merged /usr directory]), + [case "${enableval}" in + yes) enable_merged_usr=yes ;; + no) enable_merged_usr=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-merged-usr]) + esac], [enable_merged_usr=no]) +AM_CONDITIONAL([MERGED_USR], [test "x$enable_merged_usr" = "xyes"]) + +AS_IF([test "x$enable_merged_usr" = "xyes"], [ + AC_DEFINE([MERGED_USR], [1], + [Support for merged /usr directory])]) + +SNAP_MOUNT_DIR="/snap" +AC_ARG_WITH([snap-mount-dir], + AS_HELP_STRING([--with-snap-mount-dir=DIR], [Use an alternate snap mount directory]), + [SNAP_MOUNT_DIR="$withval"]) +AC_SUBST(SNAP_MOUNT_DIR) +AC_DEFINE_UNQUOTED([SNAP_MOUNT_DIR], "${SNAP_MOUNT_DIR}", [Location of the snap mount points]) + +AC_ARG_ENABLE([caps-over-setuid], + AS_HELP_STRING([--enable-caps-over-setuid], [Use capabilities rather than setuid bit]), + [case "${enableval}" in + yes) enable_caps_over_setuid=yes ;; + no) enable_caps_over_setuid=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-caps-over-setuid]) + esac], [enable_caps_over_setuid=no]) +AM_CONDITIONAL([CAPS_OVER_SETUID], [test "x$enable_caps_over_setuid" = "xyes"]) + +AS_IF([test "x$enable_caps_over_setuid" = "xyes"], [ + AC_DEFINE([CAPS_OVER_SETUID], [1], + [Use capabilities rather than setuid bit])]) + +AC_PATH_PROG([HAVE_RST2MAN],[rst2man]) +AS_IF([test "x$HAVE_RST2MAN" = "x"], [AC_MSG_ERROR(["cannot find the rst2man tool, install python-docutils or similar"])]) + +AC_CONFIG_FILES([Makefile snap-confine/Makefile snap-confine/tests/Makefile snap-confine/manpages/Makefile]) +AC_OUTPUT diff --git a/cmd/snap-confine/80-snappy-assign.rules b/cmd/snap-confine/80-snappy-assign.rules new file mode 100644 index 00000000..5285c310 --- /dev/null +++ b/cmd/snap-confine/80-snappy-assign.rules @@ -0,0 +1,2 @@ +# add/remove snap package access to assigned devices +TAG=="snap_*", RUN+="/lib/udev/snappy-app-dev $env{ACTION} $env{TAG} $devpath $major:$minor" diff --git a/cmd/snap-confine/Makefile.am b/cmd/snap-confine/Makefile.am new file mode 100644 index 00000000..49f20149 --- /dev/null +++ b/cmd/snap-confine/Makefile.am @@ -0,0 +1,193 @@ +SUBDIRS = manpages tests + +noinst_LIBRARIES = libsnap-confine-private.a + +libsnap_confine_private_a_SOURCES = \ + error.h \ + error.c \ + utils.h \ + utils.c \ + secure-getenv.c \ + secure-getenv.h + +libexec_PROGRAMS = snap-confine snap-discard-ns +noinst_PROGRAMS = decode-mount-opts +if WITH_UNIT_TESTS +noinst_PROGRAMS += snap-confine-unit-tests +endif + +decode_mount_opts_SOURCES = \ + decode-mount-opts.c \ + mount-opt.c \ + mount-opt.h + +snap_discard_ns_SOURCES = \ + ns-support.c \ + ns-support.h \ + apparmor-support.c \ + apparmor-support.h \ + cleanup-funcs.c \ + cleanup-funcs.h \ + mountinfo.c \ + mountinfo.h \ + snap-discard-ns.c +snap_discard_ns_CFLAGS = -Wall -Werror $(AM_CFLAGS) +snap_discard_ns_LDFLAGS = $(AM_LDFLAGS) +snap_discard_ns_LDADD = libsnap-confine-private.a +snap_discard_ns_CFLAGS += $(SECCOMP_CFLAGS) +snap_discard_ns_LDADD += $(SECCOMP_LIBS) + +if APPARMOR +snap_discard_ns_CFLAGS += $(APPARMOR_CFLAGS) +snap_discard_ns_LDADD += $(APPARMOR_LIBS) +endif + +snap_confine_SOURCES = \ + snap-confine.c \ + snap.c \ + snap.h \ + classic.c \ + classic.h \ + mount-support.c \ + mount-support.h \ + mount-support-nvidia.c \ + mount-support-nvidia.h \ + cleanup-funcs.c \ + cleanup-funcs.h \ + udev-support.c \ + udev-support.h \ + user-support.c \ + user-support.h \ + quirks.c \ + quirks.h \ + mount-opt.c \ + mount-opt.h \ + mountinfo.c \ + mountinfo.h \ + ns-support.c \ + ns-support.h \ + apparmor-support.c \ + apparmor-support.h + +snap_confine_CFLAGS = -Wall -Werror $(AM_CFLAGS) +snap_confine_LDFLAGS = $(AM_LDFLAGS) +snap_confine_LDADD = libsnap-confine-private.a +snap_confine_CFLAGS += $(LIBUDEV_CFLAGS) +snap_confine_LDADD += $(LIBUDEV_LIBS) + +# This is here to help fix rpmlint hardening issue. +# https://en.opensuse.org/openSUSE:Packaging_checks#non-position-independent-executable +snap_confine_CFLAGS += $(SUID_CFLAGS) +snap_confine_LDFLAGS += $(SUID_LDFLAGS) + +if SECCOMP +snap_confine_SOURCES += \ + seccomp-support.c \ + seccomp-support.h +snap_confine_CFLAGS += $(SECCOMP_CFLAGS) +snap_confine_LDADD += $(SECCOMP_LIBS) +endif + +if APPARMOR +snap_confine_CFLAGS += $(APPARMOR_CFLAGS) +snap_confine_LDADD += $(APPARMOR_LIBS) +endif + +if WITH_UNIT_TESTS +snap_confine_unit_tests_SOURCES = \ + unit-tests-main.c \ + classic.c \ + classic.h \ + quirks.c \ + quirks.h \ + unit-tests.c \ + unit-tests.h \ + utils-test.c \ + cleanup-funcs-test.c \ + mount-support-test.c \ + verify-executable-name-test.c \ + mountinfo-test.c \ + ns-support-test.c \ + apparmor-support.c \ + apparmor-support.h \ + mount-opt-test.c \ + error-test.c +snap_confine_unit_tests_CFLAGS = $(snap_confine_CFLAGS) $(GLIB_CFLAGS) +snap_confine_unit_tests_LDADD = $(snap_confine_LDADD) $(GLIB_LIBS) +snap_confine_unit_tests_LDFLAGS = $(snap_confine_LDFLAGS) +endif + +# Force particular coding style on all source and header files. +.PHONY: check-syntax +check-syntax: + @d=`mktemp -d`; \ + trap 'rm -rf $d' EXIT; \ + for f in $(wildcard $(srcdir)/*.c) $(wildcard $(srcdir)/*.h); do \ + out="$$d/`basename $$f.out`"; \ + echo "Checking $$f ... "; \ + indent -linux "$$f" -o "$$out"; \ + diff -Naur "$$f" "$$out" || exit 1; \ + done; + +.PHONY: check-unit-tests +check-unit-tests: snap-confine-unit-tests +if WITH_UNIT_TESTS + ./snap-confine-unit-tests +endif + + +# Run check-syntax when checking +# TODO: conver those to autotools-style tests later +check: check-syntax check-unit-tests + +.PHONY: fmt +fmt: + for f in $(wildcard $(srcdir)/*.c) $(wildcard $(srcdir)/*.h); do \ + echo "Formatting $$f ... "; \ + indent -linux "$$f"; \ + done; + +EXTRA_DIST = PORTING 80-snappy-assign.rules snappy-app-dev snap-confine.apparmor.in + +snap-confine.apparmor: snap-confine.apparmor.in Makefile + sed -e 's,[@]LIBEXECDIR[@],$(libexecdir),g' -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@ + +# NOTE: This makes distcheck fail but it is required for udev, so go figure. +# http://www.gnu.org/software/automake/manual/automake.html#Hard_002dCoded-Install-Paths +# +# Install udev rules and the apparmor profile +# +# NOTE: the funky make functions here just convert /foo/bar/froz into foo.bar.froz +# The inner subst replaces slashes with dots and the outer patsubst strips the leading dot +# +# NOTE: The 'void' directory *has to* be chmod 000 +install-data-local: snap-confine.apparmor + install -d -m 755 $(DESTDIR)$(shell pkg-config udev --variable=udevdir)/rules.d + install -m 644 $(srcdir)/80-snappy-assign.rules $(DESTDIR)$(shell pkg-config udev --variable=udevdir)/rules.d + install -d -m 755 $(DESTDIR)/etc/apparmor.d/ + install -m 644 snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine + install -d -m 000 $(DESTDIR)/var/lib/snapd/void + +# Install support script for udev rules +install-exec-local: + install -d -m 755 $(DESTDIR)$(shell pkg-config udev --variable=udevdir) + install -m 755 $(srcdir)/snappy-app-dev $(DESTDIR)$(shell pkg-config udev --variable=udevdir) + +install-exec-hook: +if CAPS_OVER_SETUID +# Ensure that snap-confine has CAP_SYS_ADMIN capability + setcap cap_sys_admin=pe $(DESTDIR)$(libexecdir)/snap-confine +else +# Ensure that snap-confine is +s (setuid) + chmod 4755 $(DESTDIR)$(libexecdir)/snap-confine +endif + install -d -m 755 $(DESTDIR)$(bindir) + ln -sf $(libexecdir)/snap-confine $(DESTDIR)$(bindir)/ubuntu-core-launcher + +# The hack target helps devlopers work on snap-confine on their live system by +# installing a fresh copy of snap confine and the appropriate apparmor profile. +.PHONY: hack +hack: snap-confine snap-confine.apparmor + sudo install -D -m 4755 snap-confine $(DESTDIR)$(libexecdir)/snap-confine + sudo install -m 644 snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine + sudo apparmor_parser -r snap-confine.apparmor diff --git a/cmd/snap-confine/PORTING b/cmd/snap-confine/PORTING new file mode 100644 index 00000000..43869489 --- /dev/null +++ b/cmd/snap-confine/PORTING @@ -0,0 +1,15 @@ +Welcome brave porters! + +This file is intended to guide you towards porting snappy (comprised of snapd +and this project, snap-confine) to work on a new kernel. The confinement setup by +snap-confine has several requirements on the kernel. + +TODO: list required patches (apparmor, seccomp) +TODO: list required kernel configufation +TODO: list minimum supported kernel version + +While you are working on porting those patches to your kernel of choice, you +may configure snap-confine with --disable-security. This switch drops +requirement on apparmor, seccomp and udev and reduces snap-confine to arrange +the filesystem in a correct way for snaps to operate without really confining +them in any way. diff --git a/cmd/snap-confine/README.mount_namespace b/cmd/snap-confine/README.mount_namespace new file mode 100644 index 00000000..a44e145d --- /dev/null +++ b/cmd/snap-confine/README.mount_namespace @@ -0,0 +1,138 @@ += Mount namespace setup in snap-confine = + +This document provides a terse explanation of the mount setup using syscall +traces to show precisely what is happening and show the difference between +all snaps images and classic. + +Obtain traces with (ignoring select helps keep strace from hanging): +$ sudo snap install hello-world +$ sudo /usr/lib/snapd/snap-discard-ns hello-world +$ sudo strace -f -vv -s8192 -o /tmp/trace.unshare -e trace='!select' /snap/bin/hello-world +$ sudo strace -f -vv -s8192 -o /tmp/trace.setns -e trace='!select' /snap/bin/hello-world + +Examine /tmp/trace.unshare for initial mount namespace setup and +/tmp/trace.setns for seeing how the mount namespace is reused on subsequent +runs. Note that running /usr/lib/snapd/snap-discard-ns prior to running the +command is required for creating the new mount namespace (otherwise the +previous mount namespace will be reused). + + += Mount namespace setup in detail = +Here are the steps snap-confine takes when setting up the mount namespace for a +given snap: + +# Create the /run/snapd/ns directory to save off the mount namespace to be +# shared on other app-invocations +open("/", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 3 +mkdirat(3, "run", 0755) = -1 EEXIST (File exists) +openat(3, "run", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 4 +mkdirat(4, "snapd", 0755) = -1 EEXIST (File exists) +openat(4, "snapd", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 3 +mkdirat(3, "ns", 0755) = -1 EEXIST (File exists) +openat(3, "ns", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 4 + +# If /run/snapd/ns/.mnt exists, enter that namespace: +openat(3, "hello-world.mnt", O_RDONLY|O_CREAT|O_NOFOLLOW|O_CLOEXEC, 0600) = 5 +fstatfs(5, {f_type=0x6e736673, ...) = 0 +setns(5, CLONE_NEWNS) = 0 +... mount namespace setup finished, go on to setup the rest of the sandbox ... + + +# Otherwise, create a new mount namespace +unshare(CLONE_NEWNS) +mount("none", "/", NULL, MS_REC|MS_SLAVE, NULL) = 0 + +# Classic-only - mount rootfs in the namespace +mkdir("/tmp/snap.rootfs_HkQghZ", 0700) = 0 +mount("/snap/ubuntu-core/current", "/tmp/snap.rootfs_HkQghZ", NULL, MS_BIND, NULL) = 0 + +# Classic only - mount directories from host over rootfs +mount("/dev", "/tmp/snap.rootfs_HkQghZ/dev", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/etc", "/tmp/snap.rootfs_HkQghZ/etc", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/home", "/tmp/snap.rootfs_HkQghZ/home", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/root", "/tmp/snap.rootfs_HkQghZ/root", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/proc", "/tmp/snap.rootfs_HkQghZ/proc", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/sys", "/tmp/snap.rootfs_HkQghZ/sys", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/tmp", "/tmp/snap.rootfs_HkQghZ/tmp", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/var/snap", "/tmp/snap.rootfs_HkQghZ/var/snap", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/var/lib/snapd", "/tmp/snap.rootfs_HkQghZ/var/lib/snapd", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/var/tmp", "/tmp/snap.rootfs_HkQghZ/var/tmp", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/run", "/tmp/snap.rootfs_HkQghZ/run", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/media", "/tmp/snap.rootfs_HkQghZ/media", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/lib/modules", "/tmp/snap.rootfs_HkQghZ/lib/modules", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/usr/src", "/tmp/snap.rootfs_HkQghZ/usr/src", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/var/log", "/tmp/snap.rootfs_HkQghZ/var/log", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/snap", "/tmp/snap.rootfs_HkQghZ/snap", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/snap/ubuntu-core/current/etc/alternatives", "/tmp/snap.rootfs_HkQghZ/etc/alternatives", NULL, MS_BIND|MS_SLAVE, NULL) = 0 +mount("/", "/tmp/snap.rootfs_HkQghZ/var/lib/snapd/hostfs", NULL, MS_RDONLY|MS_BIND, NULL) = 0 + +# Classic only - pivot_root into the rootfs +pivot_root(".", ".") = 0 +umount2(".", MNT_DETACH) = 0 + +# Create a bind-mounted private /tmp +mkdir("/tmp/snap.0_snap.hello-world.hello-world_QXGSt1", 0700) = 0 +mkdir("/tmp/snap.0_snap.hello-world.hello-world_QXGSt1/tmp", 01777) = 0 +mount("/tmp/snap.0_snap.hello-world.hello-world_QXGSt1/tmp", "/tmp", NULL, MS_BIND, NULL) = 0 +mount("none", "/tmp", NULL, MS_PRIVATE, NULL) = 0 + +# Create a per-snap /dev/pts +mount("devpts", "/dev/pts", "devpts", MS_MGC_VAL, "newinstance,ptmxmode=0666,mode=0"...) +mount("/dev/pts/ptmx", "/dev/ptmx", 0x5574dfe9a5c3, MS_BIND, NULL) + +# Classic only - process quirks mounts by: +# creating temporary quirks directory for moving /var/lib/snapd aside +mkdir("/tmp/snapd.quirks_xKIzG3", 0700) = 0 +# moving /var/lib/snapd aside +mount("/var/lib/snapd", "/tmp/snapd.quirks_xKIzG3", NULL, MS_MOVE, NULL) = 0 +# creating a tmpfs on /var/lib for our mount points +mount("none", "/var/lib", "tmpfs", MS_NOSUID|MS_NODEV, NULL) = 0 +# mimicking the vanilla /var/lib/* from the core snap in /var/lib in tmpfs +# (the directories to mimic are dynamically determined and will vary as the +# core snap changes. Syscalls for finding what to mount and creating the +# mount points are omitted) +mount("/snap/ubuntu-core/current/var/lib/apparmor", "/var/lib/apparmor", NULL, MS_RDONLY|MS_NOSUID|MS_NODEV|MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/snap/ubuntu-core/current/var/lib/classic", "/var/lib/classic", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/console-conf", "/var/lib/console-conf", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/dbus", "/var/lib/dbus", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/dhcp", "/var/lib/dhcp", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/extrausers", "/var/lib/extrausers", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/initramfs-tools", "/var/lib/initramfs-tools", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/initscripts", "/var/lib/initscripts", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/insserv", "/var/lib/insserv", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/logrotate", "/var/lib/logrotate", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/machines", "/var/lib/machines", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/misc", "/var/lib/misc", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/pam", "/var/lib/pam", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/python", "/var/lib/python", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/resolvconf", "/var/lib/resolvconf", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/snapd", "/var/lib/snapd", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/sudo", "/var/lib/sudo", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/systemd", "/var/lib/systemd", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/ubuntu-fan", "/var/lib/ubuntu-fan", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/ucf", "/var/lib/ucf", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/update-rc.d", "/var/lib/update-rc.d", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/urandom", "/var/lib/urandom", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/vim", "/var/lib/vim", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/waagent", "/var/lib/waagent", ...) = 0 +# unmounting the /var/lib/snapd that was just mimicked +umount2("/var/lib/snapd", 0) +# moving back the /var/lib/snapd that was set aside +mount("/tmp/snapd.quirks_xKIzG3", "/var/lib/snapd", NULL, MS_MOVE, NULL) = 0 +# cleaning up the temporary directory +rmdir("/tmp/snapd.quirks_xKIzG3") = 0 +# applying the actual quirk mounts as needed (for now, lxd, but more may +# come). Eg: +mount("/var/lib/snapd/hostfs/var/lib/lxd", "/var/lib/lxd", NULL, MS_REC|MS_SLAVE|MS_NODEV|MS_NOSUID|MS_NOEXEC) = 0 +# End quirk mounts on classic + +# Process snap-defined mounts (eg, for content interface, mount the source to +# the target as defined in /var/lib/snapd/mount/snap...fstab) +# Eg: +mount("/snap/some-content-snap/current/src", "/snap/hello-world/current/dst", NULL, MS_RDONLY|MS_NOSUID|MS_NODEV|MS_BIND, NULL) + +# Bind mount this namespace to the application-specific NSFS magic file to +# preserve it across snap invocations (an fchdir() happened just after the +# unshare(), above). +mount("/proc/12887/ns/mnt", "hello-world.mnt", NULL, MS_BIND, NULL) = 0 +... mount namespace setup finished, go on to setup the rest of the sandbox ... diff --git a/cmd/snap-confine/README.nvidia b/cmd/snap-confine/README.nvidia new file mode 100644 index 00000000..bdcae4c2 --- /dev/null +++ b/cmd/snap-confine/README.nvidia @@ -0,0 +1,26 @@ +Nvidia on Arch +============== + +On Arch nvidia support differs depending on the version of the driver user. +Free drivers should work out of the box without any changes. Proprietary +drivers were tested on the following driver versions: + +nvidia-340xx 340.96-13 +nvidia-340xx-libgl 340.96-1 +nvidia-340xx-utils 340.96-1 + +The way the driver stack works was changed significantly in driver 364 and that +version does not yet work correctly (we will gladly take patches if you beat us +to the punch!). There is some ongoing work but it needs more investigation. + +Nvidia on Ubuntu +================ + +On Ubuntu nvidia drivers are provided in a different way and we believe that +all versions work correctly. + +Nvidia on $DISTRO +================= + +Free drivers should work everywhere. Support for proprietary drivers will be +added on a case-by-case basis. diff --git a/cmd/snap-confine/README.syscalls b/cmd/snap-confine/README.syscalls new file mode 100644 index 00000000..fee72df1 --- /dev/null +++ b/cmd/snap-confine/README.syscalls @@ -0,0 +1,436 @@ +To get all the syscalls, grab all the linux-libc-dev packages for all the +architectures (eg, amd64, arm64, armhf, i386, powerpc, ppc64el) and put then +in a directory. Then: + +mkdir extracted +for i in ./*deb ; do + dpkg-deb -x $i ./extracted +done + +for i in `find . -name "unistd*.h"|grep gnu` ; do egrep '^#define .*_NR_([a-z0-9_\-]*)' $i | awk '{print $2}' | sed 's/.*_NR_//' ; done|sort -u + +NOTE: syscall() isn't actually a syscall, it is a glibc wrapping to reference +a syscall by number (therefore, it should be omitted from filter policy). ARM +OABI did define this, but it has been obsoleted in EABI. + +For example, on Ubuntu 16.04 with the 4.4.0-16.32 Linux kernel, these are the +syscalls: +accept +accept4 +access +acct +add_key +adjtimex +afs_syscall +alarm +arch_prctl +arm_fadvise64_64 +arm_sync_file_range +bdflush +bind +bpf +break +breakpoint +brk +cacheflush +capget +capset +chdir +chmod +chown +chown32 +chroot +clock_adjtime +clock_getres +clock_gettime +clock_nanosleep +clock_settime +clone +close +connect +creat +create_module +delete_module +dup +dup2 +dup3 +epoll_create +epoll_create1 +epoll_ctl +epoll_ctl_old +epoll_pwait +epoll_wait +epoll_wait_old +eventfd +eventfd2 +execve +execveat +exit +exit_group +faccessat +fadvise64 +fadvise64_64 +fallocate +fanotify_init +fanotify_mark +fchdir +fchmod +fchmodat +fchown +fchown32 +fchownat +fcntl +fcntl64 +fdatasync +fgetxattr +finit_module +flistxattr +flock +fork +fremovexattr +fsetxattr +fstat +fstat64 +fstatat64 +fstatfs +fstatfs64 +fsync +ftime +ftruncate +ftruncate64 +futex +futimesat +getcpu +getcwd +getdents +getdents64 +getegid +getegid32 +geteuid +geteuid32 +getgid +getgid32 +getgroups +getgroups32 +getitimer +get_kernel_syms +get_mempolicy +getpeername +getpgid +getpgrp +getpid +getpmsg +getppid +getpriority +getrandom +getresgid +getresgid32 +getresuid +getresuid32 +getrlimit +get_robust_list +getrusage +getsid +getsockname +getsockopt +get_thread_area +gettid +gettimeofday +getuid +getuid32 +getxattr +gtty +idle +init_module +inotify_add_watch +inotify_init +inotify_init1 +inotify_rm_watch +io_cancel +ioctl +io_destroy +io_getevents +ioperm +iopl +ioprio_get +ioprio_set +io_setup +io_submit +ipc +kcmp +kexec_file_load +kexec_load +keyctl +kill +lchown +lchown32 +lgetxattr +link +linkat +listen +listxattr +llistxattr +_llseek +lock +lookup_dcookie +lremovexattr +lseek +lsetxattr +lstat +lstat64 +madvise +mbind +membarrier +memfd_create +migrate_pages +mincore +mkdir +mkdirat +mknod +mknodat +mlock +mlock2 +mlockall +mmap +mmap2 +modify_ldt +mount +move_pages +mprotect +mpx +mq_getsetattr +mq_notify +mq_open +mq_timedreceive +mq_timedsend +mq_unlink +mremap +msgctl +msgget +msgrcv +msgsnd +msync +multiplexer +munlock +munlockall +munmap +name_to_handle_at +nanosleep +newfstatat +_newselect +nfsservctl +nice +oldfstat +oldlstat +oldolduname +oldstat +olduname +open +openat +open_by_handle_at +pause +pciconfig_iobase +pciconfig_read +pciconfig_write +perf_event_open +personality +pipe +pipe2 +pivot_root +poll +ppoll +prctl +pread64 +preadv +prlimit64 +process_vm_readv +process_vm_writev +prof +profil +pselect6 +ptrace +putpmsg +pwrite64 +pwritev +query_module +quotactl +read +readahead +readdir +readlink +readlinkat +readv +reboot +recv +recvfrom +recvmmsg +recvmsg +remap_file_pages +removexattr +rename +renameat +renameat2 +request_key +restart_syscall +rmdir +rtas +rt_sigaction +rt_sigpending +rt_sigprocmask +rt_sigqueueinfo +rt_sigreturn +rt_sigsuspend +rt_sigtimedwait +rt_tgsigqueueinfo +s390_pci_mmio_read +s390_pci_mmio_write +s390_runtime_instr +sched_getaffinity +sched_getattr +sched_getparam +sched_get_priority_max +sched_get_priority_min +sched_getscheduler +sched_rr_get_interval +sched_setaffinity +sched_setattr +sched_setparam +sched_setscheduler +sched_yield +seccomp +security +select +semctl +semget +semop +semtimedop +send +sendfile +sendfile64 +sendmmsg +sendmsg +sendto +setdomainname +setfsgid +setfsgid32 +setfsuid +setfsuid32 +setgid +setgid32 +setgroups +setgroups32 +sethostname +setitimer +set_mempolicy +setns +setpgid +setpriority +setregid +setregid32 +setresgid +setresgid32 +setresuid +setresuid32 +setreuid +setreuid32 +setrlimit +set_robust_list +setsid +setsockopt +set_thread_area +set_tid_address +settimeofday +set_tls +setuid +setuid32 +setxattr +sgetmask +shmat +shmctl +shmdt +shmget +shutdown +sigaction +sigaltstack +signal +signalfd +signalfd4 +sigpending +sigprocmask +sigreturn +sigsuspend +socket +socketcall +socketpair +splice +spu_create +spu_run +ssetmask +stat +stat64 +statfs +statfs64 +stime +stty +subpage_prot +swapcontext +swapoff +swapon +switch_endian +symlink +symlinkat +sync +sync_file_range +sync_file_range2 +syncfs +syscall +_sysctl +sys_debug_setcontext +sysfs +sysinfo +syslog +tee +tgkill +time +timer_create +timer_delete +timerfd +timerfd_create +timerfd_gettime +timerfd_settime +timer_getoverrun +timer_gettime +timer_settime +times +tkill +truncate +truncate64 +tuxcall +ugetrlimit +ulimit +umask +umount +umount2 +uname +unlink +unlinkat +unshare +uselib +userfaultfd +usr26 +usr32 +ustat +utime +utimensat +utimes +vfork +vhangup +vm86 +vm86old +vmsplice +vserver +wait4 +waitid +waitpid +write +writev diff --git a/cmd/snap-confine/apparmor-support.c b/cmd/snap-confine/apparmor-support.c new file mode 100644 index 00000000..e325e7a3 --- /dev/null +++ b/cmd/snap-confine/apparmor-support.c @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "apparmor-support.h" + +#include +#include +#ifdef HAVE_APPARMOR +#include +#endif // ifdef HAVE_APPARMOR + +#include "cleanup-funcs.h" +#include "utils.h" + +// NOTE: Those constants map exactly what apparmor is returning and cannot be +// changed without breaking apparmor functionality. +#define SC_AA_ENFORCE_STR "enforce" +#define SC_AA_COMPLAIN_STR "complain" +#define SC_AA_MIXED_STR "mixed" +#define SC_AA_UNCONFINED_STR "unconfined" + +void sc_init_apparmor_support(struct sc_apparmor *apparmor) +{ +#ifdef HAVE_APPARMOR + // Use aa_is_enabled() to see if apparmor is available in the kernel and + // enabled at boot time. If it isn't log a diagnostic message and assume + // we're not confined. + if (aa_is_enabled() != true) { + switch (errno) { + case ENOSYS: + debug + ("apparmor extensions to the system are not available"); + break; + case ECANCELED: + debug + ("apparmor is available on the system but has been disabled at boot"); + break; + case ENOENT: + debug + ("apparmor is available but the interface but the interface is not available"); + case EPERM: + // NOTE: fall-through + case EACCES: + debug + ("insufficient permissions to determine if apparmor is enabled"); + break; + default: + debug("apparmor is not enabled: %s", strerror(errno)); + break; + } + apparmor->is_confined = false; + apparmor->mode = SC_AA_NOT_APPLICABLE; + return; + } + // Use aa_getcon() to check the label of the current process and + // confinement type. Note that the returned label must be released with + // free() but the mode is a constant string that must not be freed. + char *label __attribute__ ((cleanup(sc_cleanup_string))) = NULL; + char *mode = NULL; + if (aa_getcon(&label, &mode) < 0) { + die("cannot query current apparmor profile"); + } + // The label has a special value "unconfined" that is applied to all + // processes without a dedicated profile. If that label is used then the + // current process is not confined. All other labels imply confinement. + if (label != NULL && strcmp(label, SC_AA_UNCONFINED_STR) == 0) { + apparmor->is_confined = false; + } else { + apparmor->is_confined = true; + } + // There are several possible results for the confinement type (mode) that + // are checked for below. + if (mode != NULL && strcmp(mode, SC_AA_COMPLAIN_STR) == 0) { + apparmor->mode = SC_AA_COMPLAIN; + } else if (mode != NULL && strcmp(mode, SC_AA_ENFORCE_STR) == 0) { + apparmor->mode = SC_AA_ENFORCE; + } else if (mode != NULL && strcmp(mode, SC_AA_MIXED_STR) == 0) { + apparmor->mode = SC_AA_MIXED; + } else { + apparmor->mode = SC_AA_INVALID; + } +#else + apparmor->mode = SC_AA_NOT_APPLICABLE; + apparmor->is_confined = false; +#endif // ifdef HAVE_APPARMOR +} + +void +sc_maybe_aa_change_onexec(struct sc_apparmor *apparmor, const char *profile) +{ +#ifdef HAVE_APPARMOR + debug("requesting changing of apparmor profile on next exec to %s", + profile); + if (aa_change_onexec(profile) < 0) { + if (secure_getenv("SNAPPY_LAUNCHER_INSIDE_TESTS") == NULL) { + die("cannot change profile for the next exec call"); + } + } +#endif // ifdef HAVE_APPARMOR +} + +void +sc_maybe_aa_change_hat(struct sc_apparmor *apparmor, + const char *subprofile, unsigned long magic_token) +{ +#ifdef HAVE_APPARMOR + if (apparmor->is_confined) { + debug("changing apparmor hat to %s", subprofile); + if (aa_change_hat(subprofile, magic_token) < 0) { + die("cannot change apparmor hat"); + } + } +#endif // ifdef HAVE_APPARMOR +} diff --git a/cmd/snap-confine/apparmor-support.h b/cmd/snap-confine/apparmor-support.h new file mode 100644 index 00000000..b90f285c --- /dev/null +++ b/cmd/snap-confine/apparmor-support.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#ifndef SNAP_CONFINE_APPARMOR_SUPPORT_H +#define SNAP_CONFINE_APPARMOR_SUPPORT_H + +#include + +/** + * Type of apparmor confinement. + **/ +enum sc_apparmor_mode { + // The enforcement mode was not recognized. + SC_AA_INVALID = -1, + // The enforcement mode is not applicable because apparmor is disabled. + SC_AA_NOT_APPLICABLE = 0, + // The enforcement mode is "enforcing" + SC_AA_ENFORCE = 1, + // The enforcement mode is "complain" + SC_AA_COMPLAIN, + // The enforcement mode is "mixed" + SC_AA_MIXED, +}; + +/** + * Data required to manage apparmor wrapper. + **/ +struct sc_apparmor { + // The mode of enforcement. In addition to the two apparmor defined modes + // can be also SC_AA_INVALID (unknown mode reported by apparmor) and + // SC_AA_NOT_APPLICABLE (when we're not linked with apparmor). + enum sc_apparmor_mode mode; + // Flag indicating that the current process is confined. + bool is_confined; +}; + +/** + * Initialize apparmor support. + * + * This operation should be done even when apparmor support is disabled at + * compile time. Internally the supplied structure is initialized based on the + * information returned from aa_getcon(2) or if apparmor is disabled at compile + * time, with built-in constants. + * + * The main action performed here is to check if snap-confine is currently + * confined, this information is used later in sc_maybe_change_apparmor_hat() + * + * As with many functions in the snap-confine tree, all errors result in + * process termination. + **/ +void sc_init_apparmor_support(struct sc_apparmor *apparmor); + +/** + * Maybe call aa_change_onexec(2) + * + * This function does nothing when apparmor support is not enabled at compile + * time. If apparmor is enabled then profile change request is attempted. + * + * As with many functions in the snap-confine tree, all errors result in + * process termination. As an exception, when SNAPPY_LAUNCHER_INSIDE_TESTS + * environment variable is set then the process is not terminated. + **/ +void +sc_maybe_aa_change_onexec(struct sc_apparmor *apparmor, const char *profile); + +/** + * Maybe call aa_change_hat(2) + * + * This function does nothing when apparmor support is not enabled at compile + * time. If apparmor is enabled then hat change is attempted. + * + * As with many functions in the snap-confine tree, all errors result in + * process termination. + **/ +void +sc_maybe_aa_change_hat(struct sc_apparmor *apparmor, + const char *subprofile, unsigned long magic_token); + +#endif diff --git a/cmd/snap-confine/classic.c b/cmd/snap-confine/classic.c new file mode 100644 index 00000000..2a3196c3 --- /dev/null +++ b/cmd/snap-confine/classic.c @@ -0,0 +1,15 @@ +#include "config.h" +#include "classic.h" + +#include + +bool is_running_on_classic_distribution() +{ + // NOTE: keep this list sorted please + return false + || access("/var/lib/dpkg/status", F_OK) == 0 + || access("/var/lib/pacman", F_OK) == 0 + || access("/var/lib/portage", F_OK) == 0 + || access("/var/lib/rpm", F_OK) == 0 + || access("/sbin/procd", F_OK) == 0; +} diff --git a/cmd/snap-confine/classic.h b/cmd/snap-confine/classic.h new file mode 100644 index 00000000..8927979a --- /dev/null +++ b/cmd/snap-confine/classic.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 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 . + * + */ +#ifndef SNAP_CONFINE_CLASSIC_H +#define SNAP_CONFINE_CLASSIC_H + +#include + +// Location of the host filesystem directory in the core snap. +#define SC_HOSTFS_DIR "/var/lib/snapd/hostfs" + +bool is_running_on_classic_distribution(); + +#endif diff --git a/cmd/snap-confine/cleanup-funcs-test.c b/cmd/snap-confine/cleanup-funcs-test.c new file mode 100644 index 00000000..7283d2dd --- /dev/null +++ b/cmd/snap-confine/cleanup-funcs-test.c @@ -0,0 +1,41 @@ +/* + * Copyright (C) 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 . + * + */ + +#include "cleanup-funcs.h" +#include "cleanup-funcs.c" + +#include + +// Test that cleanup functions are applied as expected +static void test_cleanup_sanity() +{ + int called = 0; + void fn(int *ptr) { + called = 1; + } + { + int test __attribute__ ((cleanup(fn))); + test = 0; + test++; + } + g_assert_cmpint(called, ==, 1); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/cleanup/sanity", test_cleanup_sanity); +} diff --git a/cmd/snap-confine/cleanup-funcs.c b/cmd/snap-confine/cleanup-funcs.c new file mode 100644 index 00000000..9b5ceadd --- /dev/null +++ b/cmd/snap-confine/cleanup-funcs.c @@ -0,0 +1,57 @@ +/* + * Copyright (C) 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 . + * + */ + +#include "cleanup-funcs.h" + +#include +#include + +void sc_cleanup_string(char **ptr) +{ + free(*ptr); +} + +void sc_cleanup_file(FILE ** ptr) +{ + if (*ptr != NULL) + fclose(*ptr); +} + +void sc_cleanup_endmntent(FILE ** ptr) +{ + if (*ptr != NULL) + endmntent(*ptr); +} + +#ifdef HAVE_SECCOMP +void sc_cleanup_seccomp_release(scmp_filter_ctx * ptr) +{ + seccomp_release(*ptr); +} +#endif // HAVE_SECCOMP + +void sc_cleanup_closedir(DIR ** ptr) +{ + if (*ptr != NULL) { + closedir(*ptr); + } +} + +void sc_cleanup_close(int *ptr) +{ + close(*ptr); +} diff --git a/cmd/snap-confine/cleanup-funcs.h b/cmd/snap-confine/cleanup-funcs.h new file mode 100644 index 00000000..ca0d37ca --- /dev/null +++ b/cmd/snap-confine/cleanup-funcs.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 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 . + * + */ + +#ifndef SNAP_CONFINE_CLEANUP_FUNCS_H +#define SNAP_CONFINE_CLEANUP_FUNCS_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#include +#include +#ifdef HAVE_SECCOMP +#include +#endif // HAVE_SECCOMP +#include +#include + +/** + * Free a dynamically allocated string. + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_string))). + **/ +void sc_cleanup_string(char **ptr); + +/** + * Close an open file. + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_file))). + **/ +void sc_cleanup_file(FILE ** ptr); + +/** + * Close an open file with endmntent(3) + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_endmntent))). + **/ +void sc_cleanup_endmntent(FILE ** ptr); + +#ifdef HAVE_SECCOMP +/** + * Release a seccomp context with seccomp_release(3) + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_seccomp_release))). + **/ +void sc_cleanup_seccomp_release(scmp_filter_ctx * ptr); +#endif // HAVE_SECCOMP + +/** + * Close an open directory with closedir(3) + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_closedir))). + **/ +void sc_cleanup_closedir(DIR ** ptr); + +/** + * Close an open file descriptor with close(2) + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_close))). + **/ +void sc_cleanup_close(int *ptr); + +#endif diff --git a/cmd/snap-confine/decode-mount-opts.c b/cmd/snap-confine/decode-mount-opts.c new file mode 100644 index 00000000..d077bb1f --- /dev/null +++ b/cmd/snap-confine/decode-mount-opts.c @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#include +#include + +#include "mount-opt.h" + +int main(int argc, char *argv[]) +{ + if (argc != 2) { + printf("usage: decode-mount-opts OPT\n"); + return 0; + } + char *end; + unsigned long mountflags = strtoul(argv[1], &end, 0); + if (*end != '\0') { + fprintf(stderr, "cannot parse given argument as a number\n"); + return 1; + } + printf("%#lx is %s\n", mountflags, sc_mount_opt2str(mountflags)); + return 0; +} diff --git a/cmd/snap-confine/error-test.c b/cmd/snap-confine/error-test.c new file mode 100644 index 00000000..9ca7e089 --- /dev/null +++ b/cmd/snap-confine/error-test.c @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#include "error.h" +#include "error.c" + +#include +#include + +static void test_sc_error_init() +{ + struct sc_error *err; + // Create an error + err = sc_error_init("domain", 42, "printer is on %s", "fire"); + g_assert_nonnull(err); + g_test_queue_destroy((GDestroyNotify) sc_error_free, err); + + // Inspect the exposed attributes + g_assert_cmpstr(sc_error_domain(err), ==, "domain"); + g_assert_cmpint(sc_error_code(err), ==, 42); + g_assert_cmpstr(sc_error_msg(err), ==, "printer is on fire"); +} + +static void test_sc_error_init_from_errno() +{ + struct sc_error *err; + // Create an error + err = sc_error_init_from_errno(ENOENT, "printer is on %s", "fire"); + g_assert_nonnull(err); + g_test_queue_destroy((GDestroyNotify) sc_error_free, err); + + // Inspect the exposed attributes + g_assert_cmpstr(sc_error_domain(err), ==, SC_ERRNO_DOMAIN); + g_assert_cmpint(sc_error_code(err), ==, ENOENT); + g_assert_cmpstr(sc_error_msg(err), ==, "printer is on fire"); +} + +static void test_sc_error_cleanup() +{ + // Check that sc_error_cleanup() is safe to use. + + // Cleanup is safe on NULL errors. + struct sc_error *err = NULL; + sc_cleanup_error(&err); + + // Cleanup is safe on non-NULL errors. + err = sc_error_init("domain", 123, "msg"); + g_assert_nonnull(err); + sc_cleanup_error(&err); + g_assert_null(err); +} + +static void test_sc_error_domain__NULL() +{ + // Check that sc_error_domain() dies if called with NULL error. + if (g_test_subprocess()) { + // NOTE: the code below fools gcc 5.4 but your mileage may vary. + struct sc_error *err = NULL; + const char *domain = sc_error_domain(err); + (void)(domain); + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot obtain error domain from NULL error\n"); +} + +static void test_sc_error_code__NULL() +{ + // Check that sc_error_code() dies if called with NULL error. + if (g_test_subprocess()) { + // NOTE: the code below fools gcc 5.4 but your mileage may vary. + struct sc_error *err = NULL; + int code = sc_error_code(err); + (void)(code); + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot obtain error code from NULL error\n"); +} + +static void test_sc_error_msg__NULL() +{ + // Check that sc_error_msg() dies if called with NULL error. + if (g_test_subprocess()) { + // NOTE: the code below fools gcc 5.4 but your mileage may vary. + struct sc_error *err = NULL; + const char *msg = sc_error_msg(err); + (void)(msg); + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot obtain error message from NULL error\n"); +} + +static void test_sc_die_on_error__NULL() +{ + // Check that sc_die_on_error() does nothing if called with NULL error. + if (g_test_subprocess()) { + sc_die_on_error(NULL); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_passed(); +} + +static void test_sc_die_on_error__regular() +{ + // Check that sc_die_on_error() dies if called with an error. + if (g_test_subprocess()) { + struct sc_error *err = + sc_error_init("domain", 42, "just testing"); + sc_die_on_error(err); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("just testing\n"); +} + +static void test_sc_die_on_error__errno() +{ + // Check that sc_die_on_error() dies if called with an errno-based error. + if (g_test_subprocess()) { + struct sc_error *err = + sc_error_init_from_errno(ENOENT, "just testing"); + sc_die_on_error(err); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("just testing: No such file or directory\n"); +} + +static void test_sc_error_forward__nothing() +{ + // Check that forwarding NULL does exactly that. + struct sc_error *recipient = (void *)0xDEADBEEF; + struct sc_error *err = NULL; + sc_error_forward(&recipient, err); + g_assert_null(recipient); +} + +static void test_sc_error_forward__something_somewhere() +{ + // Check that forwarding a real error works OK. + struct sc_error *recipient = NULL; + struct sc_error *err = sc_error_init("domain", 42, "just testing"); + g_test_queue_destroy((GDestroyNotify) sc_error_free, err); + g_assert_nonnull(err); + sc_error_forward(&recipient, err); + g_assert_nonnull(recipient); +} + +static void test_sc_error_forward__something_nowhere() +{ + // Check that forwarding a real error nowhere calls die() + if (g_test_subprocess()) { + // NOTE: the code below fools gcc 5.4 but your mileage may vary. + struct sc_error **err_ptr = NULL; + struct sc_error *err = + sc_error_init("domain", 42, "just testing"); + g_test_queue_destroy((GDestroyNotify) sc_error_free, err); + g_assert_nonnull(err); + sc_error_forward(err_ptr, err); + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("just testing\n"); +} + +static void test_sc_error_match__typical() +{ + // NULL error doesn't match anything. + g_assert_false(sc_error_match(NULL, "domain", 42)); + + // Non-NULL error matches if domain and code both match. + struct sc_error *err = sc_error_init("domain", 42, "just testing"); + g_test_queue_destroy((GDestroyNotify) sc_error_free, err); + g_assert_true(sc_error_match(err, "domain", 42)); + g_assert_false(sc_error_match(err, "domain", 1)); + g_assert_false(sc_error_match(err, "other-domain", 42)); + g_assert_false(sc_error_match(err, "other-domain", 1)); +} + +static void test_sc_error_match__NULL_domain() +{ + // Using a NULL domain is a fatal bug. + if (g_test_subprocess()) { + // NOTE: the code below fools gcc 5.4 but your mileage may vary. + struct sc_error *err = NULL; + const char *domain = NULL; + g_assert_false(sc_error_match(err, domain, 42)); + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot match error to a NULL domain\n"); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/error/sc_error_init", test_sc_error_init); + g_test_add_func("/error/sc_error_init_from_errno", + test_sc_error_init_from_errno); + g_test_add_func("/error/sc_error_cleanup", test_sc_error_cleanup); + g_test_add_func("/error/sc_error_domain/NULL", + test_sc_error_domain__NULL); + g_test_add_func("/error/sc_error_code/NULL", test_sc_error_code__NULL); + g_test_add_func("/error/sc_error_msg/NULL", test_sc_error_msg__NULL); + g_test_add_func("/error/sc_die_on_error/NULL", + test_sc_die_on_error__NULL); + g_test_add_func("/error/sc_die_on_error/regular", + test_sc_die_on_error__regular); + g_test_add_func("/error/sc_die_on_error/errno", + test_sc_die_on_error__errno); + g_test_add_func("/error/sc_error_formward/nothing", + test_sc_error_forward__nothing); + g_test_add_func("/error/sc_error_formward/something_somewhere", + test_sc_error_forward__something_somewhere); + g_test_add_func("/error/sc_error_formward/something_nowhere", + test_sc_error_forward__something_nowhere); + g_test_add_func("/error/sc_error_match/typical", + test_sc_error_match__typical); + g_test_add_func("/error/sc_error_match/NULL_domain", + test_sc_error_match__NULL_domain); +} diff --git a/cmd/snap-confine/error.c b/cmd/snap-confine/error.c new file mode 100644 index 00000000..21faaf76 --- /dev/null +++ b/cmd/snap-confine/error.c @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2016 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 . + * + */ +#include "error.h" + +// To get vasprintf +#define _GNU_SOURCE + +#include "utils.h" + +#include +#include +#include +#include + +struct sc_error { + // Error domain defines a scope for particular error codes. + const char *domain; + // Code differentiates particular errors for the programmer. + // The code may be zero if the particular meaning is not relevant. + int code; + // Message carries a formatted description of the problem. + char *msg; +}; + +static struct sc_error *sc_error_initv(const char *domain, int code, + const char *msgfmt, va_list ap) +{ + struct sc_error *err = calloc(1, sizeof *err); + if (err == NULL) { + die("cannot allocate memory for error object"); + } + err->domain = domain; + err->code = code; + if (vasprintf(&err->msg, msgfmt, ap) == -1) { + die("cannot format error message"); + } + return err; +} + +struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, + ...) +{ + va_list ap; + va_start(ap, msgfmt); + struct sc_error *err = sc_error_initv(domain, code, msgfmt, ap); + va_end(ap); + return err; +} + +struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, + ...) +{ + va_list ap; + va_start(ap, msgfmt); + struct sc_error *err = + sc_error_initv(SC_ERRNO_DOMAIN, errno_copy, msgfmt, ap); + va_end(ap); + return err; +} + +const char *sc_error_domain(struct sc_error *err) +{ + if (err == NULL) { + die("cannot obtain error domain from NULL error"); + } + return err->domain; +} + +int sc_error_code(struct sc_error *err) +{ + if (err == NULL) { + die("cannot obtain error code from NULL error"); + } + return err->code; +} + +const char *sc_error_msg(struct sc_error *err) +{ + if (err == NULL) { + die("cannot obtain error message from NULL error"); + } + return err->msg; +} + +void sc_error_free(struct sc_error *err) +{ + if (err != NULL) { + free(err->msg); + err->msg = NULL; + free(err); + } +} + +void sc_cleanup_error(struct sc_error **ptr) +{ + sc_error_free(*ptr); + *ptr = NULL; +} + +void sc_die_on_error(struct sc_error *error) +{ + if (error != NULL) { + if (strcmp(sc_error_domain(error), SC_ERRNO_DOMAIN) == 0) { + // Set errno just before the call to die() as it is used internally + errno = sc_error_code(error); + die("%s", sc_error_msg(error)); + } else { + errno = 0; + die("%s", sc_error_msg(error)); + } + } +} + +void sc_error_forward(struct sc_error **recipient, struct sc_error *error) +{ + if (recipient != NULL) { + *recipient = error; + } else { + sc_die_on_error(error); + } +} + +bool sc_error_match(struct sc_error *error, const char *domain, int code) +{ + if (domain == NULL) { + die("cannot match error to a NULL domain"); + } + if (error == NULL) { + return false; + } + return strcmp(sc_error_domain(error), domain) == 0 + && sc_error_code(error) == code; +} diff --git a/cmd/snap-confine/error.h b/cmd/snap-confine/error.h new file mode 100644 index 00000000..71db201d --- /dev/null +++ b/cmd/snap-confine/error.h @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#ifndef SNAP_CONFINE_ERROR_H +#define SNAP_CONFINE_ERROR_H + +#include + +#define SC_GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) + +/** + * The attribute returns_nonnull is only supported by GCC versions >= 4.9.0. + * Enable building of snap-confine on platforms that are stuck with older + * GCC versions. + **/ +#if SC_GCC_VERSION >= 40900 +#define SC_APPEND_RETURNS_NONNULL , returns_nonnull +#else +#define SC_APPEND_RETURNS_NONNULL +#endif + +/** + * This module defines APIs for simple error management. + * + * Errors are allocated objects that can be returned and passed around from + * functions. Errors carry a formatted message and optionally a scoped error + * code. The code is coped with a string "domain" that simply acts as a + * namespace for various interacting modules. + **/ + +/** + * Opaque error structure. + **/ +struct sc_error; + +/** + * Error domain for errors related to system errno. + **/ +#define SC_ERRNO_DOMAIN "errno" + +/** + * Initialize a new error object. + * + * The domain is a cookie-like string that allows the caller to distinguish + * between "namespaces" of error codes. It should be a static string that is + * provided by the caller. Both the domain and the error code can be retrieved + * later. + * + * This function calls die() in case of memory allocation failure. + **/ +__attribute__ ((warn_unused_result, + format(printf, 3, 4) SC_APPEND_RETURNS_NONNULL)) +struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, + ...); + +/** + * Initialize an errno-based error. + * + * The error carries a copy of errno and a custom error message as designed by + * the caller. See sc_error_init() for a more complete description. + * + * This function calls die() in case of memory allocation failure. + **/ +__attribute__ ((warn_unused_result, + format(printf, 2, 3) SC_APPEND_RETURNS_NONNULL)) +struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, + ...); + +/** + * Get the error domain out of an error object. + * + * The error domain acts as a namespace for error codes. + * No change of ownership takes place. + **/ +__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL)) +const char *sc_error_domain(struct sc_error *err); + +/** + * Get the error code out of an error object. + * + * The error code is scoped by the error domain. + * + * An error code of zero is special-cased to indicate that no particular error + * code is reserved for this error and it's not something that the programmer + * can rely on programmatically. This can be used to return an error message + * without having to allocate a distinct code for each one. + **/ +__attribute__ ((warn_unused_result)) +int sc_error_code(struct sc_error *err); + +/** + * Get the error message out of an error object. + * + * The error message is bound to the life-cycle of the error object. + * No change of ownership takes place. + **/ +__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL)) +const char *sc_error_msg(struct sc_error *err); + +/** + * Free an error object. + * + * The error object can be NULL. + **/ +void sc_error_free(struct sc_error *error); + +/** + * Cleanup an error with sc_error_free() + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_error))). + **/ +__attribute__ ((nonnull)) +void sc_cleanup_error(struct sc_error **ptr); + +/** + * + * Die if there's an error. + * + * This function is a correct way to die() if the passed error is not NULL. + * + * The error message is derived from the data in the error, using the special + * errno domain to provide additional information if that is available. + **/ +void sc_die_on_error(struct sc_error *error); + +/** + * Forward an error to the caller. + * + * This tries to forward an error to the caller. If this is impossible because + * the caller did not provide a location for the error to be stored then the + * sc_die_on_error() is called as a safety measure. + * + * Change of ownership takes place and the error is now stored in the recipient. + **/ +// NOTE: There's no nonnull(1) attribute as the recipient *can* be NULL. With +// the attribute in place GCC optimizes some things out and tests fail. +void sc_error_forward(struct sc_error **recipient, struct sc_error *error); + +/** + * Check if a given error matches the specified domain and code. + * + * It is okay to match a NULL error, the function simply returns false in that + * case. The domain cannot be NULL though. + **/ +__attribute__ ((warn_unused_result)) +bool sc_error_match(struct sc_error *error, const char *domain, int code); + +#endif diff --git a/cmd/snap-confine/manpages/Makefile.am b/cmd/snap-confine/manpages/Makefile.am new file mode 100644 index 00000000..e59b0743 --- /dev/null +++ b/cmd/snap-confine/manpages/Makefile.am @@ -0,0 +1,10 @@ +dist_man_MANS = snap-confine.5 snap-discard-ns.5 ubuntu-core-launcher.1 + +CLEANFILES = snap-confine.5 snap-discard-ns.5 ubuntu-core-launcher.1 +EXTRA_DIST = snap-confine.rst snap-discard-ns.rst ubuntu-core-launcher.rst + +%.5: %.rst + rst2man $^ > $@ + +ubuntu-core-launcher.1: ubuntu-core-launcher.rst + rst2man $^ > $@ diff --git a/cmd/snap-confine/manpages/snap-confine.rst b/cmd/snap-confine/manpages/snap-confine.rst new file mode 100644 index 00000000..9adc21cb --- /dev/null +++ b/cmd/snap-confine/manpages/snap-confine.rst @@ -0,0 +1,177 @@ +============== + snap-confine +============== + +----------------------------------------------- +internal tool for confining snappy applications +----------------------------------------------- + +:Author: zygmunt.krynicki@canonical.com +:Date: 2016-10-05 +:Copyright: Canonical Ltd. +:Version: 1.0.43 +:Manual section: 5 +:Manual group: snappy + +SYNOPSIS +======== + + snap-confine SECURITY_TAG COMMAND [...ARGUMENTS] + +DESCRIPTION +=========== + +The `snap-confine` is a program used internally by `snapd` to construct a +confined execution environment for snap applications. + +OPTIONS +======= + +The `snap-confine` program does not support any options. + +FEATURES +======== + +Apparmor profiles +----------------- + +`snap-confine` switches to the apparmor profile `$SECURITY_TAG`. The profile is +**mandatory** and `snap-confine` will refuse to run without it. + +has to be loaded into the kernel prior to using `snap-confine`. Typically this +is arranged for by `snapd`. The profile contains rich description of what the +application process is allowed to do, this includes system calls, file paths, +access patterns, linux capabilities, etc. The apparmor profile can also do +extensive dbus mediation. Refer to apparmor documentation for more details. + +Seccomp profiles +---------------- + +`snap-confine` looks for the `/var/lib/snapd/seccomp/profiles/$SECURITY_TAG` +file. This file is **mandatory** and `snap-confine` will refuse to run without +it. + +The file is read and parsed using a custom syntax that describes the set of +allowed system calls and optionally their arguments. The profile is then used +to confine the started application. + +As a security precaution disallowed system calls cause the started application +executable to be killed by the kernel. In the future this restriction may be +lifted to return `EPERM` instead. + +Mount profiles +-------------- + +`snap-confine` looks for the `/var/lib/snapd/mount/$SECURITY_TAG.fstab` file. +If present it is read, parsed and treated like a typical `fstab(5)` file. +The mount directives listed there are executed in order. All directives must +succeed as any failure will abort execution. + +By default all mount entries start with the following flags: `bind`, `ro`, +`nodev`, `nosuid`. Some of those flags can be reversed by an appropriate +option (e.g. `rw` can cause the mount point to be writable). + +As a security precaution only `bind` mounts are supported at this time. + +Quirks +------ + +`snap-confine` contains a quirk system that emulates some or the behavior of +the older versions of snap-confine that certain snaps (still in devmode but +useful and important) have grown to rely on. This section documents the list of +quirks: + +- The /var/lib/lxd directory, if it exists on the host, is made available in + the execution environment. This allows various snaps, while running in + devmode, to access the LXD socket. LP: #1613845 + +Sharing of the mount namespace +------------------------------ + +As of version 1.0.41 all the applications from the same snap will share the +same mount namespace. Applications from different snaps continue to use +separate mount namespaces. + +ENVIRONMENT +=========== + +`snap-confine` responds to the following environment variables + +`SNAP_CONFINE_DEBUG`: + When defined the program will print additional diagnostic information about + the actions being performed. All the output goes to stderr. + +The following variables are only used when `snap-confine` is not setuid root. +This is only applicable when testing the program itself. + +`SNAPPY_LAUNCHER_INSIDE_TESTS`: + Internal variable that should not be relied upon. + +`SNAP_CONFINE_NO_ROOT`: + Internal variable that should not be relied upon. + +`SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR`: + Internal variable that should not be relied upon. + +`SNAP_USER_DATA`: + Full path to the directory like /home/$LOGNAME/snap/$SNAP_NAME/$SNAP_REVISION. + + This directory is created by snap-confine on startup. This is a temporary + feature that will be merged into snapd's snap-run command. The set of directories + that can be created is confined with apparmor. + +FILES +===== + +`snap-confine` uses the following files: + +`/var/lib/snapd/mount/*.fstab`: + + Description of the mount profile. + +`/var/lib/snapd/seccomp/profiles/*`: + + Description of the seccomp profile. + +`/run/snapd/ns/`: + + Directory used to keep shared mount namespaces. + + `snap-confine` internally converts this directory to a private bind mount. + Semantically the behavior is identical to the following mount commands: + + mount --bind /run/snapd/ns /run/snapd/ns + mount --make-private /run/snapd/ns + +`/run/snapd/ns/.lock`: + + A `flock(2)`-based lock file acquired to create and convert + `/run/snapd/ns/` to a private bind mount. + +`/run/snapd/ns/$SNAP_NAME.lock`: + + A `flock(2)`-based lock file acquired to create or join the mount namespace + represented as `/run/snaps/ns/$SNAP_NAME.mnt`. + +`/run/snapd/ns/$SNAP_NAME.mnt`: + + This file can be either: + + - An empty file that may be seen before the mount namespace is preserved or + when the mount namespace is unmounted. + - A file belonging to the `nsfs` file system, representing a fully + populated mount namespace of a given snap. The file is bind mounted from + `/proc/self/ns/mnt` from the first process in any snap. + +`/proc/self/mountinfo`: + + This file is read to decide if `/run/snapd/ns/` needs to be created and + converted to a private bind mount, as described above. + +Note that the apparmor profile is external to `snap-confine` and is loaded +directly into the kernel. The actual apparmor profile is managed by `snapd`. + +BUGS +==== + +Please report all bugs with https://bugs.launchpad.net/snap-confine/+filebug diff --git a/cmd/snap-confine/manpages/snap-discard-ns.rst b/cmd/snap-confine/manpages/snap-discard-ns.rst new file mode 100644 index 00000000..6e470bc8 --- /dev/null +++ b/cmd/snap-confine/manpages/snap-discard-ns.rst @@ -0,0 +1,53 @@ +================ + snap-discard-ns +================ + +------------------------------------------------------------------------ +internal tool for discarding preserved namespaces of snappy applications +------------------------------------------------------------------------ + +:Author: zygmunt.krynicki@canonical.com +:Date: 2016-10-05 +:Copyright: Canonical Ltd. +:Version: 1.0.43 +:Manual section: 5 +:Manual group: snappy + +SYNOPSIS +======== + + snap-discard-ns SNAP_NAME + +DESCRIPTION +=========== + +The `snap-discard-ns` is a program used internally by `snapd` to discard a preserved +mount namespace of a particular snap. + +OPTIONS +======= + +The `snap-discard-ns` program does not support any options. + +ENVIRONMENT +=========== + +`snap-discard-ns` responds to the following environment variables + +`SNAP_CONFINE_DEBUG`: + When defined the program will print additional diagnostic information about + the actions being performed. All the output goes to stderr. + +FILES +===== + +`snap-discard-ns` uses the following files: + +`/run/snapd/ns/$SNAP_NAME.mnt`: + + The preserved mount namespace that is unmounted by `snap-discard-ns`. + +BUGS +==== + +Please report all bugs with https://bugs.launchpad.net/snap-confine/+filebug diff --git a/cmd/snap-confine/manpages/ubuntu-core-launcher.rst b/cmd/snap-confine/manpages/ubuntu-core-launcher.rst new file mode 100644 index 00000000..28423ae2 --- /dev/null +++ b/cmd/snap-confine/manpages/ubuntu-core-launcher.rst @@ -0,0 +1,40 @@ +====================== + ubuntu-core-launcher +====================== + +----------------------------------------------- +internal tool for confining snappy applications +----------------------------------------------- + +:Author: zygmunt.krynicki@canonical.com +:Date: 2016-10-05 +:Copyright: Canonical Ltd. +:Version: 1.0.43 +:Manual section: 1 +:Manual group: snappy + +SYNOPSIS +======== + + ubuntu-core-launcher SECURITY_TAG SECURITY_TAG COMMAND [...ARGUMENTS] + +DESCRIPTION +=========== + +This program is a thin wrapper around `snap-confine`. Please do not rely on it, +it is likely to be removed in the next release. + +OPTIONS +======= + +The `ubuntu-core-launcher` program does not support any options. + +BUGS +==== + +Please report all bugs with https://bugs.launchpad.net/snap-confine/+filebug + +SEE ALSO +======== + +`snap-confine(5)` diff --git a/cmd/snap-confine/misc/0001-Add-printk-based-debugging-to-pivot_root.patch b/cmd/snap-confine/misc/0001-Add-printk-based-debugging-to-pivot_root.patch new file mode 100644 index 00000000..225a47b8 --- /dev/null +++ b/cmd/snap-confine/misc/0001-Add-printk-based-debugging-to-pivot_root.patch @@ -0,0 +1,132 @@ +From 1ef45eb31cacd58c4c62e1fd26aa63a1f3d031a7 Mon Sep 17 00:00:00 2001 +From: Zygmunt Krynicki +Date: Thu, 29 Sep 2016 15:11:15 +0200 +Subject: [PATCH] Add printk-based debugging to pivot_root + +This patch changes pivot_root to make it obvious which error exit path +was taken. It might be useful to apply to debug and investigate how +undocumented requirements of pivot_root are not met. + +Signed-off-by: Zygmunt Krynicki +--- + fs/namespace.c | 70 ++++++++++++++++++++++++++++++++++++++++++++-------------- + 1 file changed, 53 insertions(+), 17 deletions(-) + +diff --git a/fs/namespace.c b/fs/namespace.c +index 877fc2c..6e15d1d 100644 +--- a/fs/namespace.c ++++ b/fs/namespace.c +@@ -2993,57 +2993,93 @@ SYSCALL_DEFINE2(pivot_root, const char __user *, new_root, + return -EPERM; + + error = user_path_dir(new_root, &new); +- if (error) ++ if (error) { ++ printk(KERN_ERR "user_path_dir(new_root, &new) returned an error\n"); + goto out0; ++ } + + error = user_path_dir(put_old, &old); +- if (error) ++ if (error) { ++ printk(KERN_ERR "user_path_dir(put_old, &old) returned an error\n"); + goto out1; ++ } + + error = security_sb_pivotroot(&old, &new); +- if (error) ++ if (error) { ++ printk(KERN_ERR "security_sb_pivotroot(&old, &new) returned an error\n"); + goto out2; ++ } + + get_fs_root(current->fs, &root); + old_mp = lock_mount(&old); + error = PTR_ERR(old_mp); +- if (IS_ERR(old_mp)) ++ if (IS_ERR(old_mp)) { ++ printk(KERN_ERR "IS_ERR(old_mp)\n"); + goto out3; ++ } + + error = -EINVAL; + new_mnt = real_mount(new.mnt); + root_mnt = real_mount(root.mnt); + old_mnt = real_mount(old.mnt); +- if (IS_MNT_SHARED(old_mnt) || +- IS_MNT_SHARED(new_mnt->mnt_parent) || +- IS_MNT_SHARED(root_mnt->mnt_parent)) ++ if (IS_MNT_SHARED(old_mnt)) { ++ printk(KERN_ERR "IS_MNT_SHARED(old_mnt)\n"); ++ goto out4; ++ } ++ if (IS_MNT_SHARED(new_mnt->mnt_parent)) { ++ printk(KERN_ERR "IS_MNT_SHARED(new_mnt->mnt_parent)\n"); + goto out4; +- if (!check_mnt(root_mnt) || !check_mnt(new_mnt)) ++ } ++ if (IS_MNT_SHARED(root_mnt->mnt_parent)) { ++ printk(KERN_ERR "IS_MNT_SHARED(root_mnt->mnt_parent)\n"); + goto out4; +- if (new_mnt->mnt.mnt_flags & MNT_LOCKED) ++ } ++ if (!check_mnt(root_mnt) || !check_mnt(new_mnt)) { ++ printk(KERN_ERR "!check_mnt(root_mnt) || !check_mnt(new_mnt)\n"); ++ goto out4; ++ } ++ if (new_mnt->mnt.mnt_flags & MNT_LOCKED) { ++ printk(KERN_ERR "new_mnt->mnt.mnt_flags & MNT_LOCKED\n"); + goto out4; ++ } + error = -ENOENT; +- if (d_unlinked(new.dentry)) ++ if (d_unlinked(new.dentry)) { ++ printk(KERN_ERR "d_unlinked(new.dentry)\n"); + goto out4; ++ } + error = -EBUSY; +- if (new_mnt == root_mnt || old_mnt == root_mnt) ++ if (new_mnt == root_mnt || old_mnt == root_mnt) { ++ printk(KERN_ERR "new_mnt == root_mnt || old_mnt == root_mnt\n"); + goto out4; /* loop, on the same file system */ ++ } + error = -EINVAL; +- if (root.mnt->mnt_root != root.dentry) ++ if (root.mnt->mnt_root != root.dentry) { ++ printk(KERN_ERR "root.mnt->mnt_root != root.dentry\n"); + goto out4; /* not a mountpoint */ +- if (!mnt_has_parent(root_mnt)) ++ } ++ if (!mnt_has_parent(root_mnt)) { ++ printk(KERN_ERR "!mnt_has_parent(root_mnt)\n"); + goto out4; /* not attached */ ++ } + root_mp = root_mnt->mnt_mp; +- if (new.mnt->mnt_root != new.dentry) ++ if (new.mnt->mnt_root != new.dentry) { ++ printk(KERN_ERR "new.mnt->mnt_root != new.dentry\n"); + goto out4; /* not a mountpoint */ +- if (!mnt_has_parent(new_mnt)) ++ } ++ if (!mnt_has_parent(new_mnt)) { ++ printk(KERN_ERR "!mnt_has_parent(new_mnt)\n"); + goto out4; /* not attached */ ++ } + /* make sure we can reach put_old from new_root */ +- if (!is_path_reachable(old_mnt, old.dentry, &new)) ++ if (!is_path_reachable(old_mnt, old.dentry, &new)) { ++ printk(KERN_ERR "!is_path_reachable(old_mnt, old.dentry, &new)\n"); + goto out4; ++ } + /* make certain new is below the root */ +- if (!is_path_reachable(new_mnt, new.dentry, &root)) ++ if (!is_path_reachable(new_mnt, new.dentry, &root)) { ++ printk(KERN_ERR "!is_path_reachable(new_mnt, new.dentry, &root)\n"); + goto out4; ++ } + root_mp->m_count++; /* pin it so it won't go away */ + lock_mount_hash(); + detach_mnt(new_mnt, &parent_path); +-- +2.7.4 + diff --git a/cmd/snap-confine/mount-opt-test.c b/cmd/snap-confine/mount-opt-test.c new file mode 100644 index 00000000..3625e0da --- /dev/null +++ b/cmd/snap-confine/mount-opt-test.c @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#include "mount-opt.h" +#include "mount-opt.c" + +#include +#include + +static void test_sc_mount_opt2str() +{ + g_assert_cmpstr(sc_mount_opt2str(0), ==, ""); + g_assert_cmpstr(sc_mount_opt2str(MS_RDONLY), ==, "ro"); + g_assert_cmpstr(sc_mount_opt2str(MS_NOSUID), ==, "nosuid"); + g_assert_cmpstr(sc_mount_opt2str(MS_NODEV), ==, "nodev"); + g_assert_cmpstr(sc_mount_opt2str(MS_NOEXEC), ==, "noexec"); + g_assert_cmpstr(sc_mount_opt2str(MS_SYNCHRONOUS), ==, "sync"); + g_assert_cmpstr(sc_mount_opt2str(MS_REMOUNT), ==, "remount"); + g_assert_cmpstr(sc_mount_opt2str(MS_MANDLOCK), ==, "mand"); + g_assert_cmpstr(sc_mount_opt2str(MS_DIRSYNC), ==, "dirsync"); + g_assert_cmpstr(sc_mount_opt2str(MS_NOATIME), ==, "noatime"); + g_assert_cmpstr(sc_mount_opt2str(MS_NODIRATIME), ==, "nodiratime"); + g_assert_cmpstr(sc_mount_opt2str(MS_BIND), ==, "bind"); + g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_BIND), ==, "rbind"); + g_assert_cmpstr(sc_mount_opt2str(MS_MOVE), ==, "move"); + g_assert_cmpstr(sc_mount_opt2str(MS_SILENT), ==, "silent"); + g_assert_cmpstr(sc_mount_opt2str(MS_POSIXACL), ==, "acl"); + g_assert_cmpstr(sc_mount_opt2str(MS_UNBINDABLE), ==, "unbindable"); + g_assert_cmpstr(sc_mount_opt2str(MS_PRIVATE), ==, "private"); + g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_PRIVATE), ==, "rprivate"); + g_assert_cmpstr(sc_mount_opt2str(MS_SLAVE), ==, "slave"); + g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_SLAVE), ==, "rslave"); + g_assert_cmpstr(sc_mount_opt2str(MS_SHARED), ==, "shared"); + g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_SHARED), ==, "rshared"); + g_assert_cmpstr(sc_mount_opt2str(MS_RELATIME), ==, "relatime"); + g_assert_cmpstr(sc_mount_opt2str(MS_KERNMOUNT), ==, "kernmount"); + g_assert_cmpstr(sc_mount_opt2str(MS_I_VERSION), ==, "iversion"); + g_assert_cmpstr(sc_mount_opt2str(MS_STRICTATIME), ==, "strictatime"); + g_assert_cmpstr(sc_mount_opt2str(MS_LAZYTIME), ==, "lazytime"); + // MS_NOSEC is not defined in userspace + // MS_BORN is not defined in userspace + g_assert_cmpstr(sc_mount_opt2str(MS_ACTIVE), ==, "active"); + g_assert_cmpstr(sc_mount_opt2str(MS_NOUSER), ==, "nouser"); + g_assert_cmpstr(sc_mount_opt2str(0x300), ==, "0x300"); + // random compositions do work + g_assert_cmpstr(sc_mount_opt2str(MS_RDONLY | MS_NOEXEC | MS_BIND), ==, + "ro,noexec,bind"); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/mount/sc_mount_opt2str", test_sc_mount_opt2str); +} diff --git a/cmd/snap-confine/mount-opt.c b/cmd/snap-confine/mount-opt.c new file mode 100644 index 00000000..9779dff5 --- /dev/null +++ b/cmd/snap-confine/mount-opt.c @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#include "mount-opt.h" + +#include +#include +#include + +const char *sc_mount_opt2str(unsigned long flags) +{ + static char buf[1000]; + unsigned long used = 0; + strcpy(buf, ""); +#define F(FLAG, TEXT) do if (flags & (FLAG)) { strcat(buf, #TEXT ","); flags ^= (FLAG); } while (0) + F(MS_RDONLY, ro); + F(MS_NOSUID, nosuid); + F(MS_NODEV, nodev); + F(MS_NOEXEC, noexec); + F(MS_SYNCHRONOUS, sync); + F(MS_REMOUNT, remount); + F(MS_MANDLOCK, mand); + F(MS_DIRSYNC, dirsync); + F(MS_NOATIME, noatime); + F(MS_NODIRATIME, nodiratime); + if (flags & MS_BIND) { + if (flags & MS_REC) { + strcat(buf, "rbind,"); + used |= MS_REC; + } else { + strcat(buf, "bind,"); + } + flags ^= MS_BIND; + } + F(MS_MOVE, move); + // The MS_REC flag handled separately by affected flags (MS_BIND, + // MS_PRIVATE, MS_SLAVE, MS_SHARED) + // XXX: kernel has MS_VERBOSE, glibc has MS_SILENT, both use the same constant + F(MS_SILENT, silent); + F(MS_POSIXACL, acl); + F(MS_UNBINDABLE, unbindable); + if (flags & MS_PRIVATE) { + if (flags & MS_REC) { + strcat(buf, "rprivate,"); + used |= MS_REC; + } else { + strcat(buf, "private,"); + } + flags ^= MS_PRIVATE; + } + if (flags & MS_SLAVE) { + if (flags & MS_REC) { + strcat(buf, "rslave,"); + used |= MS_REC; + } else { + strcat(buf, "slave,"); + } + flags ^= MS_SLAVE; + } + if (flags & MS_SHARED) { + if (flags & MS_REC) { + strcat(buf, "rshared,"); + used |= MS_REC; + } else { + strcat(buf, "shared,"); + } + flags ^= MS_SHARED; + } + flags ^= used; // this is just for MS_REC + F(MS_RELATIME, relatime); + F(MS_KERNMOUNT, kernmount); + F(MS_I_VERSION, iversion); + F(MS_STRICTATIME, strictatime); +#ifndef MS_LAZYTIME +#define MS_LAZYTIME (1<<25) +#endif + F(MS_LAZYTIME, lazytime); +#ifndef MS_NOSEC +#define MS_NOSEC (1 << 28) +#endif + F(MS_NOSEC, nosec); +#ifndef MS_BORN +#define MS_BORN (1 << 29) +#endif + F(MS_BORN, born); + F(MS_ACTIVE, active); + F(MS_NOUSER, nouser); +#undef F + // Render any flags that are unaccounted for. + if (flags) { + char of[128]; + sprintf(of, "%#lx", flags); + strcat(buf, of); + } + // Chop the excess comma from the end. + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == ',') { + buf[len - 1] = 0; + } + return buf; +} diff --git a/cmd/snap-confine/mount-opt.h b/cmd/snap-confine/mount-opt.h new file mode 100644 index 00000000..494afdc9 --- /dev/null +++ b/cmd/snap-confine/mount-opt.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#ifndef SNAP_CONFINE_MOUNT_OPT_H +#define SNAP_CONFINE_MOUNT_OPT_H + +/** + * Convert flags for mount(2) system call to a string representation. + * + * The function uses an internal static buffer that is overwritten on each + * request. + **/ +const char *sc_mount_opt2str(unsigned long flags); + +#endif // SNAP_CONFINE_MOUNT_OPT_H diff --git a/cmd/snap-confine/mount-support-nvidia.c b/cmd/snap-confine/mount-support-nvidia.c new file mode 100644 index 00000000..de0a5651 --- /dev/null +++ b/cmd/snap-confine/mount-support-nvidia.c @@ -0,0 +1,254 @@ +/* + * Copyright (C) 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 . + * + */ + +#include "config.h" +#include "mount-support-nvidia.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "classic.h" +#include "cleanup-funcs.h" +#include "utils.h" + +#ifdef NVIDIA_ARCH + +// List of globs that describe nvidia userspace libraries. +// This list was compiled from the following packages. +// +// https://www.archlinux.org/packages/extra/x86_64/nvidia-304xx-libgl/files/ +// https://www.archlinux.org/packages/extra/x86_64/nvidia-304xx-utils/files/ +// https://www.archlinux.org/packages/extra/x86_64/nvidia-340xx-libgl/files/ +// https://www.archlinux.org/packages/extra/x86_64/nvidia-340xx-utils/files/ +// https://www.archlinux.org/packages/extra/x86_64/nvidia-libgl/files/ +// https://www.archlinux.org/packages/extra/x86_64/nvidia-utils/files/ +// +// FIXME: this doesn't yet work with libGLX and libglvnd redirector +// FIXME: this still doesn't work with the 361 driver +static const char *nvidia_globs[] = { + "/usr/lib/libEGL.so*", + "/usr/lib/libEGL_nvidia.so*", + "/usr/lib/libGL.so*", + "/usr/lib/libOpenGL.so*", + "/usr/lib/libGLESv1_CM.so*", + "/usr/lib/libGLESv1_CM_nvidia.so*", + "/usr/lib/libGLESv2.so*", + "/usr/lib/libGLESv2_nvidia.so*", + "/usr/lib/libGLX_indirect.so*", + "/usr/lib/libGLX_nvidia.so*", + "/usr/lib/libGLX.so*", + "/usr/lib/libGLdispatch.so*", + "/usr/lib/libGLU.so*", + "/usr/lib/libXvMCNVIDIA.so*", + "/usr/lib/libXvMCNVIDIA_dynamic.so*", + "/usr/lib/libcuda.so*", + "/usr/lib/libnvcuvid.so*", + "/usr/lib/libnvidia-cfg.so*", + "/usr/lib/libnvidia-compiler.so*", + "/usr/lib/libnvidia-eglcore.so*", + "/usr/lib/libnvidia-encode.so*", + "/usr/lib/libnvidia-fatbinaryloader.so*", + "/usr/lib/libnvidia-fbc.so*", + "/usr/lib/libnvidia-glcore.so*", + "/usr/lib/libnvidia-glsi.so*", + "/usr/lib/libnvidia-ifr.so*", + "/usr/lib/libnvidia-ml.so*", + "/usr/lib/libnvidia-ptxjitcompiler.so*", + "/usr/lib/libnvidia-tls.so*", +}; + +static const size_t nvidia_globs_len = + sizeof nvidia_globs / sizeof *nvidia_globs; + +// Populate libgl_dir with a symlink farm to files matching glob_list. +// +// The symbolic links are made in one of two ways. If the library found is a +// file a regular symlink "$libname" -> "/path/to/hostfs/$libname" is created. +// If the library is a symbolic link then relative links are kept as-is but +// absolute links are translated to have "/path/to/hostfs" up front so that +// they work after the pivot_root elsewhere. +static void sc_populate_libgl_with_hostfs_symlinks(const char *libgl_dir, + const char *glob_list[], + size_t glob_list_len) +{ + glob_t glob_res __attribute__ ((__cleanup__(globfree))) = { + .gl_pathv = NULL}; + // Find all the entries matching the list of globs + for (size_t i = 0; i < glob_list_len; ++i) { + const char *glob_pattern = glob_list[i]; + int err = + glob(glob_pattern, i ? GLOB_APPEND : 0, NULL, &glob_res); + // Not all of the files have to be there (they differ depending on the + // driver version used). Ignore all errors that are not GLOB_NOMATCH. + if (err != 0 && err != GLOB_NOMATCH) { + die("cannot search using glob pattern %s: %d", + glob_pattern, err); + } + } + // Symlink each file found + for (size_t i = 0; i < glob_res.gl_pathc; ++i) { + char symlink_name[512]; + char symlink_target[512]; + const char *pathname = glob_res.gl_pathv[i]; + char *pathname_copy + __attribute__ ((cleanup(sc_cleanup_string))) = + strdup(pathname); + char *filename = basename(pathname_copy); + struct stat stat_buf; + int err = lstat(pathname, &stat_buf); + if (err != 0) { + die("cannot stat file %s", pathname); + } + switch (stat_buf.st_mode & S_IFMT) { + case S_IFLNK:; + // Read the target of the symbolic link + char hostfs_symlink_target[512]; + ssize_t num_read; + hostfs_symlink_target[0] = 0; + num_read = + readlink(pathname, hostfs_symlink_target, + sizeof hostfs_symlink_target); + if (num_read == -1) { + die("cannot read symbolic link %s", pathname); + } + hostfs_symlink_target[num_read] = 0; + if (hostfs_symlink_target[0] == '/') { + must_snprintf(symlink_target, + sizeof symlink_target, + "/var/lib/snapd/hostfs%s", + hostfs_symlink_target); + } else { + // Keep relative symlinks as-is, so that they point to -> libfoo.so.0.123 + must_snprintf(symlink_target, + sizeof symlink_target, "%s", + hostfs_symlink_target); + } + break; + case S_IFREG: + must_snprintf(symlink_target, + sizeof symlink_target, + "/var/lib/snapd/hostfs%s", pathname); + break; + default: + debug("ignoring unsupported entry: %s", pathname); + continue; + } + must_snprintf(symlink_name, sizeof symlink_name, + "%s/%s", libgl_dir, filename); + debug("creating symbolic link %s -> %s", symlink_name, + symlink_target); + if (symlink(symlink_target, symlink_name) != 0) { + die("cannot create symbolic link %s -> %s", + symlink_name, symlink_target); + } + } +} + +static void sc_mount_nvidia_driver_arch(const char *rootfs_dir) +{ + // Bind mount a tmpfs on $rootfs_dir/var/lib/snapd/lib/gl + char buf[512]; + must_snprintf(buf, sizeof(buf), "%s%s", rootfs_dir, + "/var/lib/snapd/lib/gl"); + const char *libgl_dir = buf; + debug("mounting tmpfs at %s", libgl_dir); + if (mount("none", libgl_dir, "tmpfs", MS_NODEV | MS_NOEXEC, NULL) != 0) { + die("cannot mount tmpfs at %s", libgl_dir); + }; + // Populate libgl_dir with symlinks to libraries from hostfs + sc_populate_libgl_with_hostfs_symlinks(libgl_dir, nvidia_globs, + nvidia_globs_len); + // Remount .../lib/gl read only + debug("remounting tmpfs as read-only %s", libgl_dir); + if (mount(NULL, buf, NULL, MS_REMOUNT | MS_RDONLY, NULL) != 0) { + die("cannot remount %s as read-only", buf); + } +} + +#endif // ifdef NVIDIA_ARCH + +#ifdef NVIDIA_UBUNTU + +struct sc_nvidia_driver { + int major_version; + int minor_version; +}; + +#define SC_NVIDIA_DRIVER_VERSION_FILE "/sys/module/nvidia/version" +#define SC_LIBGL_DIR "/var/lib/snapd/lib/gl" + +static void sc_probe_nvidia_driver(struct sc_nvidia_driver *driver) +{ + FILE *file __attribute__ ((cleanup(sc_cleanup_file))) = NULL; + debug("opening file describing nvidia driver version"); + file = fopen(SC_NVIDIA_DRIVER_VERSION_FILE, "rt"); + if (file == NULL) { + if (errno == ENOENT) { + debug("nvidia driver version file doesn't exist"); + driver->major_version = 0; + driver->minor_version = 0; + return; + } + die("cannot open file describing nvidia driver version"); + } + // Driver version format is MAJOR.MINOR where both MAJOR and MINOR are + // integers. We can use sscanf to parse this data. + if (fscanf + (file, "%d.%d", &driver->major_version, + &driver->minor_version) != 2) { + die("cannot parse nvidia driver version string"); + } + debug("parsed nvidia driver version: %d.%d", driver->major_version, + driver->minor_version); +} + +static void sc_mount_nvidia_driver_ubuntu(const char *rootfs_dir) +{ + struct sc_nvidia_driver driver; + sc_probe_nvidia_driver(&driver); + if (driver.major_version != 0) { + // Bind mount the binary nvidia driver into /var/lib/snapd/lib/gl. + char src[PATH_MAX], dst[PATH_MAX]; + must_snprintf(src, sizeof src, "/usr/lib/nvidia-%d", + driver.major_version); + must_snprintf(dst, sizeof dst, "%s%s", rootfs_dir, + SC_LIBGL_DIR); + debug("bind mounting nvidia driver %s -> %s", src, dst); + if (mount(src, dst, NULL, MS_BIND, NULL) != 0) { + die("cannot bind mount nvidia driver %s -> %s", src, + dst); + } + } +} +#endif // ifdef NVIDIA_UBUNTU + +void sc_mount_nvidia_driver(const char *rootfs_dir) +{ +#ifdef NVIDIA_UBUNTU + sc_mount_nvidia_driver_ubuntu(rootfs_dir); +#endif // ifdef NVIDIA_UBUNTU +#ifdef NVIDIA_ARCH + sc_mount_nvidia_driver_arch(rootfs_dir); +#endif // ifdef NVIDIA_ARCH +} diff --git a/cmd/snap-confine/mount-support-nvidia.h b/cmd/snap-confine/mount-support-nvidia.h new file mode 100644 index 00000000..56ec893f --- /dev/null +++ b/cmd/snap-confine/mount-support-nvidia.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 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 . + * + */ + +#ifndef SNAP_CONFINE_MOUNT_SUPPORT_NVIDIA_H +#define SNAP_CONFINE_MOUNT_SUPPORT_NVIDIA_H + +/** + * Make the Nvidia driver from the classic distribution available in the snap + * execution environment. + * + * This function may be a no-op, depending on build-time configuration options. + * If enabled the behavior differs from one distribution to another because of + * differences in classic packaging and perhaps version of the Nvidia driver. + * This function is designed to be called before pivot_root() switched the root + * filesystem. + * + * On Ubuntu, there are several versions of the binary Nvidia driver. The + * drivers are all installed in /usr/lib/nvidia-$MAJOR_VERSION where + * MAJOR_VERSION is an integer like 304, 331, 340, 346, 352 or 361. The driver + * is located by inspecting /sys/modules/nvidia/version which contains the + * string "$MAJOR_VERSION.$MINOR_VERSION". The appropriate directory is then + * bind mounted to /var/lib/snapd/lib/gl relative relative to the location of + * the root filesystem directory provided as an argument. + * + * On Arch another approach is used. Because the actual driver installs a + * number of shared objects into /usr/lib, they cannot be bind mounted + * directly. Instead a tmpfs is mounted on /var/lib/snapd/lib/gl. The tmpfs is + * subsequently populated with symlinks that point to a number of files in the + * /usr/lib directory on the classic filesystem. After the pivot_root() call + * those symlinks rely on the /var/lib/snapd/hostfs directory as a "gateway". + **/ +void sc_mount_nvidia_driver(const char *rootfs_dir); + +#endif diff --git a/cmd/snap-confine/mount-support-test.c b/cmd/snap-confine/mount-support-test.c new file mode 100644 index 00000000..da3cf80c --- /dev/null +++ b/cmd/snap-confine/mount-support-test.c @@ -0,0 +1,100 @@ +/* + * Copyright (C) 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 . + * + */ + +#include "mount-support.h" +#include "mount-support.c" +#include "mount-support-nvidia.h" +#include "mount-support-nvidia.c" + +#include + +static void replace_slashes_with_NUL(char *path, size_t len) +{ + for (size_t i = 0; i < len; i++) { + if (path[i] == '/') + path[i] = '\0'; + } +} + +static void test_get_nextpath__typical() +{ + char path[] = "/some/path"; + size_t offset = 0; + size_t fulllen = strlen(path); + + // Prepare path for usage with get_nextpath() by replacing + // all path separators with the NUL byte. + replace_slashes_with_NUL(path, fulllen); + + // Run get_nextpath a few times to see what happens. + char *result; + result = get_nextpath(path, &offset, fulllen); + g_assert_cmpstr(result, ==, "some"); + result = get_nextpath(path, &offset, fulllen); + g_assert_cmpstr(result, ==, "path"); + result = get_nextpath(path, &offset, fulllen); + g_assert_cmpstr(result, ==, NULL); +} + +static void test_get_nextpath__weird() +{ + char path[] = "..///path"; + size_t offset = 0; + size_t fulllen = strlen(path); + + // Prepare path for usage with get_nextpath() by replacing + // all path separators with the NUL byte. + replace_slashes_with_NUL(path, fulllen); + + // Run get_nextpath a few times to see what happens. + char *result; + result = get_nextpath(path, &offset, fulllen); + g_assert_cmpstr(result, ==, "path"); + result = get_nextpath(path, &offset, fulllen); + g_assert_cmpstr(result, ==, NULL); +} + +static void test_is_subdir() +{ + // Sensible exaples are sensible + g_assert_true(is_subdir("/dir/subdir", "/dir/")); + g_assert_true(is_subdir("/dir/subdir", "/dir")); + g_assert_true(is_subdir("/dir/", "/dir")); + g_assert_true(is_subdir("/dir", "/dir")); + // Also without leading slash + g_assert_true(is_subdir("dir/subdir", "dir/")); + g_assert_true(is_subdir("dir/subdir", "dir")); + g_assert_true(is_subdir("dir/", "dir")); + g_assert_true(is_subdir("dir", "dir")); + // Some more ideas + g_assert_true(is_subdir("//", "/")); + g_assert_true(is_subdir("/", "/")); + g_assert_true(is_subdir("", "")); + // but this is not true + g_assert_false(is_subdir("/", "/dir")); + g_assert_false(is_subdir("/rid", "/dir")); + g_assert_false(is_subdir("/different/dir", "/dir")); + g_assert_false(is_subdir("/", "")); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/mount/get_nextpath/typical", + test_get_nextpath__typical); + g_test_add_func("/mount/get_nextpath/weird", test_get_nextpath__weird); + g_test_add_func("/mount/is_subdir", test_is_subdir); +} diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c new file mode 100644 index 00000000..3c126b6c --- /dev/null +++ b/cmd/snap-confine/mount-support.c @@ -0,0 +1,667 @@ +/* + * Copyright (C) 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 . + * + */ +#include "config.h" +#include "mount-support.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "classic.h" +#include "cleanup-funcs.h" +#include "mount-support-nvidia.h" +#include "quirks.h" +#include "snap.h" +#include "utils.h" + +#define MAX_BUF 1000 + +/*! + * The void directory. + * + * Snap confine moves to that directory in case it cannot retain the current + * working directory across the pivot_root call. + **/ +#define SC_VOID_DIR "/var/lib/snapd/void" + +/** + * Get the path to the mounted core snap on the host distribution. + * + * The core snap may be named just "core" (preferred) or "ubuntu-core" + * (legacy). The mount point dependes on build-time configuration and may + * differ from distribution to distribution. + **/ +static const char *sc_get_outer_core_mount_point() +{ + const char *core_path = SNAP_MOUNT_DIR "/core/current/"; + const char *ubuntu_core_path = SNAP_MOUNT_DIR "/ubuntu-core/current/"; + static const char *result = NULL; + if (result == NULL) { + if (access(core_path, F_OK) == 0) { + // Use the "core" snap if available. + result = core_path; + } else if (access(ubuntu_core_path, F_OK) == 0) { + // If not try to fall back to the "ubuntu-core" snap. + result = ubuntu_core_path; + } else { + die("cannot locate the core snap"); + } + } + return result; +} + +// TODO: simplify this, after all it is just a tmpfs +// TODO: fold this into bootstrap +static void setup_private_mount(const char *security_tag) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + char tmpdir[MAX_BUF] = { 0 }; + + // Create a 0700 base directory, this is the base dir that is + // protected from other users. + // + // Under that basedir, we put a 1777 /tmp dir that is then bind + // mounted for the applications to use + must_snprintf(tmpdir, sizeof(tmpdir), "/tmp/snap.%d_%s_XXXXXX", uid, + security_tag); + if (mkdtemp(tmpdir) == NULL) { + die("cannot create temporary directory essential for private /tmp"); + } + // now we create a 1777 /tmp inside our private dir + mode_t old_mask = umask(0); + char *d = strdup(tmpdir); + if (!d) { + die("cannot allocate memory for string copy"); + } + must_snprintf(tmpdir, sizeof(tmpdir), "%s/tmp", d); + free(d); + + if (mkdir(tmpdir, 01777) != 0) { + die("cannot create temporary directory for private /tmp"); + } + umask(old_mask); + + // chdir to '/' since the mount won't apply to the current directory + char *pwd = get_current_dir_name(); + if (pwd == NULL) + die("cannot get current working directory"); + if (chdir("/") != 0) + die("cannot change directory to '/'"); + + // MS_BIND is there from linux 2.4 + if (mount(tmpdir, "/tmp", NULL, MS_BIND, NULL) != 0) { + die("cannot bind mount private /tmp"); + } + // MS_PRIVATE needs linux > 2.6.11 + if (mount("none", "/tmp", NULL, MS_PRIVATE, NULL) != 0) { + die("cannot change sharing on /tmp to make it private"); + } + // do the chown after the bind mount to avoid potential shenanigans + if (chown("/tmp/", uid, gid) < 0) { + die("cannot change ownership of /tmp"); + } + // chdir to original directory + if (chdir(pwd) != 0) + die("cannot change current working directory to the original directory"); + free(pwd); + + // ensure we set the various TMPDIRs to our newly created tmpdir + const char *tmpd[] = { "TMPDIR", "TEMPDIR", NULL }; + int i; + for (i = 0; tmpd[i] != NULL; i++) { + if (setenv(tmpd[i], "/tmp", 1) != 0) { + die("cannot set environment variable '%s'", tmpd[i]); + } + } +} + +// TODO: fold this into bootstrap +static void setup_private_pts() +{ + // See https://www.kernel.org/doc/Documentation/filesystems/devpts.txt + // + // Ubuntu by default uses devpts 'single-instance' mode where + // /dev/pts/ptmx is mounted with ptmxmode=0000. We don't want to change + // the startup scripts though, so we follow the instructions in point + // '4' of 'User-space changes' in the above doc. In other words, after + // unshare(CLONE_NEWNS), we mount devpts with -o + // newinstance,ptmxmode=0666 and then bind mount /dev/pts/ptmx onto + // /dev/ptmx + + struct stat st; + + // Make sure /dev/pts/ptmx exists, otherwise we are in legacy mode + // which doesn't provide the isolation we require. + if (stat("/dev/pts/ptmx", &st) != 0) { + die("cannot stat /dev/pts/ptmx"); + } + // Make sure /dev/ptmx exists so we can bind mount over it + if (stat("/dev/ptmx", &st) != 0) { + die("cannot stat /dev/ptmx"); + } + // Since multi-instance, use ptmxmode=0666. The other options are + // copied from /etc/default/devpts + if (mount("devpts", "/dev/pts", "devpts", MS_MGC_VAL, + "newinstance,ptmxmode=0666,mode=0620,gid=5")) { + die("cannot mount a new instance of /dev/pts"); + } + + if (mount("/dev/pts/ptmx", "/dev/ptmx", "none", MS_BIND, 0)) { + die("cannot mount /dev/pts/ptmx at /dev/ptmx'"); + } +} + +/* + * Setup mount profiles as described by snapd. + * + * This function reads /var/lib/snapd/mount/$security_tag.fstab as a fstab(5) file + * and executes the mount requests described there. + * + * Currently only bind mounts are allowed. All bind mounts are read only by + * default though the `rw` flag can be used. + * + * This function is called with the rootfs being "consistent" so that it is + * either the core snap on an all-snap system or the core snap + punched holes + * on a classic system. + **/ +static void sc_setup_mount_profiles(const char *security_tag) +{ + debug("%s: %s", __FUNCTION__, security_tag); + + FILE *f __attribute__ ((cleanup(sc_cleanup_endmntent))) = NULL; + const char *mount_profile_dir = "/var/lib/snapd/mount"; + + char profile_path[PATH_MAX]; + must_snprintf(profile_path, sizeof(profile_path), "%s/%s.fstab", + mount_profile_dir, security_tag); + + debug("opening mount profile %s", profile_path); + f = setmntent(profile_path, "r"); + // it is ok for the file to not exist + if (f == NULL && errno == ENOENT) { + debug("mount profile %s doesn't exist, ignoring", profile_path); + return; + } + // however any other error is a real error + if (f == NULL) { + die("cannot open %s", profile_path); + } + + struct mntent *m = NULL; + while ((m = getmntent(f)) != NULL) { + debug("read mount entry\n" + "\tmnt_fsname: %s\n" + "\tmnt_dir: %s\n" + "\tmnt_type: %s\n" + "\tmnt_opts: %s\n" + "\tmnt_freq: %d\n" + "\tmnt_passno: %d", + m->mnt_fsname, m->mnt_dir, m->mnt_type, + m->mnt_opts, m->mnt_freq, m->mnt_passno); + int flags = MS_BIND | MS_RDONLY | MS_NODEV | MS_NOSUID; + debug("initial flags are: bind,ro,nodev,nosuid"); + if (strcmp(m->mnt_type, "none") != 0) { + die("cannot honor mount profile, only 'none' filesystem type is supported"); + } + if (hasmntopt(m, "bind") == NULL) { + die("cannot honor mount profile, the bind mount flag is mandatory"); + } + if (hasmntopt(m, "rw") != NULL) { + flags &= ~MS_RDONLY; + } + if (mount(m->mnt_fsname, m->mnt_dir, NULL, flags, NULL) != 0) { + die("cannot mount %s at %s with options %s", + m->mnt_fsname, m->mnt_dir, m->mnt_opts); + } + } +} + +struct sc_mount { + const char *path; + bool is_bidirectional; +}; + +struct sc_mount_config { + const char *rootfs_dir; + // The struct is terminated with an entry with NULL path. + const struct sc_mount *mounts; + bool on_classic; +}; + +/** + * Bootstrap mount namespace. + * + * This is a chunk of tricky code that lets us have full control over the + * layout and direction of propagation of mount events. The documentation below + * assumes knowledge of the 'sharedsubtree.txt' document from the kernel source + * tree. + * + * As a reminder two definitions are quoted below: + * + * A 'propagation event' is defined as event generated on a vfsmount + * that leads to mount or unmount actions in other vfsmounts. + * + * A 'peer group' is defined as a group of vfsmounts that propagate + * events to each other. + * + * (end of quote). + * + * The main idea is to setup a mount namespace that has a root filesystem with + * vfsmounts and peer groups that, depending on the location, either isolate + * or share with the rest of the system. + * + * The vast majority of the filesystem is shared in one direction. Events from + * the outside (from the main mount namespace) propagate inside (to namespaces + * of particular snaps) so things like new snap revisions, mounted drives, etc, + * just show up as expected but even if a snap is exploited or malicious in + * nature it cannot affect anything in another namespace where it might cause + * security or stability issues. + * + * Selected directories (today just /media) can be shared in both directions. + * This allows snaps with sufficient privileges to either create, through the + * mount system call, additional mount points that are visible by the rest of + * the system (both the main mount namespace and namespaces of individual + * snaps) or remove them, through the unmount system call. + **/ +static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config) +{ + char scratch_dir[] = "/tmp/snap.rootfs_XXXXXX"; + char src[PATH_MAX]; + char dst[PATH_MAX]; + if (mkdtemp(scratch_dir) == NULL) { + die("cannot create temporary directory for the root file system"); + } + // NOTE: at this stage we just called unshare(CLONE_NEWNS). We are in a new + // mount namespace and have a private list of mounts. + debug("scratch directory for constructing namespace: %s", scratch_dir); + // Make the root filesystem recursively shared. This way propagation events + // will be shared with main mount namespace. + debug("performing operation: mount --make-rshared /"); + if (mount("none", "/", NULL, MS_REC | MS_SHARED, NULL) < 0) { + die("cannot perform operation: mount --make-rshared /"); + } + // Bind mount the temporary scratch directory for root filesystem over + // itself so that it is a mount point. This is done so that it can become + // unbindable as explained below. + debug("performing operation: mount --bind %s %s", scratch_dir, + scratch_dir); + if (mount(scratch_dir, scratch_dir, NULL, MS_BIND, NULL) < 0) { + die("cannot perform operation: mount --bind %s %s", scratch_dir, + scratch_dir); + } + // Make the scratch directory unbindable. + // + // This is necessary as otherwise a mount loop can occur and the kernel + // would crash. The term unbindable simply states that it cannot be bind + // mounted anywhere. When we construct recursive bind mounts below this + // guarantees that this directory will not be replicated anywhere. + debug("performing operation: mount --make-unbindable %s", scratch_dir); + if (mount("none", scratch_dir, NULL, MS_UNBINDABLE, NULL) < 0) { + die("cannot perform operation: mount --make-unbindable %s", + scratch_dir); + } + // Recursively bind mount desired root filesystem directory over the + // scratch directory. This puts the initial content into the scratch space + // and serves as a foundation for all subsequent operations below. + // + // The mount is recursive because it can either be applied to the root + // filesystem of a core system (aka all-snap) or the core snap on a classic + // system. In the former case we need recursive bind mounts to accurately + // replicate the state of the root filesystem into the scratch directory. + debug("performing operation: mount --rbind %s %s", config->rootfs_dir, + scratch_dir); + if (mount(config->rootfs_dir, scratch_dir, NULL, MS_REC | MS_BIND, NULL) + < 0) { + die("cannot perform operation: mount --rbind %s %s", + config->rootfs_dir, scratch_dir); + } + // Make the scratch directory recursively private. Nothing done there will + // be shared with any peer group, This effectively detaches us from the + // original namespace and coupled with pivot_root below serves as the + // foundation of the mount sandbox. + debug("performing operation: mount --make-rslave %s", scratch_dir); + if (mount("none", scratch_dir, NULL, MS_REC | MS_SLAVE, NULL) < 0) { + die("cannot perform operation: mount --make-rslave %s", + scratch_dir); + } + // Bind mount certain directories from the host filesystem to the scratch + // directory. By default mount events will propagate in both into and out + // of the peer group. This way the running application can alter any global + // state visible on the host and in other snaps. This can be restricted by + // disabling the "is_bidirectional" flag as can be seen below. + for (const struct sc_mount * mnt = config->mounts; mnt->path != NULL; + mnt++) { + if (mnt->is_bidirectional && mkdir(mnt->path, 0755) < 0 && + errno != EEXIST) { + die("cannot create %s", mnt->path); + } + must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, mnt->path); + debug("performing operation: mount --rbind %s %s", mnt->path, + dst); + if (mount(mnt->path, dst, NULL, MS_REC | MS_BIND, NULL) < 0) { + die("cannot perform operation: mount --rbind %s %s", + mnt->path, dst); + } + if (!mnt->is_bidirectional) { + // Mount events will only propagate inwards to the namespace. This + // way the running application cannot alter any global state apart + // from that of its own snap. + debug("performing operation: mount --make-rslave %s", + dst); + if (mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL) != + 0) { + die("cannot perform operation: mount --make-rslave %s", dst); + } + } + } + // Since we mounted /etc from the host filesystem to the scratch directory, + // we may need to put /etc/alternatives from the desired root filesystem + // (e.g. the core snap) back. This way the behavior of running snaps is not + // affected by the alternatives directory from the host, if one exists. + // + // https://bugs.launchpad.net/snap-confine/+bug/1580018 + const char *etc_alternatives = "/etc/alternatives"; + if (access(etc_alternatives, F_OK) == 0) { + must_snprintf(src, sizeof src, "%s%s", config->rootfs_dir, + etc_alternatives); + must_snprintf(dst, sizeof dst, "%s%s", scratch_dir, + etc_alternatives); + debug("performing operation: mount --bind %s %s", src, dst); + if (mount(src, dst, NULL, MS_BIND, NULL) != 0) { + die("cannot perform operation: mount --bind %s %s", src, + dst); + } + debug("performing operation: mount --make-slave %s", dst); + if (mount("none", dst, NULL, MS_SLAVE, NULL) != 0) { + die("cannot perform operation: mount --make-slave %s", + dst); + } + } + // Bind mount the directory where all snaps are mounted. The location of + // the this directory on the host filesystem may not match the location in + // the desired root filesystem. In the "core" and "ubuntu-core" snaps the + // directory is always /snap. On the host it is a build-time configuration + // option stored in SNAP_MOUNT_DIR. + must_snprintf(dst, sizeof dst, "%s/snap", scratch_dir); + debug("performing operation: mount --rbind %s %s", SNAP_MOUNT_DIR, dst); + if (mount(SNAP_MOUNT_DIR, dst, NULL, MS_BIND | MS_REC | MS_SLAVE, NULL) + < 0) { + die("cannot perform operation: mount --rbind -o slave %s %s", + SNAP_MOUNT_DIR, dst); + } + debug("performing operation: mount --make-rslave %s", dst); + if (mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL) < 0) { + die("cannot perform operation: mount --make-rslave %s", dst); + } + // Create the hostfs directory if one is missing. This directory is a part + // of packaging now so perhaps this code can be removed later. + if (access(SC_HOSTFS_DIR, F_OK) != 0) { + debug("creating missing hostfs directory"); + if (mkdir(SC_HOSTFS_DIR, 0755) != 0) { + die("cannot perform operation: mkdir %s", + SC_HOSTFS_DIR); + } + } + // Make the upcoming "put_old" directory for pivot_root private so that + // mount events don't propagate to any peer group. In practice pivot root + // has a number of undocumented requirements and one of them is that the + // "put_old" directory (the second argument) cannot be shared in any way. + must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, SC_HOSTFS_DIR); + debug("performing operation: mount --bind %s %s", dst, dst); + if (mount(dst, dst, NULL, MS_BIND, NULL) < 0) { + die("cannot perform operation: mount --bind %s %s", dst, dst); + } + debug("performing operation: mount --make-private %s", dst); + if (mount("none", dst, NULL, MS_PRIVATE, NULL) < 0) { + die("cannot perform operation: mount --make-private %s", dst); + } + // On classic mount the nvidia driver. Ideally this would be done in an + // uniform way after pivot_root but this is good enough and requires less + // code changes the nvidia code assumes it has access to the existing + // pre-pivot filesystem. + if (config->on_classic) { + sc_mount_nvidia_driver(scratch_dir); + } + // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + // pivot_root + // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + // Use pivot_root to "chroot" into the scratch directory. + // + // Q: Why are we using something as esoteric as pivot_root(2)? + // A: Because this makes apparmor handling easy. Using a normal chroot + // makes all apparmor rules conditional. We are either running on an + // all-snap system where this would-be chroot didn't happen and all the + // rules see / as the root file system _OR_ we are running on top of a + // classic distribution and this chroot has now moved all paths to + // /tmp/snap.rootfs_*. + // + // Because we are using unshare(2) with CLONE_NEWNS we can essentially use + // pivot_root just like chroot but this makes apparmor unaware of the old + // root so everything works okay. + // + // HINT: If you are debugging this and are trying to see why pivot_root + // happens to return EINVAL with any changes you may be making, please + // consider applying + // misc/0001-Add-printk-based-debugging-to-pivot_root.patch to your tree + // kernel. + debug("performing operation: pivot_root %s %s", scratch_dir, dst); + if (syscall(SYS_pivot_root, scratch_dir, dst) < 0) { + die("cannot perform operation: pivot_root %s %s", scratch_dir, + dst); + } + // Unmount the self-bind mount over the scratch directory created earlier + // in the original root filesystem (which is now mounted on SC_HOSTFS_DIR). + // This way we can remove the temporary directory we created and "clean up" + // after ourselves nicely. + must_snprintf(dst, sizeof dst, "%s/%s", SC_HOSTFS_DIR, scratch_dir); + debug("performing operation: umount %s", dst); + if (umount2(dst, 0) < 0) { + die("cannot perform operation: umount %s", dst); + } + // Remove the scratch directory. Note that we are using the path that is + // based on the old root filesystem as after pivot_root we cannot guarantee + // what is present at the same location normally. (It is probably an empty + // /tmp directory that is populated in another place). + debug("performing operation: rmdir %s", dst); + if (rmdir(scratch_dir) < 0) { + die("cannot perform operation: rmdir %s", dst); + }; + // Make the old root filesystem recursively slave. This way operations + // performed in this mount namespace will not propagate to the peer group. + // This is another essential part of the confinement system. + debug("performing operation: mount --make-rslave %s", SC_HOSTFS_DIR); + if (mount("none", SC_HOSTFS_DIR, NULL, MS_REC | MS_SLAVE, NULL) < 0) { + die("cannot perform operation: mount --make-rslave %s", + SC_HOSTFS_DIR); + } + // Detach the redundant hostfs version of sysfs since it shows up in the + // mount table and software inspecting the mount table may become confused + // (eg, docker and LP:# 162601). + must_snprintf(src, sizeof src, "%s/sys", SC_HOSTFS_DIR); + debug("performing operation: umount --lazy %s", src); + if (umount2(src, UMOUNT_NOFOLLOW | MNT_DETACH) < 0) { + die("cannot perform operation: umount --lazy %s", src); + } + // Detach the redundant hostfs version of /dev since it shows up in the + // mount table and software inspecting the mount table may become confused. + must_snprintf(src, sizeof src, "%s/dev", SC_HOSTFS_DIR); + debug("performing operation: umount --lazy %s", src); + if (umount2(src, UMOUNT_NOFOLLOW | MNT_DETACH) < 0) { + die("cannot perform operation: umount --lazy %s", src); + } + // Detach the redundant hostfs version of /proc since it shows up in the + // mount table and software inspecting the mount table may become confused. + must_snprintf(src, sizeof src, "%s/proc", SC_HOSTFS_DIR); + debug("performing operation: umount --lazy %s", src); + if (umount2(src, UMOUNT_NOFOLLOW | MNT_DETACH) < 0) { + die("cannot perform operation: umount --lazy %s", src); + } +} + +/** + * @path: a pathname where / replaced with '\0'. + * @offsetp: pointer to int showing which path segment was last seen. + * Updated on return to reflect the next segment. + * @fulllen: full original path length. + * Returns a pointer to the next path segment, or NULL if done. + */ +static char * __attribute__ ((used)) + get_nextpath(char *path, size_t * offsetp, size_t fulllen) +{ + int offset = *offsetp; + + if (offset >= fulllen) + return NULL; + + while (offset < fulllen && path[offset] != '\0') + offset++; + while (offset < fulllen && path[offset] == '\0') + offset++; + + *offsetp = offset; + return (offset < fulllen) ? &path[offset] : NULL; +} + +/** + * Check that @subdir is a subdir of @dir. +**/ +static bool __attribute__ ((used)) + is_subdir(const char *subdir, const char *dir) +{ + size_t dirlen = strlen(dir); + size_t subdirlen = strlen(subdir); + + // @dir has to be at least as long as @subdir + if (subdirlen < dirlen) + return false; + // @dir has to be a prefix of @subdir + if (strncmp(subdir, dir, dirlen) != 0) + return false; + // @dir can look like "path/" (that is, end with the directory separator). + // When that is the case then given the test above we can be sure @subdir + // is a real subdirectory. + if (dirlen > 0 && dir[dirlen - 1] == '/') + return true; + // @subdir can look like "path/stuff" and when the directory separator + // is exactly at the spot where @dir ends (that is, it was not caught + // by the test above) then @subdir is a real subdirectory. + if (subdir[dirlen] == '/' && dirlen > 0) + return true; + // If both @dir and @subdir have identical length then given that the + // prefix check above @subdir is a real subdirectory. + if (subdirlen == dirlen) + return true; + return false; +} + +void sc_populate_mount_ns(const char *security_tag) +{ + // Get the current working directory before we start fiddling with + // mounts and possibly pivot_root. At the end of the whole process, we + // will try to re-locate to the same directory (if possible). + char *vanilla_cwd __attribute__ ((cleanup(sc_cleanup_string))) = NULL; + vanilla_cwd = get_current_dir_name(); + if (vanilla_cwd == NULL) { + die("cannot get the current working directory"); + } + // Remember if we are on classic, some things behave differently there. + bool on_classic = is_running_on_classic_distribution(); + if (on_classic) { + const struct sc_mount mounts[] = { + {"/dev"}, // because it contains devices on host OS + {"/etc"}, // because that's where /etc/resolv.conf lives, perhaps a bad idea + {"/home"}, // to support /home/*/snap and home interface + {"/root"}, // because that is $HOME for services + {"/proc"}, // fundamental filesystem + {"/sys"}, // fundamental filesystem + {"/tmp"}, // to get writable tmp + {"/var/snap"}, // to get access to global snap data + {"/var/lib/snapd"}, // to get access to snapd state and seccomp profiles + {"/var/tmp"}, // to get access to the other temporary directory + {"/run"}, // to get /run with sockets and what not + {"/lib/modules"}, // access to the modules of the running kernel + {"/usr/src"}, // FIXME: move to SecurityMounts in system-trace interface + {"/var/log"}, // FIXME: move to SecurityMounts in log-observe interface +#ifdef MERGED_USR + {"/run/media", true}, // access to the users removable devices +#else + {"/media", true}, // access to the users removable devices +#endif // MERGED_USR + {"/run/netns", true}, // access to the 'ip netns' network namespaces + {}, + }; + struct sc_mount_config classic_config = { + .rootfs_dir = sc_get_outer_core_mount_point(), + .mounts = mounts, + .on_classic = true, + }; + sc_bootstrap_mount_namespace(&classic_config); + } else { + // This is what happens on an all-snap system. The rootfs we start with + // is the real outer rootfs. There are no unidirectional bind mounts + // needed because everything is already OK. We still keep the + // bidirectional /media mount point so that snaps designed for mounting + // filesystems can use that space for whatever they need. + const struct sc_mount mounts[] = { + {"/media", true}, + {"/run/netns", true}, + {}, + }; + struct sc_mount_config all_snap_config = { + .rootfs_dir = "/", + .mounts = mounts, + }; + sc_bootstrap_mount_namespace(&all_snap_config); + } + + // set up private mounts + // TODO: rename this and fold it into bootstrap + setup_private_mount(security_tag); + + // set up private /dev/pts + // TODO: fold this into bootstrap + setup_private_pts(); + + // setup quirks for specific snaps + if (on_classic) { + sc_setup_quirks(); + } + // setup the security backend bind mounts + sc_setup_mount_profiles(security_tag); + + // Try to re-locate back to vanilla working directory. This can fail + // because that directory is no longer present. + if (chdir(vanilla_cwd) != 0) { + debug("cannot remain in %s, moving to the void directory", + vanilla_cwd); + if (chdir(SC_VOID_DIR) != 0) { + die("cannot change directory to %s", SC_VOID_DIR); + } + debug("successfully moved to %s", SC_VOID_DIR); + } +} diff --git a/cmd/snap-confine/mount-support.h b/cmd/snap-confine/mount-support.h new file mode 100644 index 00000000..901c690f --- /dev/null +++ b/cmd/snap-confine/mount-support.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 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 . + * + */ + +#ifndef SNAP_MOUNT_SUPPORT_H +#define SNAP_MOUNT_SUPPORT_H + +/** + * Assuming a new mountspace, populate it accordingly. + * + * This function performs many internal tasks: + * - prepares and chroots into the core snap (on classic systems) + * - creates private /tmp + * - creates private /dev/pts + * - applies quirks for specific snaps (like LXD) + * - processes mount profiles + * + * The function will also try to preserve the current working directory but if + * this is impossible it will chdir to SC_VOID_DIR. + **/ +void sc_populate_mount_ns(const char *security_tag); + +#endif diff --git a/cmd/snap-confine/mountinfo-test.c b/cmd/snap-confine/mountinfo-test.c new file mode 100644 index 00000000..1e6bae64 --- /dev/null +++ b/cmd/snap-confine/mountinfo-test.c @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#include "mountinfo.h" +#include "mountinfo.c" + +#include + +static void test_parse_mountinfo_entry__sysfs() +{ + const char *line = + "19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw"; + struct mountinfo_entry *entry = parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 19); + g_assert_cmpint(entry->parent_id, ==, 25); + g_assert_cmpint(entry->dev_major, ==, 0); + g_assert_cmpint(entry->dev_minor, ==, 18); + g_assert_cmpstr(entry->root, ==, "/"); + g_assert_cmpstr(entry->mount_dir, ==, "/sys"); + g_assert_cmpstr(entry->mount_opts, ==, + "rw,nosuid,nodev,noexec,relatime"); + g_assert_cmpstr(entry->optional_fields, ==, "shared:7"); + g_assert_cmpstr(entry->fs_type, ==, "sysfs"); + g_assert_cmpstr(entry->mount_source, ==, "sysfs"); + g_assert_cmpstr(entry->super_opts, ==, "rw"); + g_assert_null(entry->next); +} + +// Parse the /run/snapd/ns bind mount (over itself) +// Note that /run is itself a tmpfs mount point. +static void test_parse_mountinfo_entry__snapd_ns() +{ + const char *line = + "104 23 0:19 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=99840k,mode=755"; + struct mountinfo_entry *entry = parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 104); + g_assert_cmpint(entry->parent_id, ==, 23); + g_assert_cmpint(entry->dev_major, ==, 0); + g_assert_cmpint(entry->dev_minor, ==, 19); + g_assert_cmpstr(entry->root, ==, "/snapd/ns"); + g_assert_cmpstr(entry->mount_dir, ==, "/run/snapd/ns"); + g_assert_cmpstr(entry->mount_opts, ==, "rw,nosuid,noexec,relatime"); + g_assert_cmpstr(entry->optional_fields, ==, ""); + g_assert_cmpstr(entry->fs_type, ==, "tmpfs"); + g_assert_cmpstr(entry->mount_source, ==, "tmpfs"); + g_assert_cmpstr(entry->super_opts, ==, "rw,size=99840k,mode=755"); + g_assert_null(entry->next); +} + +static void test_parse_mountinfo_entry__snapd_mnt() +{ + const char *line = + "256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw"; + struct mountinfo_entry *entry = parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 256); + g_assert_cmpint(entry->parent_id, ==, 104); + g_assert_cmpint(entry->dev_major, ==, 0); + g_assert_cmpint(entry->dev_minor, ==, 3); + g_assert_cmpstr(entry->root, ==, "mnt:[4026532509]"); + g_assert_cmpstr(entry->mount_dir, ==, "/run/snapd/ns/hello-world.mnt"); + g_assert_cmpstr(entry->mount_opts, ==, "rw"); + g_assert_cmpstr(entry->optional_fields, ==, ""); + g_assert_cmpstr(entry->fs_type, ==, "nsfs"); + g_assert_cmpstr(entry->mount_source, ==, "nsfs"); + g_assert_cmpstr(entry->super_opts, ==, "rw"); + g_assert_null(entry->next); +} + +static void test_parse_mountinfo_entry__garbage() +{ + const char *line = "256 104 0:3"; + struct mountinfo_entry *entry = parse_mountinfo_entry(line); + g_assert_null(entry); +} + +static void test_parse_mountinfo_entry__no_tags() +{ + const char *line = + "1 2 3:4 root mount-dir mount-opts - fs-type mount-source super-opts"; + struct mountinfo_entry *entry = parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 1); + g_assert_cmpint(entry->parent_id, ==, 2); + g_assert_cmpint(entry->dev_major, ==, 3); + g_assert_cmpint(entry->dev_minor, ==, 4); + g_assert_cmpstr(entry->root, ==, "root"); + g_assert_cmpstr(entry->mount_dir, ==, "mount-dir"); + g_assert_cmpstr(entry->mount_opts, ==, "mount-opts"); + g_assert_cmpstr(entry->optional_fields, ==, ""); + g_assert_cmpstr(entry->fs_type, ==, "fs-type"); + g_assert_cmpstr(entry->mount_source, ==, "mount-source"); + g_assert_cmpstr(entry->super_opts, ==, "super-opts"); + g_assert_null(entry->next); +} + +static void test_parse_mountinfo_entry__one_tag() +{ + const char *line = + "1 2 3:4 root mount-dir mount-opts tag:1 - fs-type mount-source super-opts"; + struct mountinfo_entry *entry = parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 1); + g_assert_cmpint(entry->parent_id, ==, 2); + g_assert_cmpint(entry->dev_major, ==, 3); + g_assert_cmpint(entry->dev_minor, ==, 4); + g_assert_cmpstr(entry->root, ==, "root"); + g_assert_cmpstr(entry->mount_dir, ==, "mount-dir"); + g_assert_cmpstr(entry->mount_opts, ==, "mount-opts"); + g_assert_cmpstr(entry->optional_fields, ==, "tag:1"); + g_assert_cmpstr(entry->fs_type, ==, "fs-type"); + g_assert_cmpstr(entry->mount_source, ==, "mount-source"); + g_assert_cmpstr(entry->super_opts, ==, "super-opts"); + g_assert_null(entry->next); +} + +static void test_parse_mountinfo_entry__two_tags() +{ + const char *line = + "1 2 3:4 root mount-dir mount-opts tag:1 tag:2 - fs-type mount-source super-opts"; + struct mountinfo_entry *entry = parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 1); + g_assert_cmpint(entry->parent_id, ==, 2); + g_assert_cmpint(entry->dev_major, ==, 3); + g_assert_cmpint(entry->dev_minor, ==, 4); + g_assert_cmpstr(entry->root, ==, "root"); + g_assert_cmpstr(entry->mount_dir, ==, "mount-dir"); + g_assert_cmpstr(entry->mount_opts, ==, "mount-opts"); + g_assert_cmpstr(entry->optional_fields, ==, "tag:1 tag:2"); + g_assert_cmpstr(entry->fs_type, ==, "fs-type"); + g_assert_cmpstr(entry->mount_source, ==, "mount-source"); + g_assert_cmpstr(entry->super_opts, ==, "super-opts"); + g_assert_null(entry->next); +} + +static void test_accessor_funcs() +{ + const char *line = + "256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw"; + struct mountinfo_entry *entry = parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_assert_cmpint(mountinfo_entry_mount_id(entry), ==, 256); + g_assert_cmpint(mountinfo_entry_parent_id(entry), ==, 104); + g_assert_cmpint(mountinfo_entry_dev_major(entry), ==, 0); + g_assert_cmpint(mountinfo_entry_dev_minor(entry), ==, 3); + + g_assert_cmpstr(mountinfo_entry_root(entry), ==, "mnt:[4026532509]"); + g_assert_cmpstr(mountinfo_entry_mount_dir(entry), ==, + "/run/snapd/ns/hello-world.mnt"); + g_assert_cmpstr(mountinfo_entry_mount_opts(entry), ==, "rw"); + g_assert_cmpstr(mountinfo_entry_optional_fields(entry), ==, ""); + g_assert_cmpstr(mountinfo_entry_fs_type(entry), ==, "nsfs"); + g_assert_cmpstr(mountinfo_entry_mount_source(entry), ==, "nsfs"); + g_assert_cmpstr(mountinfo_entry_super_opts(entry), ==, "rw"); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/mountinfo/parse_mountinfo_entry/sysfs", + test_parse_mountinfo_entry__sysfs); + g_test_add_func("/mountinfo/parse_mountinfo_entry/snapd-ns", + test_parse_mountinfo_entry__snapd_ns); + g_test_add_func("/mountinfo/parse_mountinfo_entry/snapd-mnt", + test_parse_mountinfo_entry__snapd_mnt); + g_test_add_func("/mountinfo/parse_mountinfo_entry/garbage", + test_parse_mountinfo_entry__garbage); + g_test_add_func("/mountinfo/parse_mountinfo_entry/no_tags", + test_parse_mountinfo_entry__no_tags); + g_test_add_func("/mountinfo/parse_mountinfo_entry/one_tags", + test_parse_mountinfo_entry__one_tag); + g_test_add_func("/mountinfo/parse_mountinfo_entry/two_tags", + test_parse_mountinfo_entry__two_tags); + g_test_add_func("/mountinfo/accessor_funcs", test_accessor_funcs); +} diff --git a/cmd/snap-confine/mountinfo.c b/cmd/snap-confine/mountinfo.c new file mode 100644 index 00000000..347b9b4f --- /dev/null +++ b/cmd/snap-confine/mountinfo.c @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2016 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 . + */ + +#include "mountinfo.h" + +#include +#include +#include +#include + +struct mountinfo { + struct mountinfo_entry *first; +}; + +struct mountinfo_entry { + int mount_id; + int parent_id; + unsigned dev_major, dev_minor; + char *root; + char *mount_dir; + char *mount_opts; + char *optional_fields; + char *fs_type; + char *mount_source; + char *super_opts; + + struct mountinfo_entry *next; + // Buffer holding all of the text data above. + // + // The buffer must be the last element of the structure. It is allocated + // along with the structure itself and does not need to be freed + // separately. + char line_buf[0]; +}; + +/** + * Parse a single mountinfo entry (line). + * + * The format, described by Linux kernel documentation, is as follows: + * + * 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + * (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) + * + * (1) mount ID: unique identifier of the mount (may be reused after umount) + * (2) parent ID: ID of parent (or of self for the top of the mount tree) + * (3) major:minor: value of st_dev for files on filesystem + * (4) root: root of the mount within the filesystem + * (5) mount point: mount point relative to the process's root + * (6) mount options: per mount options + * (7) optional fields: zero or more fields of the form "tag[:value]" + * (8) separator: marks the end of the optional fields + * (9) filesystem type: name of filesystem of the form "type[.subtype]" + * (10) mount source: filesystem specific information or "none" + * (11) super options: per super block options + **/ +static struct mountinfo_entry *parse_mountinfo_entry(const char *line) + __attribute__ ((nonnull(1))); + +/** + * Free a mountinfo structure and all its entries. + **/ +static void free_mountinfo(struct mountinfo *info) + __attribute__ ((nonnull(1))); + +/** + * Free a mountinfo entry. + **/ +static void free_mountinfo_entry(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +static void cleanup_fclose(FILE ** ptr); +static void cleanup_free(char **ptr); + +struct mountinfo_entry *first_mountinfo_entry(struct mountinfo *info) +{ + return info->first; +} + +struct mountinfo_entry *next_mountinfo_entry(struct mountinfo_entry + *entry) +{ + return entry->next; +} + +int mountinfo_entry_mount_id(struct mountinfo_entry *entry) +{ + return entry->mount_id; +} + +int mountinfo_entry_parent_id(struct mountinfo_entry *entry) +{ + return entry->parent_id; +} + +unsigned mountinfo_entry_dev_major(struct mountinfo_entry *entry) +{ + return entry->dev_major; +} + +unsigned mountinfo_entry_dev_minor(struct mountinfo_entry *entry) +{ + return entry->dev_minor; +} + +const char *mountinfo_entry_root(struct mountinfo_entry *entry) +{ + return entry->root; +} + +const char *mountinfo_entry_mount_dir(struct mountinfo_entry *entry) +{ + return entry->mount_dir; +} + +const char *mountinfo_entry_mount_opts(struct mountinfo_entry *entry) +{ + return entry->mount_opts; +} + +const char *mountinfo_entry_optional_fields(struct mountinfo_entry *entry) +{ + return entry->optional_fields; +} + +const char *mountinfo_entry_fs_type(struct mountinfo_entry *entry) +{ + return entry->fs_type; +} + +const char *mountinfo_entry_mount_source(struct mountinfo_entry *entry) +{ + return entry->mount_source; +} + +const char *mountinfo_entry_super_opts(struct mountinfo_entry *entry) +{ + return entry->super_opts; +} + +struct mountinfo *parse_mountinfo(const char *fname) +{ + struct mountinfo *info = calloc(1, sizeof *info); + if (info == NULL) { + return NULL; + } + if (fname == NULL) { + fname = "/proc/self/mountinfo"; + } + FILE *f __attribute__ ((cleanup(cleanup_fclose))) = fopen(fname, "rt"); + if (f == NULL) { + free(info); + return NULL; + } + char *line __attribute__ ((cleanup(cleanup_free))) = NULL; + size_t line_size = 0; + struct mountinfo_entry *entry, *last = NULL; + for (;;) { + errno = 0; + if (getline(&line, &line_size, f) == -1) { + if (errno != 0) { + free_mountinfo(info); + return NULL; + } + break; + }; + entry = parse_mountinfo_entry(line); + if (entry == NULL) { + free_mountinfo(info); + return NULL; + } + if (last != NULL) { + last->next = entry; + } else { + info->first = entry; + } + last = entry; + } + return info; +} + +static struct mountinfo_entry *parse_mountinfo_entry(const char *line) +{ + // NOTE: the mountinfo structure is allocated along with enough extra + // storage to hold the whole line we are parsing. This is used as backing + // store for all text fields. + // + // The idea is that since the line has a given length and we are only after + // set of substrings we can easily predict the amount of required space + // (after all, it is just a set of non-overlapping substrings) and append + // it to the allocated entry structure. + // + // The parsing code below, specifically parse_next_string_field(), uses + // this extra memory to hold data parsed from the original line. In the + // end, the result is similar to using strtok except that the source and + // destination buffers are separate. + struct mountinfo_entry *entry = + calloc(1, sizeof *entry + strlen(line) + 1); + if (entry == NULL) { + return NULL; + } + int nscanned; + int offset, total_offset = 0; + nscanned = sscanf(line, "%d %d %u:%u %n", + &entry->mount_id, &entry->parent_id, + &entry->dev_major, &entry->dev_minor, &offset); + if (nscanned != 4) + goto fail; + total_offset += offset; + int total_used = 0; + char *parse_next_string_field() { + char *field = &entry->line_buf[0] + total_used; + nscanned = sscanf(line + total_offset, "%s %n", field, &offset); + if (nscanned != 1) + return NULL; + total_offset += offset; + total_used += offset + 1; + return field; + } + if ((entry->root = parse_next_string_field()) == NULL) + goto fail; + if ((entry->mount_dir = parse_next_string_field()) == NULL) + goto fail; + if ((entry->mount_opts = parse_next_string_field()) == NULL) + goto fail; + entry->optional_fields = &entry->line_buf[0] + total_used++; + // NOTE: This ensures that optional_fields is never NULL. If this changes, + // must adjust all callers of parse_mountinfo_entry() accordingly. + strcpy(entry->optional_fields, ""); + for (;;) { + char *opt_field = parse_next_string_field(); + if (opt_field == NULL) + goto fail; + if (strcmp(opt_field, "-") == 0) { + break; + } + if (*entry->optional_fields) { + strcat(entry->optional_fields, " "); + } + strcat(entry->optional_fields, opt_field); + } + if ((entry->fs_type = parse_next_string_field()) == NULL) + goto fail; + if ((entry->mount_source = parse_next_string_field()) == NULL) + goto fail; + if ((entry->super_opts = parse_next_string_field()) == NULL) + goto fail; + return entry; + fail: + free(entry); + return NULL; +} + +void cleanup_mountinfo(struct mountinfo **ptr) +{ + if (*ptr != NULL) { + free_mountinfo(*ptr); + *ptr = NULL; + } +} + +static void free_mountinfo(struct mountinfo *info) +{ + struct mountinfo_entry *entry, *next; + for (entry = info->first; entry != NULL; entry = next) { + next = entry->next; + free_mountinfo_entry(entry); + } + free(info); +} + +static void free_mountinfo_entry(struct mountinfo_entry *entry) +{ + free(entry); +} + +static void cleanup_fclose(FILE ** ptr) +{ + fclose(*ptr); +} + +static void cleanup_free(char **ptr) +{ + free(*ptr); +} diff --git a/cmd/snap-confine/mountinfo.h b/cmd/snap-confine/mountinfo.h new file mode 100644 index 00000000..c5a58dff --- /dev/null +++ b/cmd/snap-confine/mountinfo.h @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2016 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 . + */ + +#ifndef SC_MOUNTINFO_H +#define SC_MOUNTINFO_H + +/** + * Structure describing entire /proc/self/mountinfo file + **/ +struct mountinfo; + +/** + * Structure describing a single entry in /proc/self/mountinfo + **/ +struct mountinfo_entry; + +/** + * Parse a file in according to mountinfo syntax. + * + * The argument can be used to parse an arbitrary file. NULL can be used to + * implicitly parse /proc/self/mountinfo, that is the mount information + * associated with the current process. + **/ +struct mountinfo *parse_mountinfo(const char *fname); + +/** + * Free a mountinfo structure. + * + * This function is designed to be used with __attribute__((cleanup)) so it + * takes a pointer to the freed object (which is also a pointer). + **/ +void cleanup_mountinfo(struct mountinfo **ptr) __attribute__ ((nonnull(1))); + +/** + * Get the first mountinfo entry. + * + * The returned value may be NULL if the parsed file contained no entries. The + * returned value is bound to the lifecycle of the whole mountinfo structure + * and should not be freed explicitly. + **/ +struct mountinfo_entry *first_mountinfo_entry(struct mountinfo *info) + __attribute__ ((nonnull(1))); + +/** + * Get the next mountinfo entry. + * + * The returned value is a pointer to the next mountinfo entry or NULL if this + * was the last entry. The returned value is bound to the lifecycle of the + * whole mountinfo structure and should not be freed explicitly. + **/ +struct mountinfo_entry *next_mountinfo_entry(struct mountinfo_entry + *entry) + __attribute__ ((nonnull(1))); + +/** + * Get the mount identifier of a given mount entry. + **/ +int mountinfo_entry_mount_id(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +/** + * Get the parent mount identifier of a given mount entry. + **/ +int mountinfo_entry_parent_id(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +unsigned mountinfo_entry_dev_major(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +unsigned mountinfo_entry_dev_minor(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +/** + * Get the root directory of a given mount entry. + **/ +const char *mountinfo_entry_root(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +/** + * Get the mount point of a given mount entry. + **/ +const char *mountinfo_entry_mount_dir(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +/** + * Get the mount options of a given mount entry. + **/ +const char *mountinfo_entry_mount_opts(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +/** + * Get optional tagged data associated of a given mount entry. + * + * The return value is a string (possibly empty but never NULL) in the format + * tag[:value]. Known tags are: + * + * "shared:X": + * mount is shared in peer group X + * "master:X": + * mount is slave to peer group X + * "propagate_from:X" + * mount is slave and receives propagation from peer group X (*) + * "unbindable": + * mount is unbindable + * + * (*) X is the closest dominant peer group under the process's root. + * If X is the immediate master of the mount, or if there's no dominant peer + * group under the same root, then only the "master:X" field is present and not + * the "propagate_from:X" field. + **/ +const char *mountinfo_entry_optional_fields(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +/** + * Get the file system type of a given mount entry. + **/ +const char *mountinfo_entry_fs_type(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +/** + * Get the source of a given mount entry. + **/ +const char *mountinfo_entry_mount_source(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +/** + * Get the super block options of a given mount entry. + **/ +const char *mountinfo_entry_super_opts(struct mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +#endif diff --git a/cmd/snap-confine/ns-support-test.c b/cmd/snap-confine/ns-support-test.c new file mode 100644 index 00000000..c6438e97 --- /dev/null +++ b/cmd/snap-confine/ns-support-test.c @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#include "ns-support.h" +#include "ns-support.c" + +#include "cleanup-funcs.h" + +#include +#include // for NSFS_MAGIC +#include +#include + +#include +#include + +// Set alternate namespace directory +static void sc_set_ns_dir(const char *dir) +{ + sc_ns_dir = dir; +} + +// Shell-out to "rm -rf -- $dir" as long as $dir is in /tmp. +static void rm_rf_tmp(const char *dir) +{ + // Sanity check, don't remove anything that's not in the temporary + // directory. This is here to prevent unintended data loss. + if (!g_str_has_prefix(dir, "/tmp/")) + die("refusing to remove: %s", dir); + const gchar *working_directory = NULL; + gchar **argv = NULL; + gchar **envp = NULL; + GSpawnFlags flags = G_SPAWN_SEARCH_PATH; + GSpawnChildSetupFunc child_setup = NULL; + gpointer user_data = NULL; + gchar **standard_output = NULL; + gchar **standard_error = NULL; + gint exit_status = 0; + GError *error = NULL; + + argv = calloc(5, sizeof *argv); + if (argv == NULL) + die("cannot allocate command argument array"); + argv[0] = g_strdup("rm"); + if (argv[0] == NULL) + die("cannot allocate memory"); + argv[1] = g_strdup("-rf"); + if (argv[1] == NULL) + die("cannot allocate memory"); + argv[2] = g_strdup("--"); + if (argv[2] == NULL) + die("cannot allocate memory"); + argv[3] = g_strdup(dir); + if (argv[3] == NULL) + die("cannot allocate memory"); + argv[4] = NULL; + g_assert_true(g_spawn_sync + (working_directory, argv, envp, flags, child_setup, + user_data, standard_output, standard_error, &exit_status, + &error)); + g_assert_true(g_spawn_check_exit_status(exit_status, NULL)); + if (error != NULL) { + g_test_message("cannot remove temporary directory: %s\n", + error->message); + g_error_free(error); + } + g_free(argv[0]); + g_free(argv[1]); + g_free(argv[2]); + g_free(argv[3]); + g_free(argv); +} + +// Check that rm_rf_tmp doesn't remove things outside of /tmp +static void test_rm_rf_tmp() +{ + if (access("/nonexistent", F_OK) == 0) { + g_test_message + ("/nonexistent exists but this test doesn't want it to"); + g_test_fail(); + return; + } + if (g_test_subprocess()) { + rm_rf_tmp("/nonexistent"); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +// Use temporary directory for namespace groups. +// +// The directory is automatically reset to the real value at the end of the +// test. +static const char *sc_test_use_fake_ns_dir() +{ + char *ns_dir = NULL; + if (g_test_subprocess()) { + // Check if the environment variable is set. If so then someone is already + // managing the temporary directory and we should not create a new one. + ns_dir = getenv("SNAP_CONFINE_NS_DIR"); + g_assert_nonnull(ns_dir); + } else { + ns_dir = g_dir_make_tmp(NULL, NULL); + g_assert_nonnull(ns_dir); + g_test_queue_free(ns_dir); + g_assert_cmpint(setenv("SNAP_CONFINE_NS_DIR", ns_dir, 0), ==, + 0); + g_test_queue_destroy((GDestroyNotify) unsetenv, + "SNAP_CONFINE_NS_DIR"); + g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, ns_dir); + } + g_test_queue_destroy((GDestroyNotify) sc_set_ns_dir, SC_NS_DIR); + sc_set_ns_dir(ns_dir); + return ns_dir; +} + +// Check that allocating a namespace group sets up internal data structures to +// safe values. +static void test_sc_alloc_ns_group() +{ + struct sc_ns_group *group = NULL; + group = sc_alloc_ns_group(); + g_test_queue_free(group); + g_assert_nonnull(group); + g_assert_cmpint(group->dir_fd, ==, -1); + g_assert_cmpint(group->lock_fd, ==, -1); + g_assert_cmpint(group->event_fd, ==, -1); + g_assert_cmpint(group->child, ==, 0); + g_assert_cmpint(group->should_populate, ==, false); + g_assert_null(group->name); +} + +// Initialize a namespace group. +// +// The group is automatically destroyed at the end of the test. +static struct sc_ns_group *sc_test_open_ns_group(const char *group_name) +{ + // Initialize a namespace group + struct sc_ns_group *group = NULL; + if (group_name == NULL) { + group_name = "test-group"; + } + group = sc_open_ns_group(group_name, 0); + g_test_queue_destroy((GDestroyNotify) sc_close_ns_group, group); + // Check if the returned group data looks okay + g_assert_nonnull(group); + g_assert_cmpint(group->dir_fd, !=, -1); + g_assert_cmpint(group->lock_fd, !=, -1); + g_assert_cmpint(group->event_fd, ==, -1); + g_assert_cmpint(group->child, ==, 0); + g_assert_cmpint(group->should_populate, ==, false); + g_assert_cmpstr(group->name, ==, group_name); + return group; +} + +// Check that initializing a namespace group creates the appropriate +// filesystem structure and obtains open file descriptors for the lock. +static void test_sc_open_ns_group() +{ + const char *ns_dir = sc_test_use_fake_ns_dir(); + struct sc_ns_group *group = sc_test_open_ns_group(NULL); + // Check that the group directory exists + g_assert_true(g_file_test + (ns_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)); + // Check that the lock file exists + char *lock_file __attribute__ ((cleanup(sc_cleanup_string))) = NULL; + lock_file = + g_strdup_printf("%s/%s%s", ns_dir, group->name, SC_NS_LOCK_FILE); + g_assert_true(g_file_test + (lock_file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)); +} + +static void test_sc_open_ns_group_graceful() +{ + sc_set_ns_dir("/nonexistent"); + g_test_queue_destroy((GDestroyNotify) sc_set_ns_dir, SC_NS_DIR); + struct sc_ns_group *group = + sc_open_ns_group("foo", SC_NS_FAIL_GRACEFULLY); + g_assert_null(group); +} + +static void test_sc_lock_ns_mutex_precondition() +{ + sc_test_use_fake_ns_dir(); + if (g_test_subprocess()) { + struct sc_ns_group *group = sc_alloc_ns_group(); + g_test_queue_free(group); + // Try to lock the mutex, this should abort because we never opened the + // lock file and don't have a valid file descriptor. + sc_lock_ns_mutex(group); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_unlock_ns_mutex_precondition() +{ + sc_test_use_fake_ns_dir(); + if (g_test_subprocess()) { + struct sc_ns_group *group = sc_alloc_ns_group(); + g_test_queue_free(group); + // Try to unlock the mutex, this should abort because we never opened the + // lock file and don't have a valid file descriptor. + sc_unlock_ns_mutex(group); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +// Check that locking a namespace actually flock's the mutex with LOCK_EX +static void test_sc_lock_unlock_ns_mutex() +{ + const char *ns_dir = sc_test_use_fake_ns_dir(); + struct sc_ns_group *group = sc_test_open_ns_group(NULL); + // Lock the namespace group mutex + sc_lock_ns_mutex(group); + // Construct the name of the lock file + char *lock_file __attribute__ ((cleanup(sc_cleanup_string))) = NULL; + lock_file = + g_strdup_printf("%s/%s%s", ns_dir, group->name, SC_NS_LOCK_FILE); + // Open the lock file again to obtain a separate file descriptor. + // According to flock(2) locks are associated with an open file table entry + // so this descriptor will be separate and can compete for the same lock. + int lock_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1; + lock_fd = open(lock_file, O_RDWR | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint(lock_fd, !=, -1); + // The non-blocking lock operation should fail with EWOULDBLOCK as the lock + // file is locked by sc_nlock_ns_mutex() already. + int err = flock(lock_fd, LOCK_EX | LOCK_NB); + int saved_errno = errno; + g_assert_cmpint(err, ==, -1); + g_assert_cmpint(saved_errno, ==, EWOULDBLOCK); + // Unlock the namespace group mutex + sc_unlock_ns_mutex(group); + // Re-attempt the locking operation. This time it should succeed. + err = flock(lock_fd, LOCK_EX | LOCK_NB); + g_assert_cmpint(err, ==, 0); +} + +static void unmount_dir(void *dir) +{ + umount(dir); +} + +static void test_sc_is_ns_group_dir_private() +{ + if (geteuid() != 0) { + g_test_skip("this test needs to run as root"); + return; + } + const char *ns_dir = sc_test_use_fake_ns_dir(); + g_test_queue_destroy(unmount_dir, (char *)ns_dir); + + if (g_test_subprocess()) { + // The temporary directory should not be private initially + g_assert_false(sc_is_ns_group_dir_private()); + + /// do what "mount --bind /foo /foo; mount --make-private /foo" does. + int err; + err = mount(ns_dir, ns_dir, NULL, MS_BIND, NULL); + g_assert_cmpint(err, ==, 0); + err = mount(NULL, ns_dir, NULL, MS_PRIVATE, NULL); + g_assert_cmpint(err, ==, 0); + + // The temporary directory should now be private + g_assert_true(sc_is_ns_group_dir_private()); + return; + } + g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_passed(); +} + +static void test_sc_initialize_ns_groups() +{ + if (geteuid() != 0) { + g_test_skip("this test needs to run as root"); + return; + } + // NOTE: this is g_test_subprocess aware! + const char *ns_dir = sc_test_use_fake_ns_dir(); + g_test_queue_destroy(unmount_dir, (char *)ns_dir); + if (g_test_subprocess()) { + // Initialize namespace groups using a fake directory. + sc_initialize_ns_groups(); + + // Check that the fake directory is now a private mount. + g_assert_true(sc_is_ns_group_dir_private()); + + // Check that the lock file did not leak unclosed. + + // Construct the name of the lock file + char *lock_file __attribute__ ((cleanup(sc_cleanup_string))) = + NULL; + lock_file = + g_strdup_printf("%s/%s", sc_ns_dir, SC_NS_LOCK_FILE); + // Attempt to open and lock the lock file. + int lock_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1; + lock_fd = open(lock_file, O_RDWR | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint(lock_fd, !=, -1); + // The non-blocking lock operation should not fail + int err = flock(lock_fd, LOCK_EX | LOCK_NB); + g_assert_cmpint(err, ==, 0); + return; + } + g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_passed(); +} + +// Sanity check, ensure that the namespace filesystem identifier is what we +// expect, aka NSFS_MAGIC. +static void test_nsfs_fs_id() +{ + struct utsname uts; + if (uname(&uts) < 0) { + g_test_message("cannot use uname(2)"); + g_test_fail(); + return; + } + int major, minor; + if (sscanf(uts.release, "%d.%d", &major, &minor) != 2) { + g_test_message("cannot use sscanf(2) to parse kernel release"); + g_test_fail(); + return; + } + if (major < 3 || (major == 3 && minor < 19)) { + g_test_skip("this test needs kernel 3.19+"); + return; + } + struct statfs buf; + int err = statfs("/proc/self/ns/mnt", &buf); + g_assert_cmpint(err, ==, 0); + g_assert_cmpint(buf.f_type, ==, NSFS_MAGIC); +} + +static void test_sc_enable_sanity_timeout() +{ + if (g_test_subprocess()) { + sc_enable_sanity_timeout(); + debug("waiting..."); + usleep(4 * G_USEC_PER_SEC); + debug("woke up"); + sc_disable_sanity_timeout(); + return; + } + g_test_trap_subprocess(NULL, 5 * G_USEC_PER_SEC, + G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_failed(); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/internal/rm_rf_tmp", test_rm_rf_tmp); + g_test_add_func("/ns/sc_enable_sanity_timeout", + test_sc_enable_sanity_timeout); + g_test_add_func("/ns/sc_alloc_ns_group", test_sc_alloc_ns_group); + g_test_add_func("/ns/sc_open_ns_group", test_sc_open_ns_group); + g_test_add_func("/ns/sc_open_ns_group/graceful", + test_sc_open_ns_group_graceful); + g_test_add_func("/ns/sc_lock_unlock_ns_mutex", + test_sc_lock_unlock_ns_mutex); + g_test_add_func("/ns/sc_lock_ns_mutex/precondition", + test_sc_lock_ns_mutex_precondition); + g_test_add_func("/ns/sc_unlock_ns_mutex/precondition", + test_sc_unlock_ns_mutex_precondition); + g_test_add_func("/ns/nsfs_fs_id", test_nsfs_fs_id); + g_test_add_func("/system/ns/sc_is_ns_group_dir_private", + test_sc_is_ns_group_dir_private); + g_test_add_func("/system/ns/sc_initialize_ns_groups", + test_sc_initialize_ns_groups); +} diff --git a/cmd/snap-confine/ns-support.c b/cmd/snap-confine/ns-support.c new file mode 100644 index 00000000..f4ef4fa7 --- /dev/null +++ b/cmd/snap-confine/ns-support.c @@ -0,0 +1,553 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#include "ns-support.h" + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils.h" +#include "user-support.h" +#include "mountinfo.h" +#include "cleanup-funcs.h" + +/** + * Flag indicating that a sanity timeout has expired. + **/ +static volatile sig_atomic_t sanity_timeout_expired = 0; + +/** + * Signal handler for SIGALRM that sets sanity_timeout_expired flag to 1. + **/ +static void sc_SIGALRM_handler(int signum) +{ + sanity_timeout_expired = 1; +} + +/** + * Enable a sanity-check timeout. + * + * The timeout is based on good-old alarm(2) and is intended to break a + * suspended system call, such as flock, after a few seconds. The built-in + * timeout is primed for three seconds. After that any sleeping system calls + * are interrupted and a flag is set. + * + * The call should be paired with sc_disable_sanity_check_timeout() that + * disables the alarm and acts on the flag, aborting the process if the timeout + * gets exceeded. + **/ +static void sc_enable_sanity_timeout() +{ + sanity_timeout_expired = 0; + struct sigaction act = {.sa_handler = sc_SIGALRM_handler }; + if (sigemptyset(&act.sa_mask) < 0) { + die("cannot initialize POSIX signal set"); + } + // NOTE: we are using sigaction so that we can explicitly control signal + // flags and *not* pass the SA_RESTART flag. The intent is so that any + // system call we may be sleeping on to get interrupted. + act.sa_flags = 0; + if (sigaction(SIGALRM, &act, NULL) < 0) { + die("cannot install signal handler for SIGALRM"); + } + alarm(3); + debug("sanity timeout initialized and set for three seconds"); +} + +/** + * Disable sanity-check timeout and abort the process if it expired. + * + * This call has to be paired with sc_enable_sanity_timeout(), see the function + * description for more details. + **/ +static void sc_disable_sanity_timeout() +{ + if (sanity_timeout_expired) { + die("sanity timeout expired"); + } + alarm(0); + struct sigaction act = {.sa_handler = SIG_DFL }; + if (sigemptyset(&act.sa_mask) < 0) { + die("cannot initialize POSIX signal set"); + } + if (sigaction(SIGALRM, &act, NULL) < 0) { + die("cannot uninstall signal handler for SIGALRM"); + } + debug("sanity timeout reset and disabled"); +} + +/*! + * The void directory. + * + * Snap confine moves to that directory in case it cannot retain the current + * working directory across the pivot_root call. + **/ +#define SC_VOID_DIR "/var/lib/snapd/void" + +/** + * Directory where snap-confine keeps namespace files. + **/ +#define SC_NS_DIR "/run/snapd/ns" + +/** + * Effective value of SC_NS_DIR. + * + * We use 'const char *' so we can update sc_ns_dir in the testsuite + **/ +static const char *sc_ns_dir = SC_NS_DIR; + +/** + * Name of the lock file associated with SC_NS_DIR. + * and a given group identifier (typically SNAP_NAME). + **/ +#define SC_NS_LOCK_FILE ".lock" + +/** + * Name of the preserved mount namespace associated with SC_NS_DIR + * and a given group identifier (typically SNAP_NAME). + **/ +#define SC_NS_MNT_FILE ".mnt" + +/** + * Read /proc/self/mountinfo and check if /run/snapd/ns is a private bind mount. + * + * We do this because /run/snapd/ns cannot be shared with any other peers as per: + * https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt + **/ +static bool sc_is_ns_group_dir_private() +{ + struct mountinfo *info + __attribute__ ((cleanup(cleanup_mountinfo))) = NULL; + info = parse_mountinfo(NULL); + if (info == NULL) { + die("cannot parse /proc/self/mountinfo"); + } + struct mountinfo_entry *entry = first_mountinfo_entry(info); + while (entry != NULL) { + const char *mount_dir = mountinfo_entry_mount_dir(entry); + const char *optional_fields = + mountinfo_entry_optional_fields(entry); + if (strcmp(mount_dir, sc_ns_dir) == 0 + && strcmp(optional_fields, "") == 0) { + // If /run/snapd/ns has no optional fields, we know it is mounted + // private and there is nothing else to do. + return true; + } + entry = next_mountinfo_entry(entry); + } + return false; +} + +void sc_initialize_ns_groups() +{ + debug("creating namespace group directory %s", sc_ns_dir); + if (sc_nonfatal_mkpath(sc_ns_dir, 0755) < 0) { + die("cannot create namespace group directory %s", sc_ns_dir); + } + debug("opening namespace group directory %s", sc_ns_dir); + int dir_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1; + dir_fd = open(sc_ns_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW); + if (dir_fd < 0) { + die("cannot open namespace group directory"); + } + debug("opening lock file for group directory"); + int lock_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1; + lock_fd = openat(dir_fd, + SC_NS_LOCK_FILE, + O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); + if (lock_fd < 0) { + die("cannot open lock file for namespace group directory"); + } + debug("locking the namespace group directory"); + sc_enable_sanity_timeout(); + if (flock(lock_fd, LOCK_EX) < 0) { + die("cannot acquire exclusive lock for namespace group directory"); + } + sc_disable_sanity_timeout(); + if (!sc_is_ns_group_dir_private()) { + debug + ("bind mounting the namespace group directory over itself"); + if (mount(sc_ns_dir, sc_ns_dir, NULL, MS_BIND | MS_REC, NULL) < + 0) { + die("cannot bind mount namespace group directory over itself"); + } + debug + ("making the namespace group directory mount point private"); + if (mount(NULL, sc_ns_dir, NULL, MS_PRIVATE, NULL) < 0) { + die("cannot make the namespace group directory mount point private"); + } + } else { + debug + ("namespace group directory does not require intialization"); + } + debug("unlocking the namespace group directory"); + if (flock(lock_fd, LOCK_UN) < 0) { + die("cannot release lock for namespace control directory"); + } +} + +struct sc_ns_group { + // Name of the namespace group ($SNAP_NAME). + char *name; + // Descriptor to the namespace group control directory. This descriptor is + // opened with O_PATH|O_DIRECTORY so it's only used for openat() calls. + int dir_fd; + // Descriptor to a namespace-specific lock file (i.e. $SNAP_NAME.lock). + int lock_fd; + // Descriptor to an eventfd that is used to notify the child that it can + // now complete its job and exit. + int event_fd; + // Identifier of the child process that is used during the one-time (per + // group) initialization and capture process. + pid_t child; + // Flag set when this process created a fresh namespace should populate it. + bool should_populate; +}; + +static struct sc_ns_group *sc_alloc_ns_group() +{ + struct sc_ns_group *group = calloc(1, sizeof *group); + if (group == NULL) { + die("cannot allocate memory for namespace group"); + } + group->dir_fd = -1; + group->lock_fd = -1; + group->event_fd = -1; + // Redundant with calloc but some functions check for the non-zero value so + // I'd like to keep this explicit in the code. + group->child = 0; + return group; +} + +struct sc_ns_group *sc_open_ns_group(const char *group_name, + const unsigned flags) +{ + struct sc_ns_group *group = sc_alloc_ns_group(); + debug("opening namespace group directory %s", sc_ns_dir); + group->dir_fd = + open(sc_ns_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW); + if (group->dir_fd < 0) { + if (flags & SC_NS_FAIL_GRACEFULLY && errno == ENOENT) { + free(group); + return NULL; + } + die("cannot open directory for namespace group %s", group_name); + } + char lock_fname[PATH_MAX]; + must_snprintf(lock_fname, sizeof lock_fname, "%s%s", group_name, + SC_NS_LOCK_FILE); + debug("opening lock file for namespace group %s", group_name); + group->lock_fd = + openat(group->dir_fd, lock_fname, + O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); + if (group->lock_fd < 0) { + die("cannot open lock file for namespace group %s", group_name); + } + group->name = strdup(group_name); + if (group->name == NULL) { + die("cannot duplicate namespace group name %s", group_name); + } + return group; +} + +void sc_close_ns_group(struct sc_ns_group *group) +{ + debug("releasing resources associated with namespace group %s", + group->name); + close(group->dir_fd); + close(group->lock_fd); + close(group->event_fd); + free(group->name); + free(group); +} + +void sc_lock_ns_mutex(struct sc_ns_group *group) +{ + if (group->lock_fd < 0) { + die("precondition failed: we don't have an open file descriptor for the mutex file"); + } + debug("acquiring exclusive lock for namespace group %s", group->name); + sc_enable_sanity_timeout(); + if (flock(group->lock_fd, LOCK_EX) < 0) { + die("cannot acquire exclusive lock for namespace group %s", + group->name); + } + sc_disable_sanity_timeout(); + debug("acquired exclusive lock for namespace group %s", group->name); +} + +void sc_unlock_ns_mutex(struct sc_ns_group *group) +{ + if (group->lock_fd < 0) { + die("precondition failed: we don't have an open file descriptor for the mutex file"); + } + debug("releasing lock for namespace group %s", group->name); + if (flock(group->lock_fd, LOCK_UN) < 0) { + die("cannot release lock for namespace group %s", group->name); + } + debug("released lock for namespace group %s", group->name); +} + +void sc_create_or_join_ns_group(struct sc_ns_group *group, + struct sc_apparmor *apparmor) +{ + // Open the mount namespace file. + char mnt_fname[PATH_MAX]; + must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name, + SC_NS_MNT_FILE); + int mnt_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1; + // NOTE: There is no O_EXCL here because the file can be around but + // doesn't have to be a mounted namespace. + // + // If the mounted namespace is discarded with + // sc_discard_preserved_ns_group() it will revert to a regular file. If + // snap-confine is killed for whatever reason after the file is created but + // before the file is bind-mounted it will also be a regular file. + mnt_fd = + openat(group->dir_fd, mnt_fname, + O_CREAT | O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600); + if (mnt_fd < 0) { + die("cannot open mount namespace file for namespace group %s", + group->name); + } + // Check if we got an nsfs-based file or a regular file. This can be + // reliably tested because nsfs has an unique filesystem type NSFS_MAGIC. + // We can just ensure that this is the case thanks to fstatfs. + struct statfs buf; + if (fstatfs(mnt_fd, &buf) < 0) { + die("cannot perform fstatfs() on an mount namespace file descriptor"); + } +#ifndef NSFS_MAGIC +// Account for kernel headers old enough to not know about NSFS_MAGIC. +#define NSFS_MAGIC 0x6e736673 +#endif + if (buf.f_type == NSFS_MAGIC) { + char *vanilla_cwd __attribute__ ((cleanup(sc_cleanup_string))) = + NULL; + vanilla_cwd = get_current_dir_name(); + if (vanilla_cwd == NULL) { + die("cannot get the current working directory"); + } + debug + ("attempting to re-associate the mount namespace with the namespace group %s", + group->name); + if (setns(mnt_fd, CLONE_NEWNS) < 0) { + die("cannot re-associate the mount namespace with namespace group %s", group->name); + } + debug + ("successfully re-associated the mount namespace with the namespace group %s", + group->name); + // Try to re-locate back to vanilla working directory. This can fail + // because that directory is no longer present. + if (chdir(vanilla_cwd) != 0) { + debug + ("cannot remain in %s, moving to the void directory", + vanilla_cwd); + if (chdir(SC_VOID_DIR) != 0) { + die("cannot change directory to %s", + SC_VOID_DIR); + } + debug("successfully moved to %s", SC_VOID_DIR); + } + return; + } + debug("initializing new namespace group %s", group->name); + // Create a new namespace and ask the caller to populate it. + // For rationale of forking see this: + // https://lists.linuxfoundation.org/pipermail/containers/2013-August/033386.html + // + // The eventfd created here is used to synchronize the child and the parent + // processes. It effectively tells the child to perform the capture + // operation. + group->event_fd = eventfd(0, EFD_CLOEXEC); + if (group->event_fd < 0) { + die("cannot create eventfd for mount namespace capture"); + } + debug("forking support process for mount namespace capture"); + // Store the PID of the "parent" process. This done instead of calls to + // getppid() because then we can reliably track the PID of the parent even + // if the child process is re-parented. + pid_t parent = getpid(); + // Glibc defines pid as a signed 32bit integer. There's no standard way to + // print pid's portably so this is the best we can do. + pid_t pid = fork(); + debug("forked support process has pid %d", (int)pid); + if (pid < 0) { + die("cannot fork support process for mount namespace capture"); + } + if (pid == 0) { + // This is the child process which will capture the mount namespace. + // + // It will do so by bind-mounting the SC_NS_MNT_FILE after the parent + // process calls unshare() and finishes setting up the namespace + // completely. + // Change the hat to a sub-profile that has limited permissions + // necessary to accomplish the capture of the mount namespace. + debug + ("changing apparmor hat of the support process for mount namespace capture"); + sc_maybe_aa_change_hat(apparmor, + "mount-namespace-capture-helper", 0); + // Configure the child to die as soon as the parent dies. In an odd + // case where the parent is killed then we don't want to complete our + // task or wait for anything. + if (prctl(PR_SET_PDEATHSIG, SIGINT, 0, 0, 0) < 0) { + die("cannot set parent process death notification signal to SIGINT"); + } + // Check that parent process is still alive. If this is the case then + // we can *almost* reliably rely on the PR_SET_PDEATHSIG signal to wake + // us up from eventfd_read() below. In the rare case that the PID numbers + // overflow and the now-dead parent PID is recycled we will still hang + // forever on the read from eventfd below. + debug("ensuring that parent process is still alive"); + if (kill(parent, 0) < 0) { + switch (errno) { + case ESRCH: + debug("parent process has already terminated"); + abort(); + default: + die("cannot ensure that parent process is still alive"); + break; + } + } + if (fchdir(group->dir_fd) < 0) { + die("cannot move process for mount namespace capture to namespace group directory"); + } + debug + ("waiting for a eventfd data from the parent process to continue"); + eventfd_t value = 0; + sc_enable_sanity_timeout(); + if (eventfd_read(group->event_fd, &value) < 0) { + die("cannot read expected data from eventfd"); + } + sc_disable_sanity_timeout(); + debug + ("capturing mount namespace of process %d in namespace group %s", + (int)parent, group->name); + char src[PATH_MAX]; + char dst[PATH_MAX]; + must_snprintf(src, sizeof src, "/proc/%d/ns/mnt", (int)parent); + must_snprintf(dst, sizeof dst, "%s%s", group->name, + SC_NS_MNT_FILE); + if (mount(src, dst, NULL, MS_BIND, NULL) < 0) { + die("cannot bind-mount the mount namespace file %s -> %s", src, dst); + } + debug + ("successfully captured mount namespace in namespace group %s", + group->name); + exit(0); + } else { + group->child = pid; + // Unshare the mount namespace and set a flag instructing the caller that + // the namespace is pristine and needs to be populated now. + debug("unsharing the mount namespace"); + if (unshare(CLONE_NEWNS) < 0) { + die("cannot unshare the mount namespace"); + } + group->should_populate = true; + } +} + +bool sc_should_populate_ns_group(struct sc_ns_group *group) +{ + return group->should_populate; +} + +void sc_preserve_populated_ns_group(struct sc_ns_group *group) +{ + if (group->child == 0) { + die("precondition failed: we don't have a support process for mount namespace capture"); + } + if (group->event_fd < 0) { + die("precondition failed: we don't have an eventfd for mount namespace capture"); + } + debug + ("asking support process for mount namespace capture (pid: %d) to perform the capture", + group->child); + if (eventfd_write(group->event_fd, 1) < 0) { + die("cannot write eventfd"); + } + debug + ("waiting for the support process for mount namespace capture to exit"); + int status = 0; + errno = 0; + if (waitpid(group->child, &status, 0) < 0) { + die("cannot wait for the support process for mount namespace capture"); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + die("support process for mount namespace capture exited abnormally"); + } + debug("support process for mount namespace capture exited normally"); + group->child = 0; +} + +void sc_discard_preserved_ns_group(struct sc_ns_group *group) +{ + // Remember the current working directory + int old_dir_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1; + old_dir_fd = open(".", O_PATH | O_DIRECTORY | O_CLOEXEC); + if (old_dir_fd < 0) { + die("cannot open current directory"); + } + // Move to the mount namespace directory (/run/snapd/ns) + if (fchdir(group->dir_fd) < 0) { + die("cannot move to namespace group directory"); + } + // Unmount ${group_name}.mnt which holds the preserved namespace + char mnt_fname[PATH_MAX]; + must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name, + SC_NS_MNT_FILE); + debug("unmounting preserved mount namespace file %s", mnt_fname); + if (umount2(mnt_fname, UMOUNT_NOFOLLOW) < 0) { + switch (errno) { + case EINVAL: + // EINVAL is returned when there's nothing to unmount (no bind-mount). + // Instead of checking for this explicitly (which is always racy) we + // just unmount and check the return code. + break; + case ENOENT: + // We may be asked to discard a namespace that doesn't yet + // exist (even the mount point may be absent). We just + // ignore that error and return gracefully. + break; + default: + die("cannot unmount preserved mount namespace file %s", + mnt_fname); + break; + } + } + // Get back to the original directory + if (fchdir(old_dir_fd) < 0) { + die("cannot move back to original directory"); + } +} diff --git a/cmd/snap-confine/ns-support.h b/cmd/snap-confine/ns-support.h new file mode 100644 index 00000000..f48dfa09 --- /dev/null +++ b/cmd/snap-confine/ns-support.h @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#ifndef SNAP_NAMESPACE_SUPPORT +#define SNAP_NAMESPACE_SUPPORT + +#include + +#include "apparmor-support.h" + +/** + * Initialize namespace sharing. + * + * This function must be called once in each process that wishes to create or + * join a namespace group. + * + * It is responsible for bind mounting the control directory over itself and + * making it private (unsharing it with all the other peers) so that it can be + * used for storing preserved namespaces as bind-mounted files from the nsfs + * filesystem (namespace filesystem). + * + * This function acquires a flock(2)-based lock to ensure that no other instance + * of snap-confine attempts to do this concurrently. If a process dies for any + * reason then the lock is released and other instances of snap-confine can + * complete the initialization. + * + * This function inspects /proc/self/mountinfo to determine if the directory + * where namespaces are kept (/run/snapd/ns) is correctly prepared as described + * above. + * + * For more details see namespaces(7). + **/ +void sc_initialize_ns_groups(); + +/** + * Data required to manage namespaces amongst a group of processes. + */ +struct sc_ns_group; + +enum { + SC_NS_FAIL_GRACEFULLY = 1 +}; + +/** + * Open a namespace group. + * + * This will open and keep file descriptors for /run/snapd/ns/ as well as for + * /run/snapd/ns/${group_name}.lock. The lock file is created if necessary but + * is not locked until sc_lock_ns_mutex() is called. + * + * If the flags argument is SC_NS_FAIL_GRACEFULLY then the function returns + * NULL if the /run/snapd/ns directory doesn't exist. In all other cases it + * calls die() and exits the process. + */ +struct sc_ns_group *sc_open_ns_group(const char *group_name, + const unsigned flags); + +/** + * Close namespace group. + * + * This will close all of the open file descriptors and release allocated memory. + */ +void sc_close_ns_group(struct sc_ns_group *group); + +/** + * Acquire exclusive lock to the namespace group. + * + * This will attempt to acquire an flock-based exclusive lock on the file + * descriptor associated with /run/snapd/ns/${group_name}.lock. If the process + * is killed while the lock is held the lock is automatically released by the + * kernel. + * + * The following methods should be called only while holding the lock: + * - sc_create_or_join_ns_group() + * - sc_should_populate_ns_group() + * - sc_preserve_populated_ns_group() + * - sc_discard_preserved_ns_group() + **/ +void sc_lock_ns_mutex(struct sc_ns_group *group); + +/** + * Release lock to the namespace group. + * + * This will attempt to release a flock-based lock on the file descriptor + * associated with /run/snapd/ns/${group_name}.lock. + **/ +void sc_unlock_ns_mutex(struct sc_ns_group *group); + +/** + * Join the mount namespace associated with this group if one exists. + * + * Technically the function opens /run/snapd/ns/${group_name}.mnt and tries to + * use setns() with the obtained file descriptor. If the call succeeds then the + * function returns and subsequent call to sc_should_populate_ns_group() will + * return false. + * + * If the call fails then an eventfd is constructed and a support process is + * forked. The child process waits until data is written to the eventfd (this + * can be done by calling sc_preserve_populated_ns_group()). In the meantime + * the parent process unshares the mount namespace and sets a flag so that + * sc_should_populate_ns_group() returns true. + * + * @returns true if the mount namespace needs to be populated + **/ +void sc_create_or_join_ns_group(struct sc_ns_group *group, + struct sc_apparmor *apparmor); + +/** + * Check if the namespace needs to be populated. + * + * If the return value is true then at this stage the namespace is already + * unshared. The caller should perform any mount operations that are desired + * and then proceed to call sc_preserve_populated_ns_group(). + **/ +bool sc_should_populate_ns_group(struct sc_ns_group *group); + +/** + * Preserve prepared namespace group. + * + * This function signals the child support process for namespace capture to + * perform the capture and shut down. It must be called after the call to + * sc_create_or_join_ns_group() and only when sc_should_populate_ns_group() + * returns true. + * + * Technically this function writes to an eventfd that causes the child process + * to wake up, bind mount /proc/$ppid/ns/mnt to /run/snapd/ns/${group_name}.mnt + * and then exit. The parent process (the caller) then collects the child + * process and returns. + **/ +void sc_preserve_populated_ns_group(struct sc_ns_group *group); + +/** + * Discard the preserved namespace group. + * + * This function unmounts the bind-mounted files representing the kernel mount + * namespace. + **/ +void sc_discard_preserved_ns_group(struct sc_ns_group *group); + +#endif diff --git a/cmd/snap-confine/quirks.c b/cmd/snap-confine/quirks.c new file mode 100644 index 00000000..96e85123 --- /dev/null +++ b/cmd/snap-confine/quirks.c @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2016 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 . + * + */ +#include "config.h" +#include "quirks.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "utils.h" +#include "cleanup-funcs.h" +#include "classic.h" +#include "mount-opt.h" +// XXX: for smaller patch, this should be in utils.h later +#include "user-support.h" + +/** + * Get the path to the mounted core snap in the execution environment. + * + * The core snap may be named just "core" (preferred) or "ubuntu-core" + * (legacy). The mount point does not depend on build-time configuration and + * does not differ from distribution to distribution. + **/ +static const char *sc_get_inner_core_mount_point() +{ + const char *core_path = "/snap/core/current/"; + const char *ubuntu_core_path = "/snap/ubuntu-core/current/"; + static const char *result = NULL; + if (result == NULL) { + if (access(core_path, F_OK) == 0) { + // Use the "core" snap if available. + result = core_path; + } else if (access(ubuntu_core_path, F_OK) == 0) { + // If not try to fall back to the "ubuntu-core" snap. + result = ubuntu_core_path; + } else { + die("cannot locate the core snap"); + } + } + return result; +} + +/** + * Mount a tmpfs at a given directory. + * + * The empty tmpfs is used as a substrate to create additional directories and + * then bind mounts to other destinations. + * + * It is useful to poke unexpected holes in the read-only core snap. + **/ +static void sc_quirk_setup_tmpfs(const char *dirname) +{ + debug("mounting tmpfs at %s", dirname); + if (mount("none", dirname, "tmpfs", MS_NODEV | MS_NOSUID, NULL) != 0) { + die("cannot mount tmpfs at %s", dirname); + }; +} + +/** + * Create an empty directory and bind mount something there. + * + * The empty directory is created at destdir. The bind mount is + * done from srcdir to destdir. The bind mount is performed with + * caller-defined flags. + **/ +static void sc_quirk_mkdir_bind(const char *src_dir, const char *dest_dir, + unsigned flags) +{ + flags |= MS_BIND; + debug("creating empty directory at %s", dest_dir); + if (sc_nonfatal_mkpath(dest_dir, 0755) < 0) { + die("cannot create empty directory at %s", dest_dir); + } + const char *flags_str = sc_mount_opt2str(flags); + debug("performing operation: mount %s %s -o %s", src_dir, dest_dir, + flags_str); + if (mount(src_dir, dest_dir, NULL, flags, NULL) != 0) { + die("cannot perform operation: mount %s %s -o %s", src_dir, + dest_dir, flags_str); + } +} + +/** + * Create a writable mimic directory based on reference directory. + * + * The mimic directory is a tmpfs populated with bind mounts to the (possibly + * read only) directories in the reference directory. While all the read-only + * content stays read-only the actual mimic directory is writable so additional + * content can be placed there. + * + * Flags are forwarded to sc_quirk_mkdir_bind() + **/ +static void sc_quirk_create_writable_mimic(const char *mimic_dir, + const char *ref_dir, unsigned flags) +{ + debug("creating writable mimic directory %s based on %s", mimic_dir, + ref_dir); + sc_quirk_setup_tmpfs(mimic_dir); + debug("bind-mounting all the files from the reference directory"); + DIR *dirp __attribute__ ((cleanup(sc_cleanup_closedir))) = NULL; + dirp = opendir(ref_dir); + if (dirp == NULL) { + die("cannot open reference directory %s", ref_dir); + } + struct dirent *entryp = NULL; + do { + char src_name[PATH_MAX * 2]; + char dest_name[PATH_MAX * 2]; + // Set errno to zero, if readdir fails it will not only return null but + // set errno to a non-zero value. This is how we can differentiate + // end-of-directory from an actual error. + errno = 0; + entryp = readdir(dirp); + if (entryp == NULL && errno != 0) { + die("cannot read another directory entry"); + } + if (entryp == NULL) { + break; + } + if (strcmp(entryp->d_name, ".") == 0 + || strcmp(entryp->d_name, "..") == 0) { + continue; + } + if (entryp->d_type != DT_DIR && entryp->d_type != DT_REG) { + die("unsupported entry type of file %s (%d)", + entryp->d_name, entryp->d_type); + } + must_snprintf(src_name, sizeof src_name, "%s/%s", ref_dir, + entryp->d_name); + must_snprintf(dest_name, sizeof dest_name, "%s/%s", mimic_dir, + entryp->d_name); + sc_quirk_mkdir_bind(src_name, dest_name, flags); + } while (entryp != NULL); +} + +/** + * Setup a quirk for LXD. + * + * An existing LXD snap relies on pre-chroot behavior to access /var/lib/lxd + * while in devmode. Since that directory doesn't exist in the core snap the + * quirk punches a custom hole so that this directory shows the hostfs content + * if such directory exists on the host. + * + * See: https://bugs.launchpad.net/snap-confine/+bug/1613845 + **/ +static void sc_setup_lxd_quirk() +{ + const char *hostfs_lxd_dir = SC_HOSTFS_DIR "/var/lib/lxd"; + if (access(hostfs_lxd_dir, F_OK) == 0) { + const char *lxd_dir = "/var/lib/lxd"; + debug("setting up quirk for LXD (see LP: #1613845)"); + sc_quirk_mkdir_bind(hostfs_lxd_dir, lxd_dir, + MS_REC | MS_SLAVE | MS_NODEV | MS_NOSUID | + MS_NOEXEC); + } +} + +void sc_setup_quirks() +{ + // because /var/lib/snapd is essential let's move it to /tmp/snapd for a sec + char snapd_tmp[] = "/tmp/snapd.quirks_XXXXXX"; + if (mkdtemp(snapd_tmp) == 0) { + die("cannot create temporary directory for /var/lib/snapd mount point"); + } + debug("performing operation: mount --move %s %s", "/var/lib/snapd", + snapd_tmp); + if (mount("/var/lib/snapd", snapd_tmp, NULL, MS_MOVE, NULL) + != 0) { + die("cannot perform operation: mount --move %s %s", + "/var/lib/snapd", snapd_tmp); + } + // now let's make /var/lib the vanilla /var/lib from the core snap + char buf[PATH_MAX]; + must_snprintf(buf, sizeof buf, "%s/var/lib", + sc_get_inner_core_mount_point()); + sc_quirk_create_writable_mimic("/var/lib", buf, + MS_RDONLY | MS_REC | MS_SLAVE | MS_NODEV + | MS_NOSUID); + // now let's move /var/lib/snapd (that was originally there) back + debug("performing operation: umount %s", "/var/lib/snapd"); + if (umount("/var/lib/snapd") != 0) { + die("cannot perform operation: umount %s", "/var/lib/snapd"); + } + debug("performing operation: mount --move %s %s", snapd_tmp, + "/var/lib/snapd"); + if (mount(snapd_tmp, "/var/lib/snapd", NULL, MS_MOVE, NULL) + != 0) { + die("cannot perform operation: mount --move %s %s", snapd_tmp, + "/var/lib/snapd"); + } + debug("performing operation: rmdir %s", snapd_tmp); + if (rmdir(snapd_tmp) != 0) { + die("cannot perform operation: rmdir %s", snapd_tmp); + } + // We are now ready to apply any quirks that relate to /var/lib + sc_setup_lxd_quirk(); +} diff --git a/cmd/snap-confine/quirks.h b/cmd/snap-confine/quirks.h new file mode 100644 index 00000000..3d323d73 --- /dev/null +++ b/cmd/snap-confine/quirks.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#ifndef SNAP_QUIRKS_H +#define SNAP_QUIRKS_H + +/** + * Setup various quirks that have to exists for now. + * + * This function applies non-standard tweaks that are required + * because of requirement to stay compatible with certain snaps + * that were tested with pre-chroot layout. + **/ +void sc_setup_quirks(); + +#endif diff --git a/cmd/snap-confine/seccomp-support.c b/cmd/snap-confine/seccomp-support.c new file mode 100644 index 00000000..ced5044f --- /dev/null +++ b/cmd/snap-confine/seccomp-support.c @@ -0,0 +1,759 @@ +/* + * Copyright (C) 2015-2017 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 . + * + */ +#include "config.h" +#include "seccomp-support.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// needed for search mappings +#include +#include +#include +#include +#include +#include + +#include + +#include "utils.h" +#include "secure-getenv.h" + +#define sc_map_add(X) sc_map_add_kvp(#X, X) + +// libseccomp maximum per ARG_COUNT_MAX in src/arch.h +#define SC_ARGS_MAXLENGTH 6 +#define SC_MAX_LINE_LENGTH 82 // 80 + '\n' + '\0' + +enum parse_ret { + PARSE_INVALID_SYSCALL = -2, + PARSE_ERROR = -1, + PARSE_OK = 0, +}; + +struct preprocess { + bool unrestricted; + bool complain; +}; + +/* + * arg_cmp contains items of type scmp_arg_cmp (from SCMP_CMP macro) and + * length is the number of items in arg_cmp that are active such that if + * length is '3' arg_cmp[0], arg_cmp[1] and arg_cmp[2] are used, when length + * is '1' only arg_cmp[0] and when length is '0', none are used. + */ +struct seccomp_args { + int syscall_nr; + unsigned int length; + struct scmp_arg_cmp arg_cmp[SC_ARGS_MAXLENGTH]; +}; + +struct sc_map_entry { + ENTRY *e; + ENTRY *ep; + struct sc_map_entry *next; +}; + +struct sc_map_list { + struct sc_map_entry *list; + int count; +}; + +static char *filter_profile_dir = "/var/lib/snapd/seccomp/profiles/"; +static struct hsearch_data sc_map_htab; +static struct sc_map_list sc_map_entries; + +/* + * Setup an hsearch map to map strings in the policy (eg, AF_UNIX) to + * scmp_datum_t values. Abstract away hsearch implementation behind sc_map_* + * functions in case we want to swap this out. + * + * sc_map_init() - initialize the hash map via linked list of + * of entries + * sc_map_add_kvp(key, value) - create entry from key/value pair and add to + * linked list + * sc_map_search(s) - if found, return scmp_datum_t for key, else set errno + * sc_map_destroy() - destroy the hash map and linked list + */ +static scmp_datum_t sc_map_search(char *s) +{ + ENTRY e; + ENTRY *ep = NULL; + scmp_datum_t val = 0; + errno = 0; + + e.key = s; + if (hsearch_r(e, FIND, &ep, &sc_map_htab) == 0) + die("hsearch_r failed"); + + if (ep != NULL) { + scmp_datum_t *val_p = NULL; + val_p = ep->data; + val = *val_p; + } else + errno = EINVAL; + + return val; +} + +static void sc_map_add_kvp(const char *key, scmp_datum_t value) +{ + struct sc_map_entry *node; + scmp_datum_t *value_copy; + + node = malloc(sizeof(*node)); + if (node == NULL) + die("Out of memory creating sc_map_entries"); + + node->e = malloc(sizeof(*node->e)); + if (node->e == NULL) + die("Out of memory creating ENTRY"); + + node->e->key = strdup(key); + if (node->e->key == NULL) + die("Out of memory creating e->key"); + + value_copy = malloc(sizeof(*value_copy)); + if (value_copy == NULL) + die("Out of memory creating e->data"); + *value_copy = value; + node->e->data = value_copy; + + node->ep = NULL; + node->next = NULL; + + if (sc_map_entries.list == NULL) { + sc_map_entries.count = 1; + sc_map_entries.list = node; + } else { + struct sc_map_entry *p = sc_map_entries.list; + while (p->next != NULL) + p = p->next; + p->next = node; + sc_map_entries.count++; + } +} + +static void sc_map_init() +{ + // initialize the map linked list + sc_map_entries.list = NULL; + sc_map_entries.count = 0; + + // build up the map linked list + + // man 2 socket - domain + sc_map_add(AF_UNIX); + sc_map_add(AF_LOCAL); + sc_map_add(AF_INET); + sc_map_add(AF_INET6); + sc_map_add(AF_IPX); + sc_map_add(AF_NETLINK); + sc_map_add(AF_X25); + sc_map_add(AF_AX25); + sc_map_add(AF_ATMPVC); + sc_map_add(AF_APPLETALK); + sc_map_add(AF_PACKET); + sc_map_add(AF_ALG); + // linux/can.h + sc_map_add(AF_CAN); + + // man 2 socket - type + sc_map_add(SOCK_STREAM); + sc_map_add(SOCK_DGRAM); + sc_map_add(SOCK_SEQPACKET); + sc_map_add(SOCK_RAW); + sc_map_add(SOCK_RDM); + sc_map_add(SOCK_PACKET); + + // man 2 prctl +#ifndef PR_CAP_AMBIENT +#define PR_CAP_AMBIENT 47 +#define PR_CAP_AMBIENT_IS_SET 1 +#define PR_CAP_AMBIENT_RAISE 2 +#define PR_CAP_AMBIENT_LOWER 3 +#define PR_CAP_AMBIENT_CLEAR_ALL 4 +#endif // PR_CAP_AMBIENT + + sc_map_add(PR_CAP_AMBIENT); + sc_map_add(PR_CAP_AMBIENT_RAISE); + sc_map_add(PR_CAP_AMBIENT_LOWER); + sc_map_add(PR_CAP_AMBIENT_IS_SET); + sc_map_add(PR_CAP_AMBIENT_CLEAR_ALL); + sc_map_add(PR_CAPBSET_READ); + sc_map_add(PR_CAPBSET_DROP); + sc_map_add(PR_SET_CHILD_SUBREAPER); + sc_map_add(PR_GET_CHILD_SUBREAPER); + sc_map_add(PR_SET_DUMPABLE); + sc_map_add(PR_GET_DUMPABLE); + sc_map_add(PR_SET_ENDIAN); + sc_map_add(PR_GET_ENDIAN); + sc_map_add(PR_SET_FPEMU); + sc_map_add(PR_GET_FPEMU); + sc_map_add(PR_SET_FPEXC); + sc_map_add(PR_GET_FPEXC); + sc_map_add(PR_SET_KEEPCAPS); + sc_map_add(PR_GET_KEEPCAPS); + sc_map_add(PR_MCE_KILL); + sc_map_add(PR_MCE_KILL_GET); + sc_map_add(PR_SET_MM); + sc_map_add(PR_SET_MM_START_CODE); + sc_map_add(PR_SET_MM_END_CODE); + sc_map_add(PR_SET_MM_START_DATA); + sc_map_add(PR_SET_MM_END_DATA); + sc_map_add(PR_SET_MM_START_STACK); + sc_map_add(PR_SET_MM_START_BRK); + sc_map_add(PR_SET_MM_BRK); + sc_map_add(PR_SET_MM_ARG_START); + sc_map_add(PR_SET_MM_ARG_END); + sc_map_add(PR_SET_MM_ENV_START); + sc_map_add(PR_SET_MM_ENV_END); + sc_map_add(PR_SET_MM_AUXV); + sc_map_add(PR_SET_MM_EXE_FILE); +#ifndef PR_MPX_ENABLE_MANAGEMENT +#define PR_MPX_ENABLE_MANAGEMENT 43 +#endif // PR_MPX_ENABLE_MANAGEMENT + sc_map_add(PR_MPX_ENABLE_MANAGEMENT); +#ifndef PR_MPX_DISABLE_MANAGEMENT +#define PR_MPX_DISABLE_MANAGEMENT 44 +#endif // PR_MPX_DISABLE_MANAGEMENT + sc_map_add(PR_MPX_DISABLE_MANAGEMENT); + sc_map_add(PR_SET_NAME); + sc_map_add(PR_GET_NAME); + sc_map_add(PR_SET_NO_NEW_PRIVS); + sc_map_add(PR_GET_NO_NEW_PRIVS); + sc_map_add(PR_SET_PDEATHSIG); + sc_map_add(PR_GET_PDEATHSIG); + sc_map_add(PR_SET_PTRACER); + sc_map_add(PR_SET_SECCOMP); + sc_map_add(PR_GET_SECCOMP); + sc_map_add(PR_SET_SECUREBITS); + sc_map_add(PR_GET_SECUREBITS); +#ifndef PR_SET_THP_DISABLE +#define PR_SET_THP_DISABLE 41 +#endif // PR_SET_THP_DISABLE + sc_map_add(PR_SET_THP_DISABLE); + sc_map_add(PR_TASK_PERF_EVENTS_DISABLE); + sc_map_add(PR_TASK_PERF_EVENTS_ENABLE); +#ifndef PR_GET_THP_DISABLE +#define PR_GET_THP_DISABLE 42 +#endif // PR_GET_THP_DISABLE + sc_map_add(PR_GET_THP_DISABLE); + sc_map_add(PR_GET_TID_ADDRESS); + sc_map_add(PR_SET_TIMERSLACK); + sc_map_add(PR_GET_TIMERSLACK); + sc_map_add(PR_SET_TIMING); + sc_map_add(PR_GET_TIMING); + sc_map_add(PR_SET_TSC); + sc_map_add(PR_GET_TSC); + sc_map_add(PR_SET_UNALIGN); + sc_map_add(PR_GET_UNALIGN); + + // man 2 getpriority + sc_map_add(PRIO_PROCESS); + sc_map_add(PRIO_PGRP); + sc_map_add(PRIO_USER); + + // man 2 setns + sc_map_add(CLONE_NEWIPC); + sc_map_add(CLONE_NEWNET); + sc_map_add(CLONE_NEWNS); + sc_map_add(CLONE_NEWPID); + sc_map_add(CLONE_NEWUSER); + sc_map_add(CLONE_NEWUTS); + + // initialize the htab for our map + memset((void *)&sc_map_htab, 0, sizeof(sc_map_htab)); + if (hcreate_r(sc_map_entries.count, &sc_map_htab) == 0) + die("could not create map"); + + // add elements from linked list to map + struct sc_map_entry *p = sc_map_entries.list; + while (p != NULL) { + errno = 0; + if (hsearch_r(*p->e, ENTER, &p->ep, &sc_map_htab) == 0) + die("hsearch_r failed"); + + if (&p->ep == NULL) + die("could not initialize map"); + + p = p->next; + } +} + +static void sc_map_destroy() +{ + // this frees all of the nodes' ep so we don't have to below + hdestroy_r(&sc_map_htab); + + struct sc_map_entry *next = sc_map_entries.list; + struct sc_map_entry *p = NULL; + while (next != NULL) { + p = next; + next = p->next; + free(p->e->key); + free(p->e->data); + free(p->e); + free(p); + } +} + +/* Caller must check if errno != 0 */ +static scmp_datum_t read_number(char *s) +{ + scmp_datum_t val = 0; + + errno = 0; + + // per seccomp.h definition of scmp_datum_t, negative numbers are not + // supported, so fail if we see one or if we get one. Also fail if + // string is 0 length. + if (s[0] == '-' || s[0] == '\0') { + errno = EINVAL; + return val; + } + // check if number + for (int i = 0; i < strlen(s); i++) { + if (isdigit(s[i]) == 0) { + errno = EINVAL; + break; + } + } + if (errno == 0) { // found a number, so parse it + char *end; + // strtol may set errno to ERANGE + val = strtoul(s, &end, 10); + if (end == s || *end != '\0') + errno = EINVAL; + } else // try our map (sc_map_search sets errno) + val = sc_map_search(s); + + return val; +} + +static int parse_line(char *line, struct seccomp_args *sargs) +{ + // strtok_r needs a pointer to keep track of where it is in the + // string. + char *buf_saveptr; + + // Initialize our struct + sargs->length = 0; + sargs->syscall_nr = -1; + + if (strlen(line) == 0) + return PARSE_ERROR; + + // Initialize tokenizer and obtain first token. + char *buf_token = strtok_r(line, " \t", &buf_saveptr); + if (buf_token == NULL) + return PARSE_ERROR; + + // syscall not available on this arch/kernel + sargs->syscall_nr = seccomp_syscall_resolve_name(buf_token); + if (sargs->syscall_nr == __NR_SCMP_ERROR) + return PARSE_INVALID_SYSCALL; + + // Parse for syscall arguments. Since we haven't yet searched for the + // next token, buf_token is still the syscall itself so start 'pos' as + // -1 and only if there is an arg to parse, increment it. + int pos = -1; + while (pos < SC_ARGS_MAXLENGTH) { + buf_token = strtok_r(NULL, " \t", &buf_saveptr); + if (buf_token == NULL) + break; + // we found a token, so increment position and process it + pos++; + if (strcmp(buf_token, "-") == 0) // skip arg + continue; + + enum scmp_compare op = -1; + scmp_datum_t value = 0; + if (strlen(buf_token) == 0) { + return PARSE_ERROR; + } else if (strlen(buf_token) == 1) { + // syscall N (length of '1' indicates a single digit) + op = SCMP_CMP_EQ; + value = read_number(buf_token); + } else if (strncmp(buf_token, ">=", 2) == 0) { + // syscall >=N + op = SCMP_CMP_GE; + value = read_number(&buf_token[2]); + } else if (strncmp(buf_token, "<=", 2) == 0) { + // syscall <=N + op = SCMP_CMP_LE; + value = read_number(&buf_token[2]); + } else if (strncmp(buf_token, "!", 1) == 0) { + // syscall !N + op = SCMP_CMP_NE; + value = read_number(&buf_token[1]); + } else if (strncmp(buf_token, ">", 1) == 0) { + // syscall >N + op = SCMP_CMP_GT; + value = read_number(&buf_token[1]); + } else if (strncmp(buf_token, "<", 1) == 0) { + // syscall arg_cmp[sargs->length] = SCMP_CMP(pos, op, value); + sargs->length++; + + //printf("\nDEBUG: SCMP_CMP(%d, %d, %llu)\n", pos, op, value); + } + // too many args + if (pos >= SC_ARGS_MAXLENGTH) + return PARSE_ERROR; + + return PARSE_OK; +} + +// strip whitespace from the end of the given string (inplace) +static size_t trim_right(char *s, size_t slen) +{ + while (slen > 0 && isspace(s[slen - 1])) { + s[--slen] = 0; + } + return slen; +} + +// Read a relevant line and return the length. Return length '0' for comments, +// empty lines and lines with only whitespace (so a caller can easily skip +// them). The line buffer is right whitespaced trimmed and the final length of +// the trimmed line is returned. +static size_t validate_and_trim_line(char *buf, size_t buf_len, size_t lineno) +{ + size_t len = 0; + + // comment, ignore + if (buf[0] == '#') + return len; + + // ensure the entire line was read + len = strlen(buf); + if (len == 0) + return len; + else if (buf[len - 1] != '\n' && len > (buf_len - 2)) { + fprintf(stderr, + "seccomp filter line %zu was too long (%zu characters max)\n", + lineno, buf_len - 2); + errno = 0; + die("aborting"); + } + // kill final newline + len = trim_right(buf, len); + + return len; +} + +static void preprocess_filter(FILE * f, struct preprocess *p) +{ + char buf[SC_MAX_LINE_LENGTH]; + size_t lineno = 0; + + p->unrestricted = false; + p->complain = false; + + while (fgets(buf, sizeof(buf), f) != NULL) { + lineno++; + + // skip policy-irrelevant lines + if (validate_and_trim_line(buf, sizeof(buf), lineno) == 0) + continue; + + // check for special "@unrestricted" rule which short-circuits + // seccomp sandbox + if (strcmp(buf, "@unrestricted") == 0) + p->unrestricted = true; + + // check for special "@complain" rule + if (strcmp(buf, "@complain") == 0) + p->complain = true; + } + + if (fseek(f, 0L, SEEK_SET) != 0) + die("could not rewind file"); + + return; +} + +static uint32_t uts_machine_to_seccomp_arch(const char *uts_machine) +{ + if (strcmp(uts_machine, "i686") == 0) + return SCMP_ARCH_X86; + else if (strcmp(uts_machine, "x86_64") == 0) + return SCMP_ARCH_X86_64; + else if (strncmp(uts_machine, "armv7", 5) == 0) + return SCMP_ARCH_ARM; +#if defined (SCMP_ARCH_AARCH64) + else if (strncmp(uts_machine, "aarch64", 7) == 0) + return SCMP_ARCH_AARCH64; +#endif +#if defined (SCMP_ARCH_PPC64LE) + else if (strncmp(uts_machine, "ppc64le", 7) == 0) + return SCMP_ARCH_PPC64LE; +#endif +#if defined (SCMP_ARCH_PPC64) + else if (strncmp(uts_machine, "ppc64", 5) == 0) + return SCMP_ARCH_PPC64; +#endif +#if defined (SCMP_ARCH_PPC) + else if (strncmp(uts_machine, "ppc", 3) == 0) + return SCMP_ARCH_PPC; +#endif +#if defined (SCMP_ARCH_S390X) + else if (strncmp(uts_machine, "s390x", 5) == 0) + return SCMP_ARCH_S390X; +#endif + return 0; +} + +static uint32_t get_hostarch(void) +{ + struct utsname uts; + if (uname(&uts) < 0) + die("uname() failed"); + uint32_t arch = uts_machine_to_seccomp_arch(uts.machine); + if (arch > 0) + return arch; + // Just return the seccomp userspace native arch if we can't detect the + // kernel host arch. + return seccomp_arch_native(); +} + +static void sc_add_seccomp_archs(scmp_filter_ctx * ctx) +{ + uint32_t native_arch = seccomp_arch_native(); // seccomp userspace + uint32_t host_arch = get_hostarch(); // kernel + uint32_t compat_arch = 0; + + debug("host arch (kernel) is '%d'", host_arch); + debug("native arch (userspace) is '%d'", native_arch); + + // For architectures that support a compat architecture, when the + // kernel and userspace match, add the compat arch, otherwise add + // the kernel arch to support the kernel's arch (eg, 64bit kernels with + // 32bit userspace). + if (host_arch == native_arch) { + switch (host_arch) { +#if defined (SCMP_ARCH_X86_64) + case SCMP_ARCH_X86_64: + compat_arch = SCMP_ARCH_X86; + break; +#endif +#if defined(SCMP_ARCH_AARCH64) + case SCMP_ARCH_AARCH64: + compat_arch = SCMP_ARCH_ARM; + break; +#endif +#if defined (SCMP_ARCH_PPC64) + case SCMP_ARCH_PPC64: + compat_arch = SCMP_ARCH_PPC; + break; +#endif + default: + break; + } + } else + compat_arch = host_arch; + + if (compat_arch > 0 && seccomp_arch_exist(ctx, compat_arch) == -EEXIST) { + debug("adding compat arch '%d'", compat_arch); + if (seccomp_arch_add(ctx, compat_arch) < 0) + die("seccomp_arch_add(..., compat_arch) failed"); + } +} + +scmp_filter_ctx sc_prepare_seccomp_context(const char *filter_profile) +{ + int rc = 0; + scmp_filter_ctx ctx = NULL; + FILE *f = NULL; + size_t lineno = 0; + uid_t real_uid, effective_uid, saved_uid; + struct preprocess pre; + struct seccomp_args sargs; + + debug("preparing seccomp profile associated with security tag %s", + filter_profile); + + // initialize hsearch map + sc_map_init(); + + ctx = seccomp_init(SCMP_ACT_KILL); + if (ctx == NULL) { + errno = ENOMEM; + die("seccomp_init() failed"); + } + // Setup native arch and any compatibility archs + sc_add_seccomp_archs(ctx); + + // Disable NO_NEW_PRIVS because it interferes with exec transitions in + // AppArmor. Unfortunately this means that security policies must be + // very careful to not allow the following otherwise apps can escape + // the sandbox: + // - seccomp syscall + // - prctl with PR_SET_SECCOMP + // - ptrace (trace) in AppArmor + // - capability sys_admin in AppArmor + // Note that with NO_NEW_PRIVS disabled, CAP_SYS_ADMIN is required to + // change the seccomp sandbox. + + if (getresuid(&real_uid, &effective_uid, &saved_uid) != 0) + die("could not find user IDs"); + + // If running privileged or capable of raising, disable nnp + if (real_uid == 0 || effective_uid == 0 || saved_uid == 0) + if (seccomp_attr_set(ctx, SCMP_FLTATR_CTL_NNP, 0) != 0) + die("Cannot disable nnp"); + + // Note that secure_gettenv will always return NULL when suid, so + // SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR can't be (ab)used in that case. + if (secure_getenv("SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR") != NULL) + filter_profile_dir = + secure_getenv("SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR"); + + char profile_path[512]; // arbitrary path name limit + must_snprintf(profile_path, sizeof(profile_path), "%s/%s", + filter_profile_dir, filter_profile); + + f = fopen(profile_path, "r"); + if (f == NULL) { + fprintf(stderr, "Can not open %s (%s)\n", profile_path, + strerror(errno)); + die("aborting"); + } + // Note, preprocess_filter() die()s on error + preprocess_filter(f, &pre); + + if (pre.unrestricted) { + seccomp_release(ctx); + ctx = NULL; + goto out; + } + // FIXME: right now complain mode is the equivalent to unrestricted. + // We'll want to change this once we seccomp logging is in order. + if (pre.complain) { + seccomp_release(ctx); + ctx = NULL; + goto out; + } + + char buf[SC_MAX_LINE_LENGTH]; + while (fgets(buf, sizeof(buf), f) != NULL) { + lineno++; + + // skip policy-irrelevant lines + if (validate_and_trim_line(buf, sizeof(buf), lineno) == 0) + continue; + + char *buf_copy = strdup(buf); + if (buf_copy == NULL) + die("Out of memory"); + + int pr_rc = parse_line(buf_copy, &sargs); + free(buf_copy); + if (pr_rc != PARSE_OK) { + // as this is a syscall whitelist an invalid syscall + // is ok and the error can be ignored + if (pr_rc == PARSE_INVALID_SYSCALL) + continue; + die("could not parse line"); + } + + rc = seccomp_rule_add_exact_array(ctx, SCMP_ACT_ALLOW, + sargs.syscall_nr, + sargs.length, sargs.arg_cmp); + if (rc != 0) { + rc = seccomp_rule_add_array(ctx, SCMP_ACT_ALLOW, + sargs.syscall_nr, + sargs.length, + sargs.arg_cmp); + if (rc != 0) { + fprintf(stderr, + "seccomp_rule_add_array failed with %i for '%s'\n", + rc, buf); + errno = 0; + die("aborting"); + } + } + } + + out: + if (f != NULL) { + if (fclose(f) != 0) + die("could not close seccomp file"); + } + sc_map_destroy(); + return ctx; +} + +void sc_load_seccomp_context(scmp_filter_ctx ctx) +{ + int rc; + uid_t real_uid, effective_uid, saved_uid; + + // if sc_prepare_seccomp_context() sees @unrestricted or @complain it bails + // out early and destroys the context object. In that case we have nothing + // to do. + if (ctx == NULL) { + return; + } + + if (getresuid(&real_uid, &effective_uid, &saved_uid) != 0) + die("could not find user IDs"); + + // If not root but can raise, then raise privileges to load seccomp + // policy since we don't have nnp + debug("raising privileges to load seccomp profile"); + if (effective_uid != 0 && saved_uid == 0) { + if (seteuid(0) != 0) + die("seteuid failed"); + if (geteuid() != 0) + die("raising privs before seccomp_load did not work"); + } + // load it into the kernel + debug("loading seccomp profile into the kernel"); + rc = seccomp_load(ctx); + if (rc != 0) { + fprintf(stderr, "seccomp_load failed with %i\n", rc); + die("aborting"); + } + // drop privileges again + debug("dropping privileges after loading seccomp profile"); + if (geteuid() == 0) { + unsigned real_uid = getuid(); + if (seteuid(real_uid) != 0) + die("seteuid failed"); + if (real_uid != 0 && geteuid() == 0) + die("dropping privs after seccomp_load did not work"); + } +} diff --git a/cmd/snap-confine/seccomp-support.h b/cmd/snap-confine/seccomp-support.h new file mode 100644 index 00000000..68aacf2a --- /dev/null +++ b/cmd/snap-confine/seccomp-support.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2015-2017 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 . + * + */ +#ifndef SNAP_CONFINE_SECCOMP_SUPPORT_H +#define SNAP_CONFINE_SECCOMP_SUPPORT_H + +#include + +/** + * Prepare seccomp profile associated with the security tag. + * + * This function loads the seccomp profile from + * /var/lib/snapd/seccomp/profiles/$SECURITY_TAG and stores it into + * scmp_filter_ctx object. + * + * The object is returned to the caller and can be made effective with a call + * to sc_load_seccomp_context(). The returned value should be cleaned up with + * seccomp_release(). + * + * This function calls die() on all errors. + **/ + +scmp_filter_ctx sc_prepare_seccomp_context(const char *security_tag); + +/** + * Load a seccomp context. + * + * This function calls seccomp_load(3) and handles errors if it fails. + **/ +void sc_load_seccomp_context(scmp_filter_ctx ctx); + +#endif diff --git a/cmd/snap-confine/secure-getenv.c b/cmd/snap-confine/secure-getenv.c new file mode 100644 index 00000000..cc8b4dc8 --- /dev/null +++ b/cmd/snap-confine/secure-getenv.c @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016 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 . + * + */ +#include "secure-getenv.h" + +#include +#include + +#ifndef HAVE_SECURE_GETENV +char *secure_getenv(const char *name) +{ + unsigned long secure = getauxval(AT_SECURE); + if (secure != 0) { + return NULL; + } + return getenv(name); +} +#endif // ! HAVE_SECURE_GETENV diff --git a/cmd/snap-confine/secure-getenv.h b/cmd/snap-confine/secure-getenv.h new file mode 100644 index 00000000..1b139a3a --- /dev/null +++ b/cmd/snap-confine/secure-getenv.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 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 . + * + */ +#ifndef SNAP_CONFINE_SECURE_GETENV_H +#define SNAP_CONFINE_SECURE_GETENV_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#ifndef HAVE_SECURE_GETENV +/** + * Secure version of getenv() + * + * This version returns NULL if the process is running within a secure context. + * This is exactly the same as the GNU extension to the standard library. It is + * only used when glibc is not available. + **/ +char *secure_getenv(const char *name) + __attribute__ ((nonnull(1), warn_unused_result)); +#endif // ! HAVE_SECURE_GETENV + +#endif diff --git a/cmd/snap-confine/snap-confine.apparmor.in b/cmd/snap-confine/snap-confine.apparmor.in new file mode 100644 index 00000000..200d9642 --- /dev/null +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -0,0 +1,358 @@ +# Author: Jamie Strandboge +#include + +@LIBEXECDIR@/snap-confine (attach_disconnected) { + # We run privileged, so be fanatical about what we include and don't use + # any abstractions + /etc/ld.so.cache r, + /lib/@{multiarch}/ld-*.so mr, + # libc, you are funny + /lib/@{multiarch}/libc{,-[0-9]*}.so* mr, + /lib/@{multiarch}/libpthread{,-[0-9]*}.so* mr, + /lib/@{multiarch}/librt{,-[0-9]*}.so* mr, + /lib/@{multiarch}/libgcc_s.so* mr, + # normal libs in order + /lib/@{multiarch}/libapparmor.so* mr, + /lib/@{multiarch}/libcgmanager.so* mr, + /lib/@{multiarch}/libdl-[0-9]*.so* mr, + /lib/@{multiarch}/libnih.so* mr, + /lib/@{multiarch}/libnih-dbus.so* mr, + /lib/@{multiarch}/libdbus-1.so* mr, + /lib/@{multiarch}/libudev.so* mr, + /usr/lib/@{multiarch}/libseccomp.so* mr, + /lib/@{multiarch}/libseccomp.so* mr, + + @LIBEXECDIR@/snap-confine mr, + + /dev/null rw, + /dev/full rw, + /dev/zero rw, + /dev/random r, + /dev/urandom r, + /dev/pts/[0-9]* rw, + + # cgroups + capability sys_admin, + capability dac_override, + /sys/fs/cgroup/devices/snap{,py}.*/ w, + /sys/fs/cgroup/devices/snap{,py}.*/tasks w, + /sys/fs/cgroup/devices/snap{,py}.*/devices.{allow,deny} w, + + # querying udev + /etc/udev/udev.conf r, + /sys/devices/**/uevent r, + /lib/udev/snappy-app-dev ixr, # drop + /run/udev/** rw, + /{,usr/}bin/tr ixr, + /usr/lib/locale/** r, + /usr/lib/@{multiarch}/gconv/gconv-modules r, + /usr/lib/@{multiarch}/gconv/gconv-modules.cache r, + + # priv dropping + capability setuid, + capability setgid, + + # changing profile + @{PROC}/[0-9]*/attr/exec w, + # Reading current profile + @{PROC}/[0-9]*/attr/current r, + + # To find where apparmor is mounted + @{PROC}/[0-9]*/mounts r, + # To find if apparmor is enabled + /sys/module/apparmor/parameters/enabled r, + + # Don't allow changing profile to unconfined or profiles that start with + # '/'. Use 'unsafe' to support snap-exec on armhf and its reliance on + # the environment for determining the capabilities of the architecture. + # 'unsafe' is ok here because the kernel will have already cleared the + # environment as part of launching snap-confine with + # CAP_SYS_ADMIN. + change_profile unsafe /** -> [^u/]**, + change_profile unsafe /** -> u[^n]**, + change_profile unsafe /** -> un[^c]**, + change_profile unsafe /** -> unc[^o]**, + change_profile unsafe /** -> unco[^n]**, + change_profile unsafe /** -> uncon[^f]**, + change_profile unsafe /** -> unconf[^i]**, + change_profile unsafe /** -> unconfi[^n]**, + change_profile unsafe /** -> unconfin[^e]**, + change_profile unsafe /** -> unconfine[^d]**, + change_profile unsafe /** -> unconfined?**, + + # allow changing to a few not caught above + change_profile unsafe /** -> {u,un,unc,unco,uncon,unconf,unconfi,unconfin,unconfine}, + + # LP: #1446794 - when this bug is fixed, change the above to: + # deny change_profile unsafe /** -> {unconfined,/**}, + # change_profile unsafe /** -> **, + + # reading seccomp filters + /{tmp/snap.rootfs_*/,}var/lib/snapd/seccomp/profiles/* r, + + # reading mount profiles + /{tmp/snap.rootfs_*/,}var/lib/snapd/mount/*.fstab r, + + # boostrapping the mount namespace + mount options=(rw rshared) -> /, + mount options=(rw bind) /tmp/snap.rootfs_*/ -> /tmp/snap.rootfs_*/, + mount options=(rw unbindable) -> /tmp/snap.rootfs_*/, + # the next line is for classic system + mount options=(rw rbind) @SNAP_MOUNT_DIR@/{,ubuntu-}core/*/ -> /tmp/snap.rootfs_*/, + # the next line is for core system + mount options=(rw rbind) / -> /tmp/snap.rootfs_*/, + # all of the constructed rootfs is a rslave + mount options=(rw rslave) -> /tmp/snap.rootfs_*/, + # bidirectional mounts (for both classic and core) + # NOTE: this doesn't capture the MERGED_USR configuration option so that + # when a distro with merged /usr and / that uses apparmor shows up it + # should be handled here. + /{,run/}media/ w, + mount options=(rw rbind) /media/ -> /tmp/snap.rootfs_*/media/, + /run/netns/ w, + mount options=(rw rbind) /run/netns/ -> /tmp/snap.rootfs_*/run/netns/, + # unidirectional mounts (only for classic system) + mount options=(rw rbind) /dev/ -> /tmp/snap.rootfs_*/dev/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/dev/, + + mount options=(rw rbind) /etc/ -> /tmp/snap.rootfs_*/etc/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/etc/, + + mount options=(rw rbind) /home/ -> /tmp/snap.rootfs_*/home/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/home/, + + mount options=(rw rbind) /root/ -> /tmp/snap.rootfs_*/root/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/root/, + + mount options=(rw rbind) /proc/ -> /tmp/snap.rootfs_*/proc/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/proc/, + + mount options=(rw rbind) /sys/ -> /tmp/snap.rootfs_*/sys/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/sys/, + + mount options=(rw rbind) /tmp/ -> /tmp/snap.rootfs_*/tmp/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/tmp/, + + mount options=(rw rbind) /var/lib/snapd/ -> /tmp/snap.rootfs_*/var/lib/snapd/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/lib/snapd/, + + mount options=(rw rbind) /var/snap/ -> /tmp/snap.rootfs_*/var/snap/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/snap/, + + mount options=(rw rbind) /var/tmp/ -> /tmp/snap.rootfs_*/var/tmp/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/tmp/, + + mount options=(rw rbind) /run/ -> /tmp/snap.rootfs_*/run/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/run/, + + mount options=(rw rbind) {/usr,}/lib/modules/ -> /tmp/snap.rootfs_*/lib/modules/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/lib/modules/, + + mount options=(rw rbind) /var/log/ -> /tmp/snap.rootfs_*/var/log/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/log/, + + mount options=(rw rbind) /usr/src/ -> /tmp/snap.rootfs_*/usr/src/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/usr/src/, + # /etc/alternatives (classic) + mount options=(rw bind) @SNAP_MOUNT_DIR@/{,ubuntu-}core/*/etc/alternatives/ -> /tmp/snap.rootfs_*/etc/alternatives/, + # /etc/alternatives (core) + mount options=(rw bind) /etc/alternatives/ -> /tmp/snap.rootfs_*/etc/alternatives/, + mount options=(rw slave) -> /tmp/snap.rootfs_*/etc/alternatives/, + # the /snap directory + mount options=(rw rbind) @SNAP_MOUNT_DIR@/ -> /tmp/snap.rootfs_*/snap/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/snap/, + # pivot_root preparation and execution + mount options=(rw bind) /tmp/snap.rootfs_*/var/lib/snapd/hostfs/ -> /tmp/snap.rootfs_*/var/lib/snapd/hostfs/, + mount options=(rw private) -> /tmp/snap.rootfs_*/var/lib/snapd/hostfs/, + pivot_root, + # cleanup + umount /var/lib/snapd/hostfs/tmp/snap.rootfs_*/, + umount /var/lib/snapd/hostfs/sys/, + umount /var/lib/snapd/hostfs/dev/, + umount /var/lib/snapd/hostfs/proc/, + mount options=(rw rslave) -> /var/lib/snapd/hostfs/, + + # set up snap-specific private /tmp dir + capability chown, + /tmp/ w, + /tmp/snap.*/ w, + /tmp/snap.*/tmp/ w, + mount options=(rw private) -> /tmp/, + mount options=(rw bind) /tmp/snap.*/tmp/ -> /tmp/, + mount fstype=devpts options=(rw) devpts -> /dev/pts/, + mount options=(rw bind) /dev/pts/ptmx -> /dev/ptmx, # for bind mounting + mount options=(rw bind) /dev/pts/ptmx -> /dev/pts/ptmx, # for bind mounting under LXD + # Workaround for LP: #1584456 on older kernels that mistakenly think + # /dev/pts/ptmx needs a trailing '/' + mount options=(rw bind) /dev/pts/ptmx/ -> /dev/ptmx/, + mount options=(rw bind) /dev/pts/ptmx/ -> /dev/pts/ptmx/, + + # for running snaps on classic + /snap/ r, + /snap/** r, + @SNAP_MOUNT_DIR@/ r, + @SNAP_MOUNT_DIR@/** r, + + # NOTE: at this stage the /snap directory is stable as we have called + # pivot_root already. + + # Support mount profiles via the content interface. This should correspond + # to permutations of $SNAP -> $SNAP for reading and $SNAP_{DATA,COMMON} -> + # $SNAP_{DATA,COMMON} for both reading and writing. + # + # Note that: + # /snap/*/*/** + # is meant to mean: + # /snap/$SNAP_NAME/$SNAP_REVISION/and-any-subdirectory + # but: + # /var/snap/*/** + # is meant to mean: + # /var/snap/$SNAP_NAME/$SNAP_REVISION/ + mount options=(ro bind) /snap/*/** -> /snap/*/*/**, + mount options=(ro bind) /snap/*/** -> /var/snap/*/**, + mount options=(rw bind) /var/snap/*/** -> /var/snap/*/**, + mount options=(ro bind) /var/snap/*/** -> /var/snap/*/**, + # But we don't want anyone to touch /snap/bin + audit deny mount /snap/bin/** -> /**, + audit deny mount /** -> /snap/bin/**, + # Allow the content interface to bind fonts from the host filesystem + mount options=(ro bind) /var/lib/snapd/hostfs/usr/share/fonts/ -> /snap/*/*/**, + + # nvidia handling, glob needs /usr/** and the launcher must be + # able to bind mount the nvidia dir + /sys/module/nvidia/version r, + /usr/** r, + mount options=(rw bind) /usr/lib/nvidia-*/ -> /{tmp/snap.rootfs_*/,}var/lib/snapd/lib/gl/, + + # for chroot on steroids, we use pivot_root as a better chroot that makes + # apparmor rules behave the same on classic and outside of classic. + + # for creating the user data directories: ~/snap, ~/snap/ and + # ~/snap// + / r, + @{HOMEDIRS}/ r, + # These should both have 'owner' match but due to LP: #1466234, we can't + # yet + @{HOME}/ r, + @{HOME}/snap/{,*/,*/*/} rw, + + # for creating the user shared memory directories + /{dev,run}/{,shm/} r, + # This should both have 'owner' match but due to LP: #1466234, we can't yet + /{dev,run}/shm/{,*/,*/*/} rw, + + # for creating the user XDG_RUNTIME_DIR: /run/user, /run/user/UID and + # /run/user/UID/ + /run/user/{,[0-9]*/,[0-9]*/*/} rw, + + # Workaround https://launchpad.net/bugs/359338 until upstream handles + # stacked filesystems generally. + # encrypted ~/.Private and old-style encrypted $HOME + @{HOME}/.Private/ r, + @{HOME}/.Private/** mrixwlk, + # new-style encrypted $HOME + @{HOMEDIRS}/.ecryptfs/*/.Private/ r, + @{HOMEDIRS}/.ecryptfs/*/.Private/** mrixwlk, + + # Allow snap-confine to move to the void + /var/lib/snapd/void/ r, + + # Support for the quirk system + /var/ r, + /var/lib/ r, + /var/lib/** rw, + /tmp/ r, + /tmp/snapd.quirks_*/ rw, + mount options=(move) /var/lib/snapd/ -> /tmp/snapd.quirks_*/, + mount fstype=tmpfs options=(rw nodev nosuid) none -> /var/lib/, + mount options=(ro rbind) /snap/{,ubuntu-}core/*/var/lib/** -> /var/lib/**, + umount /var/lib/snapd/, + mount options=(move) /tmp/snapd.quirks_*/ -> /var/lib/snapd/, + + # support for the LXD quirk + mount options=(rw rbind nodev nosuid noexec) /var/lib/snapd/hostfs/var/lib/lxd/ -> /var/lib/lxd/, + /var/lib/lxd/ w, + /var/lib/snapd/hostfs/var/lib/lxd r, + + # support for the mount namespace sharing + mount options=(rw rbind) /run/snapd/ns/ -> /run/snapd/ns/, + mount options=(private) -> /run/snapd/ns/, + / rw, + /run/ rw, + /run/snapd/ rw, + /run/snapd/ns/ rw, + /run/snapd/ns/*.lock rwk, + /run/snapd/ns/*.mnt rw, + ptrace (read, readby, tracedby) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + @{PROC}/*/mountinfo r, + capability sys_chroot, + capability sys_admin, + signal (send, receive) set=(abrt) peer=@LIBEXECDIR@/snap-confine, + signal (send) set=(int) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + signal (send, receive) set=(alrm, exists) peer=@LIBEXECDIR@/snap-confine, + signal (receive) set=(exists) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + + # For aa_change_hat() to go into ^mount-namespace-capture-helper + @{PROC}/[0-9]*/attr/current w, + + ^mount-namespace-capture-helper (attach_disconnected) { + # We run privileged, so be fanatical about what we include and don't use + # any abstractions + /etc/ld.so.cache r, + /lib/@{multiarch}/ld-*.so mr, + # libc, you are funny + /lib/@{multiarch}/libc{,-[0-9]*}.so* mr, + /lib/@{multiarch}/libpthread{,-[0-9]*}.so* mr, + /lib/@{multiarch}/librt{,-[0-9]*}.so* mr, + /lib/@{multiarch}/libgcc_s.so* mr, + # normal libs in order + /lib/@{multiarch}/libapparmor.so* mr, + /lib/@{multiarch}/libcgmanager.so* mr, + /lib/@{multiarch}/libnih.so* mr, + /lib/@{multiarch}/libnih-dbus.so* mr, + /lib/@{multiarch}/libdbus-1.so* mr, + /lib/@{multiarch}/libudev.so* mr, + /usr/lib/@{multiarch}/libseccomp.so* mr, + /lib/@{multiarch}/libseccomp.so* mr, + + @LIBEXECDIR@/snap-confine mr, + + /dev/null rw, + /dev/full rw, + /dev/zero rw, + /dev/random r, + /dev/urandom r, + + capability sys_ptrace, + capability sys_admin, + # This allows us to read and bind mount the namespace file + / r, + @{PROC}/ r, + @{PROC}/*/ r, + @{PROC}/*/ns/ r, + @{PROC}/*/ns/mnt r, + /run/ r, + /run/snapd/ r, + /run/snapd/ns/ r, + /run/snapd/ns/*.mnt rw, + # NOTE: the source name is / even though we map /proc/123/ns/mnt + mount options=(rw bind) / -> /run/snapd/ns/*.mnt, + # This is the SIGALRM that we send and receive if a timeout expires + signal (send, receive) set=(alrm) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + # Those two rules are exactly the same but we don't know if the parent process is still alive + # and hence has the appropriate label or is already dead and hence has no label. + signal (send) set=(exists) peer=@LIBEXECDIR@/snap-confine, + signal (send) set=(exists) peer=unconfined, + # This is so that we can abort + signal (send, receive) set=(abrt) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + # This is the signal we get if snap-confine dies (we subscribe to it with prctl) + signal (receive) set=(int) peer=@LIBEXECDIR@/snap-confine, + # This allows snap-confine to be killed from the outside. + signal (receive) peer=unconfined, + # This allows snap-confine to wait for us + ptrace (read, trace, tracedby) peer=@LIBEXECDIR@/snap-confine, + } + + # Allow snap-confine to be killed + signal (receive) peer=unconfined, +} diff --git a/cmd/snap-confine/snap-confine.c b/cmd/snap-confine/snap-confine.c new file mode 100644 index 00000000..20cf01de --- /dev/null +++ b/cmd/snap-confine/snap-confine.c @@ -0,0 +1,182 @@ +/* + * Copyright (C) 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 . + * + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include "classic.h" +#include "mount-support.h" +#include "snap.h" +#include "utils.h" +#ifdef HAVE_SECCOMP +#include "seccomp-support.h" +#endif // ifdef HAVE_SECCOMP +#include "udev-support.h" +#include "cleanup-funcs.h" +#include "user-support.h" +#include "ns-support.h" +#include "quirks.h" +#include "secure-getenv.h" +#include "apparmor-support.h" + +int main(int argc, char **argv) +{ + if (argc == 2 && strcmp(argv[1], "--version") == 0) { + printf("%s %s\n", PACKAGE, PACKAGE_VERSION); + return 0; + } + char *basename = strrchr(argv[0], '/'); + if (basename) { + debug("setting argv[0] to %s", basename + 1); + argv[0] = basename + 1; + } + if (argc > 1 && !strcmp(argv[0], "ubuntu-core-launcher")) { + debug("shifting arguments by one"); + argv[1] = argv[0]; + argv++; + argc--; + } + // XXX: replace with pull request 2416 in snapd + bool classic_confinement = false; + if (argc > 2 && strcmp(argv[1], "--classic") == 0) { + classic_confinement = true; + // Shift remaining arguments left + int i; + for (i = 1; i + 1 < argc; ++i) { + argv[i] = argv[i + 1]; + } + argv[i] = NULL; + argc -= 1; + } + const int NR_ARGS = 2; + if (argc < NR_ARGS + 1) + die("Usage: %s ", argv[0]); + + const char *security_tag = argv[1]; + debug("security tag is %s", security_tag); + const char *binary = argv[2]; + debug("binary to run is %s", binary); + uid_t real_uid = getuid(); + gid_t real_gid = getgid(); + + if (!verify_security_tag(security_tag)) + die("security tag %s not allowed", security_tag); + +#ifndef CAPS_OVER_SETUID + // this code always needs to run as root for the cgroup/udev setup, + // however for the tests we allow it to run as non-root + if (geteuid() != 0 && secure_getenv("SNAP_CONFINE_NO_ROOT") == NULL) { + die("need to run as root or suid"); + } +#endif + struct sc_apparmor apparmor; + sc_init_apparmor_support(&apparmor); +#ifdef HAVE_SECCOMP + scmp_filter_ctx seccomp_ctx + __attribute__ ((cleanup(sc_cleanup_seccomp_release))) = NULL; + seccomp_ctx = sc_prepare_seccomp_context(security_tag); +#endif // ifdef HAVE_SECCOMP + + if (geteuid() == 0) { + if (classic_confinement) { + /* 'classic confinement' is designed to run without the sandbox + * inside the shared namespace. Specifically: + * - snap-confine skips using the snap-specific mount namespace + * - snap-confine skips using device cgroups + * - snapd sets up a lenient AppArmor profile for snap-confine to use + * - snapd sets up a lenient seccomp profile for snap-confine to use + */ + debug + ("skipping sandbox setup, classic confinement in use"); + } else { + const char *group_name = getenv("SNAP_NAME"); + if (group_name == NULL) { + die("SNAP_NAME is not set"); + } + sc_initialize_ns_groups(); + struct sc_ns_group *group = NULL; + group = sc_open_ns_group(group_name, 0); + sc_lock_ns_mutex(group); + sc_create_or_join_ns_group(group, &apparmor); + if (sc_should_populate_ns_group(group)) { + sc_populate_mount_ns(security_tag); + sc_preserve_populated_ns_group(group); + } + sc_unlock_ns_mutex(group); + sc_close_ns_group(group); + // Reset path as we cannot rely on the path from the host OS to + // make sense. The classic distribution may use any PATH that makes + // sense but we cannot assume it makes sense for the core snap + // layout. Note that the /usr/local directories are explicitly + // left out as they are not part of the core snap. + debug + ("resetting PATH to values in sync with core snap"); + setenv("PATH", + "/usr/sbin:/usr/bin:/sbin:/bin:/usr/games", 1); + struct snappy_udev udev_s; + if (snappy_udev_init(security_tag, &udev_s) == 0) + setup_devices_cgroup(security_tag, &udev_s); + snappy_udev_cleanup(&udev_s); + } + // The rest does not so temporarily drop privs back to calling + // user (we'll permanently drop after loading seccomp) + if (setegid(real_gid) != 0) + die("setegid failed"); + if (seteuid(real_uid) != 0) + die("seteuid failed"); + + if (real_gid != 0 && geteuid() == 0) + die("dropping privs did not work"); + if (real_uid != 0 && getegid() == 0) + die("dropping privs did not work"); + } + // Ensure that the user data path exists. + setup_user_data(); +#if 0 + setup_user_xdg_runtime_dir(); +#endif + + // https://wiki.ubuntu.com/SecurityTeam/Specifications/SnappyConfinement + sc_maybe_aa_change_onexec(&apparmor, security_tag); +#ifdef HAVE_SECCOMP + sc_load_seccomp_context(seccomp_ctx); +#endif // ifdef HAVE_SECCOMP + + // Permanently drop if not root + if (geteuid() == 0) { + // Note that we do not call setgroups() here because its ok + // that the user keeps the groups he already belongs to + if (setgid(real_gid) != 0) + die("setgid failed"); + if (setuid(real_uid) != 0) + die("setuid failed"); + + if (real_gid != 0 && (getuid() == 0 || geteuid() == 0)) + die("permanently dropping privs did not work"); + if (real_uid != 0 && (getgid() == 0 || getegid() == 0)) + die("permanently dropping privs did not work"); + } + // and exec the new binary + execv(binary, (char *const *)&argv[NR_ARGS]); + perror("execv failed"); + return 1; +} diff --git a/cmd/snap-confine/snap-discard-ns.c b/cmd/snap-confine/snap-discard-ns.c new file mode 100644 index 00000000..269cebc2 --- /dev/null +++ b/cmd/snap-confine/snap-discard-ns.c @@ -0,0 +1,35 @@ +/* + * Copyright (C) 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 . + * + */ + +#include "utils.h" +#include "ns-support.h" + +int main(int argc, char **argv) +{ + if (argc != 2) + die("Usage: %s snap-name", argv[0]); + const char *snap_name = argv[1]; + struct sc_ns_group *group = + sc_open_ns_group(snap_name, SC_NS_FAIL_GRACEFULLY); + if (group != NULL) { + sc_lock_ns_mutex(group); + sc_discard_preserved_ns_group(group); + sc_unlock_ns_mutex(group); + sc_close_ns_group(group); + } + return 0; +} diff --git a/cmd/snap-confine/snap.c b/cmd/snap-confine/snap.c new file mode 100644 index 00000000..61c16e39 --- /dev/null +++ b/cmd/snap-confine/snap.c @@ -0,0 +1,45 @@ +/* + * Copyright (C) 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 . + * + */ +#include "config.h" +#include "snap.h" + +#include +#include +#include + +#include "utils.h" + +bool verify_security_tag(const char *security_tag) +{ + // The executable name is of form: + // snap..(|hook.) + // - must start with lowercase letter, then may contain + // lowercase alphanumerics and '-' + // - may contain alphanumerics and '-' + // - . + * + */ + +#ifndef SNAP_CONFINE_SNAP_H +#define SNAP_CONFINE_SNAP_H + +#include + +bool verify_security_tag(const char *security_tag); + +#endif diff --git a/cmd/snap-confine/snappy-app-dev b/cmd/snap-confine/snappy-app-dev new file mode 100755 index 00000000..94598d3a --- /dev/null +++ b/cmd/snap-confine/snappy-app-dev @@ -0,0 +1,39 @@ +#!/bin/sh +# udev callout (should live in /lib/udev) to allow a snap to access a device node +set -e +# debugging +#exec >>/tmp/snappy-app-dev.log +#exec 2>&1 +#set -x +# end debugging + +ACTION="$1" +APPNAME="$2" +DEVPATH="$3" +MAJMIN="$4" +[ -n "$APPNAME" ] || { echo "no app name given" >&2; exit 1; } +[ -n "$DEVPATH" ] || { echo "no devpath given" >&2; exit 1; } +[ -n "$MAJMIN" ] || { echo "no major/minor given" >&2; exit 0; } + +APPNAME=`echo $APPNAME | tr '_' '.'` +app_dev_cgroup="/sys/fs/cgroup/devices/$APPNAME" + +# check if it's a block or char dev +if [ "${DEVPATH#*/block/}" != "$DEVPATH" ]; then + type="b" +else + type="c" +fi + +acl="$type $MAJMIN rwm" +case "$ACTION" in + add|change) + echo "$acl" > "$app_dev_cgroup/devices.allow" + ;; + remove) + echo "$acl" > "$app_dev_cgroup/devices.deny" + ;; + *) + echo "ERROR: unknown action $ACTION" >&2 + exit 1 ;; +esac diff --git a/cmd/snap-confine/spread-tests/data/apt-keys/README.md b/cmd/snap-confine/spread-tests/data/apt-keys/README.md new file mode 100644 index 00000000..e496ac7c --- /dev/null +++ b/cmd/snap-confine/spread-tests/data/apt-keys/README.md @@ -0,0 +1,4 @@ +This directory contains keys used by the sbuild program to sign the temporary +archive. Those keys are kept in the tree as ephemeral test virtual machines do +not have sufficient entropy to generate keys by themselves in reasonable amount +of time. diff --git a/cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.pub b/cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.pub new file mode 100644 index 00000000..34d8b576 Binary files /dev/null and b/cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.pub differ diff --git a/cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.sec b/cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.sec new file mode 100644 index 00000000..cdadd277 Binary files /dev/null and b/cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.sec differ diff --git a/cmd/snap-confine/spread-tests/distros/debian. b/cmd/snap-confine/spread-tests/distros/debian. new file mode 100644 index 00000000..4a5a9eb5 --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/debian. @@ -0,0 +1,2 @@ +distro_codename=sid +distro_packaging_git_branch=debian diff --git a/cmd/snap-confine/spread-tests/distros/debian.common b/cmd/snap-confine/spread-tests/distros/debian.common new file mode 100644 index 00000000..b6084467 --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/debian.common @@ -0,0 +1,12 @@ +if [ -n "${APT_PROXY:-}" ]; then + distro_archive=${APT_PROXY}/ftp.debian.org/debian +else + distro_archive=http://ftp.debian.org/debian +fi +# NOTE: Debian packaging needs to be updated. I sent a mail to the +# debian maintainer with instructions on what needs to happen and +# how it fits into the CI system. +# +# For now all builds on debian will fail as they still contains +# debian/patches that are now applied upstream. +distro_packaging_git=git://anonscm.debian.org/collab-maint/snap-confine.git diff --git a/cmd/snap-confine/spread-tests/distros/ubuntu.14.04 b/cmd/snap-confine/spread-tests/distros/ubuntu.14.04 new file mode 100644 index 00000000..8471d577 --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/ubuntu.14.04 @@ -0,0 +1,2 @@ +distro_codename=trusty +distro_packaging_git_branch=14.04 diff --git a/cmd/snap-confine/spread-tests/distros/ubuntu.16.04 b/cmd/snap-confine/spread-tests/distros/ubuntu.16.04 new file mode 100644 index 00000000..4e89a355 --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/ubuntu.16.04 @@ -0,0 +1,2 @@ +distro_codename=xenial +distro_packaging_git_branch=16.04 diff --git a/cmd/snap-confine/spread-tests/distros/ubuntu.16.10 b/cmd/snap-confine/spread-tests/distros/ubuntu.16.10 new file mode 100644 index 00000000..374ea2e6 --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/ubuntu.16.10 @@ -0,0 +1,2 @@ +distro_codename=yakkety +distro_packaging_git_branch=16.10 diff --git a/cmd/snap-confine/spread-tests/distros/ubuntu.common b/cmd/snap-confine/spread-tests/distros/ubuntu.common new file mode 100644 index 00000000..4cb0db9d --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/ubuntu.common @@ -0,0 +1,7 @@ +if [ -n "${APT_PROXY:-}" ]; then + distro_archive=${APT_PROXY}/archive.ubuntu.com/ubuntu +else + distro_archive=http://archive.ubuntu.com/ubuntu +fi +distro_packaging_git=https://git.launchpad.net/snap-confine +sbuild_createchroot_extra="--components=main,universe" diff --git a/cmd/snap-confine/spread-tests/main/cgroup-used/task.yaml b/cmd/snap-confine/spread-tests/main/cgroup-used/task.yaml new file mode 100644 index 00000000..4c0c9272 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/cgroup-used/task.yaml @@ -0,0 +1,37 @@ +summary: Check that launcher cgroup functionality works +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Install snapd-hacker-toolbelt" + snap install snapd-hacker-toolbelt +execute: | + cd / + echo "Clear udev tags and cgroups with non-test device and running snapd-hacker-toolbelt.busybox" + echo 'KERNEL=="uinput", TAG+="snap_snapd-hacker-toolbelt_busybox"' > /etc/udev/rules.d/70-spread-test.rules + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + snapd-hacker-toolbelt.busybox echo "Hello World" | grep Hello + echo "Verify no tags for snapd-hacker-toolbelt.busybox for kmsg" + if udevadm info /sys/devices/virtual/mem/kmsg | grep snap_snapd-hacker-toolbelt_busybox ; then exit 1; fi + echo "Manually add udev tags for snapd-hacker-toolbelt.busybox for kmsg" + echo 'KERNEL=="kmsg", TAG+="snap_snapd-hacker-toolbelt_busybox"' > /etc/udev/rules.d/70-spread-test.rules + echo "Simulate snapd udev triggers" + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + echo "Verify udev has tag for kmsg" + if ! udevadm info /sys/devices/virtual/mem/kmsg | grep snap_snapd-hacker-toolbelt_busybox; then exit 1; fi + echo "Run snapd-hacker-toolbelt.busybox echo and see if kmsg added to cgroup" + snapd-hacker-toolbelt.busybox echo "Hello World" | grep Hello + if ! grep 'c 1:11 rwm' /sys/fs/cgroup/devices/snap.snapd-hacker-toolbelt.busybox/devices.list ; then exit 1; fi +restore: | + snap remove snapd-hacker-toolbelt + rm -f /etc/udev/rules.d/70-spread-test.rules + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + # no way to clear cgroup for snapd-hacker-toolbelt atm diff --git a/cmd/snap-confine/spread-tests/main/core-is-preferred/task.yaml b/cmd/snap-confine/spread-tests/main/core-is-preferred/task.yaml new file mode 100644 index 00000000..c9448ba1 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/core-is-preferred/task.yaml @@ -0,0 +1,10 @@ +summary: The snap named 'core' is preferred to the snap 'ubuntu-core' +prepare: | + snap install --devmode snapd-hacker-toolbelt + snap install core +execute: | + snapd-hacker-toolbelt.busybox cat /meta/snap.yaml | grep -q -F 'name: core' +restore: | + snap remove snapd-hacker-toolbelt + # XXX: the core snap cannot be removed, we should use a trick to remove it + # in some other way but this can wait. diff --git a/cmd/snap-confine/spread-tests/main/debug-flags/task.yaml b/cmd/snap-confine/spread-tests/main/debug-flags/task.yaml new file mode 100644 index 00000000..a95b86be --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/debug-flags/task.yaml @@ -0,0 +1,12 @@ +summary: snap-confine honors SNAP_CONFINE_DEBUG environment variable +execute: | + for value in yes no 0 1 unicorn; do + SNAP_CONFINE_DEBUG=$value ubuntu-core-launcher blah 2>debug.$value || : + done + grep -F -q 'DEBUG: shifting arguments by one' debug.yes + grep -F -q 'DEBUG: shifting arguments by one' debug.1 + grep -F -v -q 'DEBUG: shifting arguments by one' debug.no + grep -F -v -q 'DEBUG: shifting arguments by one' debug.0 + grep -F -q 'WARNING: unrecognized value of environment variable SNAP_CONFINE_DEBUG (expected yes/no or 1/0)' debug.unicorn +restore: | + rm -f debug.* diff --git a/cmd/snap-confine/spread-tests/main/discard-inexisting-ns/task.yaml b/cmd/snap-confine/spread-tests/main/discard-inexisting-ns/task.yaml new file mode 100644 index 00000000..f55d3722 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/discard-inexisting-ns/task.yaml @@ -0,0 +1,21 @@ +summary: Check that snap-discard-ns gracefully handles errors +details: | + The internal snap-discard-ns program is supposed to simply unmount + whatever is mounted at /run/snapd/ns/$SNAP_NAME.mnt. In case of + some specific failures though, we don't expect it to fail. +prepare: | + umount /run/snapd/ns || true + rm -rf /run/snapd/ns +execute: | + echo "We can try to discard a namespace before *any* snap runs" + /usr/lib/snapd/snap-discard-ns foo + echo "We can try to discard a namespace before the .mnt file exits" + mkdir -p /run/snapd/ns/ + /usr/lib/snapd/snap-discard-ns foo + echo "We can try to discard a namespace before the .mnt file is mounted" + touch /run/snapd/ns/foo.mnt + /usr/lib/snapd/snap-discard-ns foo +restore: | + rm /run/snapd/ns/foo.mnt + rm /run/snapd/ns/foo.lock + rmdir /run/snapd/ns diff --git a/cmd/snap-confine/spread-tests/main/discard-ns/task.yaml b/cmd/snap-confine/spread-tests/main/discard-ns/task.yaml new file mode 100644 index 00000000..dcad2741 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/discard-ns/task.yaml @@ -0,0 +1,23 @@ +summary: Check that snap-discard-ns works +# This is blacklisted on debian because debian doesn't use apparmor yet +systems: [-debian-8] +details: | + The internal snap-discard-ns program is supposed to simply unmount + whatever is mounted at /run/snapd/ns/$SNAP_NAME.mnt +prepare: | + mkdir -p /run/snapd/ns/ + mount --bind /run/snapd/ns /run/snapd/ns + mount --make-private /run/snapd/ns + touch /run/snapd/ns/foo.mnt + unshare --mount=/run/snapd/ns/foo.mnt true +execute: | + /usr/lib/snapd/snap-discard-ns foo + ! grep foo.mnt /proc/self/mountinfo +restore: | + umount /run/snapd/ns/foo.mnt || : + umount /run/snapd/ns + rm /run/snapd/ns/foo.mnt + rm /run/snapd/ns/foo.lock + # The removal is optional as the directory may contain other files + # that we don't want to touch here. + rmdir /run/snapd/ns || true diff --git a/cmd/snap-confine/spread-tests/main/hostfs-created-on-demand/task.yaml b/cmd/snap-confine/spread-tests/main/hostfs-created-on-demand/task.yaml new file mode 100644 index 00000000..8049b5af --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/hostfs-created-on-demand/task.yaml @@ -0,0 +1,24 @@ +summary: Check that /var/lib/snapd/hostfs is created on demand +# This is blacklisted on debian because debian doesn't use apparmor yet +systems: [-debian-8] +details: | + The /var/lib/snapd/hostfs directory is created by snap-confine + if the host packaging of snapd doesn't already provide it. +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can move the packaged hostfs directory aside" + if [ -d /var/lib/snapd/hostfs ]; then + mv /var/lib/snapd/hostfs /var/lib/snapd/hostfs.orig + fi +execute: | + cd / + echo "We can now run a busybox true just to ensure it started correctly" + /snap/bin/snapd-hacker-toolbelt.busybox true + echo "We can now check that the directory was created on the system" + test -d /var/lib/snapd/hostfs +restore: | + snap remove snapd-hacker-toolbelt + if [ -d /var/lib/snapd/hostfs.orig ]; then + mv /var/lib/snapd/hostfs.orig /var/lib/snapd/hostfs + fi diff --git a/cmd/snap-confine/spread-tests/main/media-sharing/task.yaml b/cmd/snap-confine/spread-tests/main/media-sharing/task.yaml new file mode 100644 index 00000000..f7b6f193 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/media-sharing/task.yaml @@ -0,0 +1,22 @@ +summary: The /media directory propagates events outwards +details: | + The /media directory is special in that mount events propagate outward from + the mount namespace used by snap applications into the main mount + namespace. +prepare: | + mkdir -p /media/src + mkdir -p /media/dst + touch /media/src/canary + snap install --devmode snapd-hacker-toolbelt +execute: | + export PATH=$PATH:/snap/bin + test ! -e /media/dst/canary + snapd-hacker-toolbelt.busybox sh -c 'mount --bind /media/src /media/dst' + test -e /media/dst/canary +restore: | + snap remove snapd-hacker-toolbelt + # If this doesn't work maybe it is because the test didn't execute correctly + umount /media/dst || true + rm -f /media/src/canary + rmdir /media/src + rmdir /media/dst diff --git a/cmd/snap-confine/spread-tests/main/media-visible-in-devmode/task.yaml b/cmd/snap-confine/spread-tests/main/media-visible-in-devmode/task.yaml new file mode 100644 index 00000000..6f4a0868 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/media-visible-in-devmode/task.yaml @@ -0,0 +1,15 @@ +summary: Check that /media is available to snaps installed in --devmode +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap in devmode" + snap install --devmode snapd-hacker-toolbelt + echo "Having added a canary file in /media" + echo "test" > /media/canary +execute: | + cd / + echo "We can see the canary file in /media" + [ "$(snapd-hacker-toolbelt.busybox cat /media/canary)" = "test" ] +restore: | + snap remove snapd-hacker-toolbelt + rm -f /media/canary diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.json new file mode 100644 index 00000000..1ac5d316 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.json @@ -0,0 +1,1028 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/var/lib/snapd/hostfs/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.amd64.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.amd64.json new file mode 100644 index 00000000..9d66da7f --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.amd64.json @@ -0,0 +1,798 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.i386.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.i386.json new file mode 100644 index 00000000..f9d417b3 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.i386.json @@ -0,0 +1,808 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "fuse.lxcfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/lxcfs", + "mount_src": "lxcfs", + "opt_fields": [ + "master:renumbered/32" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.amd64.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.amd64.json new file mode 100644 index 00000000..67a7c2ba --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.amd64.json @@ -0,0 +1,808 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/lxd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/lxd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.i386.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.i386.json new file mode 100644 index 00000000..67a7c2ba --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.i386.json @@ -0,0 +1,808 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/lxd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/lxd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.json new file mode 100644 index 00000000..209433e0 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.json @@ -0,0 +1,1028 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/var/lib/snapd/hostfs/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.amd64.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.amd64.json new file mode 100644 index 00000000..aa6fb331 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.amd64.json @@ -0,0 +1,788 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.i386.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.i386.json new file mode 100644 index 00000000..a7ebcdf5 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.i386.json @@ -0,0 +1,798 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "fuse.lxcfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/lxcfs", + "mount_src": "lxcfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.amd64.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.amd64.json new file mode 100644 index 00000000..0eb9e335 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.amd64.json @@ -0,0 +1,798 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/lxd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/lxd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.i386.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.i386.json new file mode 100644 index 00000000..0eb9e335 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.i386.json @@ -0,0 +1,798 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/lxd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/lxd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.json new file mode 100644 index 00000000..2f7fdc1f --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.json @@ -0,0 +1,2050 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/boot/efi", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/boot/grub", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/EFI/ubuntu" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/apparmor.d/cache", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/7" + ], + "root_dir": "/system-data/etc/apparmor.d/cache" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/system-data/etc/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/dbus-1/system.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/system-data/etc/dbus-1/system.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/default/keyboard", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/system-data/etc/default/keyboard" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/etc/fstab", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/image.fstab" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/hosts", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/system-data/etc/hosts" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/machine-id", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/system-data/etc/machine-id" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/modprobe.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/system-data/etc/modprobe.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/modules-load.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/system-data/etc/modules-load.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/netplan", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/system-data/etc/netplan" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/network/if-up.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/system-data/etc/network/if-up.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/network/interfaces.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/system-data/etc/network/interfaces.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ppp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/system-data/etc/ppp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ssh", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/system-data/etc/ssh" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/sudoers.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/system-data/etc/sudoers.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/sysctl.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/system-data/etc/sysctl.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/network", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/system-data/etc/systemd/network" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/system-data/etc/systemd/system.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/user.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/system-data/etc/systemd/user.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/udev/rules.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/system-data/etc/udev/rules.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ufw", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/system-data/etc/ufw" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/system-data/etc/writable" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/32" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/33" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/34" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/35" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/media", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "shared:renumbered/36" + ], + "root_dir": "/media" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/mnt", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/37" + ], + "root_dir": "/" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/38" + ], + "root_dir": "/" + }, + { + "fs_type": "binfmt_misc", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "binfmt_misc", + "opt_fields": [ + "master:renumbered/40" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/39" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/41" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/42" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/run/cgmanager/fs", + "mount_src": "cgmfs", + "opt_fields": [ + "master:renumbered/43" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/44" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/45" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/46" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/50" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/51" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/52" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/53" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/54" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/55" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/56" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/57" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/58" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/59" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/60" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/61" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/62" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/63" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/64" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/65" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/66" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snap.1001_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp/snap.rootfs_XXXXXX", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/snap.rootfs_XXXXXX" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/lib/snapd/snap-confine", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data/zyga/snap-confine" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/cache/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/cache/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/console-conf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dbus" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dhcp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/extrausers" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/initramfs-tools" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/misc" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK3", + "opt_fields": [], + "root_dir": "/system-data/var/lib/snapd/hostfs" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/boot/efi", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/boot/grub", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/EFI/ubuntu" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/var/lib/snapd/hostfs/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/apparmor.d/cache", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/7" + ], + "root_dir": "/system-data/etc/apparmor.d/cache" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/system-data/etc/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/dbus-1/system.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/system-data/etc/dbus-1/system.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/default/keyboard", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/system-data/etc/default/keyboard" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/fstab", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/image.fstab" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/hosts", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/system-data/etc/hosts" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/machine-id", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/system-data/etc/machine-id" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/modprobe.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/system-data/etc/modprobe.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/modules-load.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/system-data/etc/modules-load.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/netplan", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/system-data/etc/netplan" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/network/if-up.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/system-data/etc/network/if-up.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/network/interfaces.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/system-data/etc/network/interfaces.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ppp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/system-data/etc/ppp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ssh", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/system-data/etc/ssh" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/sudoers.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/system-data/etc/sudoers.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/sysctl.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/system-data/etc/sysctl.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/network", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/system-data/etc/systemd/network" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/system-data/etc/systemd/system.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/user.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/system-data/etc/systemd/user.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/udev/rules.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/system-data/etc/udev/rules.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ufw", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/system-data/etc/ufw" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/system-data/etc/writable" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/home", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/32" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/33" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/34" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/35" + ], + "root_dir": "/modules" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/mnt", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/37" + ], + "root_dir": "/" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/38" + ], + "root_dir": "/" + }, + { + "fs_type": "binfmt_misc", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc", + "mount_src": "binfmt_misc", + "opt_fields": [ + "master:renumbered/40" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/39" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/root", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/41" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/42" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/cgmanager/fs", + "mount_src": "cgmfs", + "opt_fields": [ + "master:renumbered/43" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/44" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/45" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/46" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/50" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/51" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/52" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/53" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/54" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/55" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/56" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/57" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/58" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/59" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/60" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/61" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/62" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/63" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/64" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/65" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/66" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/tmp", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/tmp/snap.rootfs_XXXXXX", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/snap.rootfs_XXXXXX" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/usr/lib/snapd/snap-confine", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data/zyga/snap-confine" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/cache/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/cache/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/console-conf", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/console-conf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/dbus", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dbus" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/dhcp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dhcp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/extrausers", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/extrausers" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/initramfs-tools", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/initramfs-tools" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/logrotate", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/misc", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/misc" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/snapd", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/snapd" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/sudo", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/70" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/random-seed", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/random-seed" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/rfkill", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/rfkill" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/waagent", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/log", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/tmp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/70" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd/random-seed", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/random-seed" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd/rfkill", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/rfkill" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.linode.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.linode.json new file mode 100644 index 00000000..93d8d808 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.linode.json @@ -0,0 +1,1800 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/boot/efi", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/boot/grub", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/EFI/ubuntu" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/apparmor.d/cache", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/7" + ], + "root_dir": "/system-data/etc/apparmor.d/cache" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/system-data/etc/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/dbus-1/system.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/system-data/etc/dbus-1/system.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/default/keyboard", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/system-data/etc/default/keyboard" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/etc/fstab", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/image.fstab" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/hosts", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/system-data/etc/hosts" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/machine-id", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/system-data/etc/machine-id" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/modprobe.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/system-data/etc/modprobe.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/modules-load.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/system-data/etc/modules-load.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/netplan", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/system-data/etc/netplan" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/network/if-up.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/system-data/etc/network/if-up.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/network/interfaces.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/system-data/etc/network/interfaces.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ppp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/system-data/etc/ppp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ssh", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/system-data/etc/ssh" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/sudoers.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/system-data/etc/sudoers.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/sysctl.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/system-data/etc/sysctl.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/network", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/system-data/etc/systemd/network" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/system-data/etc/systemd/system.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/user.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/system-data/etc/systemd/user.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/udev/rules.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/system-data/etc/udev/rules.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ufw", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/system-data/etc/ufw" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/system-data/etc/writable" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/32" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/33" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/34" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/35" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/media", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "shared:renumbered/36" + ], + "root_dir": "/media" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/mnt", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/37" + ], + "root_dir": "/" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/38" + ], + "root_dir": "/" + }, + { + "fs_type": "binfmt_misc", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "binfmt_misc", + "opt_fields": [ + "master:renumbered/40" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/39" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/41" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/42" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/run/cgmanager/fs", + "mount_src": "cgmfs", + "opt_fields": [ + "master:renumbered/43" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/44" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/45" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/46" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/50" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/51" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/52" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/53" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/54" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/55" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/56" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/57" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/58" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/59" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/60" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/61" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/62" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/63" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/64" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/65" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/66" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snap.1001_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp/snap.rootfs_XXXXXX", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/snap.rootfs_XXXXXX" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/lib/snapd/snap-confine", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data/zyga/snap-confine" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/cache/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/cache/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/console-conf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dbus" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dhcp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/extrausers" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/initramfs-tools" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/misc" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK3", + "opt_fields": [], + "root_dir": "/system-data/var/lib/snapd/hostfs" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/boot/efi", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/boot/grub", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/EFI/ubuntu" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/apparmor.d/cache", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/7" + ], + "root_dir": "/system-data/etc/apparmor.d/cache" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/system-data/etc/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/dbus-1/system.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/system-data/etc/dbus-1/system.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/default/keyboard", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/system-data/etc/default/keyboard" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/fstab", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/image.fstab" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/hosts", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/system-data/etc/hosts" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/machine-id", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/system-data/etc/machine-id" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/modprobe.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/system-data/etc/modprobe.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/modules-load.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/system-data/etc/modules-load.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/netplan", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/system-data/etc/netplan" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/network/if-up.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/system-data/etc/network/if-up.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/network/interfaces.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/system-data/etc/network/interfaces.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ppp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/system-data/etc/ppp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ssh", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/system-data/etc/ssh" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/sudoers.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/system-data/etc/sudoers.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/sysctl.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/system-data/etc/sysctl.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/network", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/system-data/etc/systemd/network" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/system-data/etc/systemd/system.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/user.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/system-data/etc/systemd/user.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/udev/rules.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/system-data/etc/udev/rules.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ufw", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/system-data/etc/ufw" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/system-data/etc/writable" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/home", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/32" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/33" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/34" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/35" + ], + "root_dir": "/modules" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/mnt", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/37" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/root", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/41" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/42" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/cgmanager/fs", + "mount_src": "cgmfs", + "opt_fields": [ + "master:renumbered/43" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/44" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/45" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/46" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/tmp", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/tmp/snap.rootfs_XXXXXX", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/snap.rootfs_XXXXXX" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/usr/lib/snapd/snap-confine", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data/zyga/snap-confine" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/cache/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/cache/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/console-conf", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/console-conf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/dbus", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dbus" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/dhcp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dhcp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/extrausers", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/extrausers" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/initramfs-tools", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/initramfs-tools" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/logrotate", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/misc", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/misc" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/snapd", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/snapd" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/sudo", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/70" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/random-seed", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/random-seed" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/rfkill", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/rfkill" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/waagent", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/log", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/tmp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/70" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd/random-seed", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/random-seed" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd/rfkill", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/rfkill" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/process.py b/cmd/snap-confine/spread-tests/main/mount-ns-layout/process.py new file mode 100755 index 00000000..1e77afc9 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/process.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +import sys +import json +import re + +class mountinfo_entry: + + def __init__(self, fs_type, mount_id, mount_opts, mount_point, mount_src, opt_fields, root_dir): + self.fs_type = fs_type + self.mount_id = mount_id + self.mount_opts = mount_opts + self.mount_point = mount_point + self.mount_src = mount_src + self.opt_fields = opt_fields + self.root_dir = root_dir + + @classmethod + def parse(cls, line): + parts = line.split() + fs_type = parts[-3] + mount_id = parts[0] + mount_opts = parts[5] + mount_point = parts[4] + mount_src = parts[-2] + root_dir = parts[3] + opt_fields = [] + i = 6 + while parts[i] != '-': + opt = parts[i] + opt_fields.append(opt) + i += 1 + opt_fields.sort() + return cls(fs_type, mount_id, mount_opts, mount_point, mount_src, + opt_fields, root_dir) + + def _fix_nondeterministic_mount_point(self): + self.mount_point = re.sub('_\w{6}', '_XXXXXX', self.mount_point) + self.mount_point = re.sub('/\d+$', '/NUMBER', self.mount_point) + + def _fix_nondeterministic_root_dir(self): + self.root_dir = re.sub('_\w{6}', '_XXXXXX', self.root_dir) + + def _fix_nondeterministic_mount_src(self): + self.mount_src = re.sub('/dev/[sv]da', '/dev/BLOCK', self.mount_src) + + def _fix_nondeterministic_opt_fields(self, seen): + fixed = [] + for opt in self.opt_fields: + if opt not in seen: + opt_id = len(seen) + seen[opt] = opt_id + else: + opt_id = seen[opt] + remapped_opt = re.sub(':\d+$', lambda m: ':renumbered/{}'.format(opt_id), opt) + fixed.append(remapped_opt) + self.opt_fields = fixed + + def _fix_nondeterministic_loop(self, seen): + if not self.mount_src.startswith("/dev/loop"): + return + if self.mount_src not in seen: + loop_id = len(seen) + seen[self.mount_src] = loop_id + else: + loop_id = seen[self.mount_src] + self.mount_src = re.sub('loop\d+$', lambda m: 'remapped-loop{}'.format(loop_id), self.mount_src) + + def as_json(self): + return { + "fs_type": self.fs_type, + "mount_opts": self.mount_opts, + "mount_point": self.mount_point, + "mount_src": self.mount_src, + "opt_fields": self.opt_fields, + "root_dir": self.root_dir, + } + + +def parse_mountinfo(lines): + return [mountinfo_entry.parse(line) for line in lines] + + +def fix_initial_nondeterminism(entries): + for entry in entries: + entry._fix_nondeterministic_mount_point() + + +def fix_remaining_nondeterminism(entries): + seen_opt_fields = {} + seen_loops = {} + for entry in entries: + entry._fix_nondeterministic_root_dir() + entry._fix_nondeterministic_mount_src() + entry._fix_nondeterministic_opt_fields(seen_opt_fields) + entry._fix_nondeterministic_loop(seen_loops) + + +def main(): + entries = parse_mountinfo(sys.stdin) + # Get rid of the core snap as it is not certain that we'll see one and we want determinism + entries = [entry for entry in entries if not re.match("/snap/core/\d+", entry.mount_point)] + # Fix random directories and non-deterministic revisions + fix_initial_nondeterminism(entries) + # Sort by just the mount point, + entries.sort(key=lambda entry: (entry.mount_point)) + # Fix remainder of the non-determinism + fix_remaining_nondeterminism(entries) + # Make entries nicely deterministic, by sorting them by mount location + entries.sort(key=lambda entry: (entry.mount_point, entry.mount_src, entry.root_dir)) + # Export everything + json.dump([entry.as_json() for entry in entries], + sys.stdout, sort_keys=True, indent=2, separators=(',', ': ')) + sys.stdout.write('\n') + + +if __name__ == '__main__': + main() diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/snap-arch.py b/cmd/snap-confine/spread-tests/main/mount-ns-layout/snap-arch.py new file mode 100755 index 00000000..0e0d8ad3 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/snap-arch.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +import os +import sys + +def main(): + kernel_arch = os.uname().machine + # Because off by one bugs and naming ... + snap_arch_map = { + 'aarch64': 'arm64', + 'armv7l': 'armhf', + 'x86_64': 'amd64', + 'i686': 'i386', + } + try: + print(snap_arch_map[kernel_arch]) + except KeyError: + print("unsupported kernel architecture: {!a}".format(kernel_arch), file=sys.stderr) + return 1 + + +if __name__ == '__main__': + main() diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/task.yaml b/cmd/snap-confine/spread-tests/main/mount-ns-layout/task.yaml new file mode 100644 index 00000000..859fe7c1 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/task.yaml @@ -0,0 +1,46 @@ +summary: Ensure that the mount namespace a given layout +details: | + This test analyzes /proc/self/mountinfo which contains a representation of + the mount table of the current process. The mount table is a very sensitive + part of the confinement design. This test measures the effective table, + normalizes it (to remove some inherent randomness of certain identifiers + and make it uniform regardless of particular names of block devices, snap + revisions, etc.) and then compares it to a canned copy. + + There are several reference tables, one for core (aka all-snap system) and + one for classic. At this time only classic systems are measured and tested. + The classic systems are further divided into those using the core snap and + those using the older ubuntu-core snap. Lastly, they are divided by + architectures to take account any architecture specific differences. +prepare: | + echo "Having installed a busybox" + snap install snapd-hacker-toolbelt +execute: | + echo "We can map the kernel architecture name to snap architecture name" + arch=$(./snap-arch.py) + echo "We can run busybox true so that snap-confine creates a mount namespace" + snapd-hacker-toolbelt.busybox true + echo "Using nsenter we can move to that namespace, inspect and normalize the mount table" + nsenter -m/run/snapd/ns/snapd-hacker-toolbelt.mnt \ + cat /proc/self/mountinfo | ./process.py > observed.json + echo "We can now compare the obtained mount table to expected values" + if [ -e /snap/core/current ]; then + cmp observed.json expected.classic.core.$SPREAD_BACKEND.$arch.json + else + cmp observed.json expected.classic.ubuntu-core.$SPREAD_BACKEND.$arch.json + fi +debug: | + echo "When something goes wrong we can display a human-readable diff" + arch=$(./snap-arch.py) + if [ -e /snap/core/current ]; then + diff -u observed.json expected.classic.core.$SPREAD_BACKEND.$arch.json || : + else + diff -u observed.json expected.classic.ubuntu-core.$SPREAD_BACKEND.$arch.json || : + fi + echo "And pastebin the raw table for analysis" + apt-get install pastebinit + nsenter -m/run/snapd/ns/snapd-hacker-toolbelt.mnt \ + cat /proc/self/mountinfo | pastebinit +restore: | + snap remove snapd-hacker-toolbelt + rm -f observed.json diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-sharing/task.yaml b/cmd/snap-confine/spread-tests/main/mount-ns-sharing/task.yaml new file mode 100644 index 00000000..402e9ed9 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-sharing/task.yaml @@ -0,0 +1,20 @@ +summary: mount namespace is shared among processes +details: | + The mount namespace is automatically shared amongst processes belonging to + a given snap. The namespace is preserved until the machine reboots or until + it is discarded with snap-discard-ns. +prepare: | + # NOTE: devmode is required because otherwise we cannot read /proc/self/ns/mnt + snap install --devmode snapd-hacker-toolbelt +execute: | + export PATH=/snap/bin:$PATH + echo "The mount namespace inside a snap is different" + outer_mnt_ns=$(readlink /proc/self/ns/mnt) + inner_mnt_ns=$(snapd-hacker-toolbelt.busybox readlink /proc/self/ns/mnt) + [ "$outer_mnt_ns" != "$inner_mnt_ns" ] + echo "The mount namespace is stable across invocations" + for i in $(seq 100); do + [ "$inner_mnt_ns" = "$(snapd-hacker-toolbelt.busybox readlink /proc/self/ns/mnt)" ] + done +restore: | + snap remove snapd-hacker-toolbelt diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-destination/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-destination/task.yaml new file mode 100644 index 00000000..f0dc28a9 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-destination/task.yaml @@ -0,0 +1,26 @@ +summary: Apparmor profile prevents bind-mounting to /snap/bin +# This is blacklisted on debian because it relies on apparmor mount mediation +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can change its mount profile externally to create bind mount /snap/bin somewhere" + echo "/snap/snapd-hacker-toolbelt/mnt -> /snap/bin" + mkdir -p /var/lib/snapd/mount + echo "/snap/snapd-hacker-toolbelt/current/mnt /snap/bin none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab +execute: | + cd / + echo "Let's clear the kernel ring buffer" + dmesg -c + echo "We can now run busybox true and expect it to fail" + orig_ratelimit=$(sysctl -n kernel.printk_ratelimit) + sysctl -w kernel.printk_ratelimit=0 + ! /snap/bin/snapd-hacker-toolbelt.busybox true + sysctl -w kernel.printk_ratelimit=$orig_ratelimit + echo "Not only the command failed because snap-confine failed, we see why!" + dmesg | grep 'apparmor="DENIED" operation="mount" info="failed srcname match" error=-13 profile="/usr/lib/snapd/snap-confine" name="/snap/bin/" pid=[0-9]\+ comm="ubuntu-core-lau" srcname="/snap/snapd-hacker-toolbelt/[0-9]\+/mnt/" flags="rw, bind"' +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab + dmesg -c diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-source/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-source/task.yaml new file mode 100644 index 00000000..0b4dc530 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-source/task.yaml @@ -0,0 +1,26 @@ +summary: Apparmor profile prevents bind-mounting from /snap/bin +# This is blacklisted on debian because it relies on apparmor mount mediation +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can change its mount profile externally to create bind mount /snap/bin somewhere" + echo "/snap/bin -> /snap/snapd-hacker-toolbelt/mnt" + mkdir -p /var/lib/snapd/mount + echo "/snap/bin /snap/snapd-hacker-toolbelt/current/mnt none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab +execute: | + cd / + echo "Let's clear the kernel ring buffer" + dmesg -c + echo "We can now run busybox true and expect it to fail" + orig_ratelimit=$(sysctl -n kernel.printk_ratelimit) + sysctl -w kernel.printk_ratelimit=0 + ! /snap/bin/snapd-hacker-toolbelt.busybox true + sysctl -w kernel.printk_ratelimit=$orig_ratelimit + echo "Not only the command failed because snap-confine failed, we see why!" + dmesg | grep 'apparmor="DENIED" operation="mount" info="failed srcname match" error=-13 profile="/usr/lib/snapd/snap-confine" name="/snap/snapd-hacker-toolbelt/[0-9]\+/mnt/" pid=[0-9]\+ comm="ubuntu-core-lau" srcname="/snap/bin/" flags="rw, bind"' +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab + dmesg -c diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-missing-dst/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-missing-dst/task.yaml new file mode 100644 index 00000000..5ecc9fac --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-missing-dst/task.yaml @@ -0,0 +1,19 @@ +summary: Check that missing destination directory aborts mount processing +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can change its mount profile externally to create a read-only bind-mount" + echo "/var/snap/snapd-hacker-toolbelt/common/src -> /var/snap/snapd-hacker-toolbelt/common/dst" + mkdir -p /var/lib/snapd/mount + echo "/var/snap/snapd-hacker-toolbelt/common/src /var/snap/snapd-hacker-toolbelt/common/dst none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab + echo "We can now create the source directory, missing the destination directory" + mkdir -p /var/snap/snapd-hacker-toolbelt/common/src +execute: | + echo "We can now run busybox.true and expect it to fail" + ( cd / && ! /snap/bin/snapd-hacker-toolbelt.busybox true ) +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-missing-src/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-missing-src/task.yaml new file mode 100644 index 00000000..70dbf535 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-missing-src/task.yaml @@ -0,0 +1,19 @@ +summary: Check that missing source directory aborts mount processing +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can change its mount profile externally to create a read-only bind-mount" + echo "/var/snap/snapd-hacker-toolbelt/common/src -> /var/snap/snapd-hacker-toolbelt/common/dst" + mkdir -p /var/lib/snapd/mount + echo "/var/snap/snapd-hacker-toolbelt/common/src /var/snap/snapd-hacker-toolbelt/common/dst none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab + echo "We can now create the destination directory, missing the source directory" + mkdir -p /var/snap/snapd-hacker-toolbelt/common/dst +execute: | + echo "We can now run busybox.true and expect it to fail" + ( cd / && ! /snap/bin/snapd-hacker-toolbelt.busybox true ) +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-mount-tmpfs/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-mount-tmpfs/task.yaml new file mode 100644 index 00000000..8226b412 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-mount-tmpfs/task.yaml @@ -0,0 +1,20 @@ +summary: Check that mount profiles cannot be used to mount tmpfs +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab +execute: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap list | grep -q snapd-hacker-toolbelt || snap install snapd-hacker-toolbelt + + echo "We can change its mount profile externally to mount tmpfs at /var/snap/snapd-hacker-toolbelt/mnt" + mkdir -p /var/lib/snapd/mount + echo "none /var/snap/snapd-hacker-toolbelt/common/mnt tmpfs rw 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab + + echo "We can now create the test mount directory" + mkdir -p /var/snap/snapd-hacker-toolbelt/common/mnt + + echo "We can now run busybox.true and expect it to fail" + ( cd / && ! /snap/bin/snapd-hacker-toolbelt.busybox true ) diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-ro-mount/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-ro-mount/task.yaml new file mode 100644 index 00000000..2ae72e6a --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-ro-mount/task.yaml @@ -0,0 +1,18 @@ +summary: Check that read-only bind mounts can be created +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can change its mount profile externally to create a read-only bind-mount" + echo "/snap/snapd-hacker-toolbelt/current/src -> /snap/snapd-hacker-toolbelt/current/dst" + mkdir -p /var/lib/snapd/mount + echo "/snap/snapd-hacker-toolbelt/current/src /snap/snapd-hacker-toolbelt/current/dst none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab +execute: | + cd / + echo "We can now look at the .id file in the destination directory" + [ "$(/snap/bin/snapd-hacker-toolbelt.busybox cat /snap/snapd-hacker-toolbelt/current/dst/.id)" = "source" ] +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-rw-mount/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-rw-mount/task.yaml new file mode 100644 index 00000000..b2e23ece --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-rw-mount/task.yaml @@ -0,0 +1,24 @@ +summary: Check that read-write bind mounts can be created +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can connect it to the mount-observe slot from the core" + snap connect snapd-hacker-toolbelt:mount-observe ubuntu-core:mount-observe + echo "We can change its mount profile externally to create a read-only bind-mount" + echo "/snap/snapd-hacker-toolbelt/current/src -> /snap/snapd-hacker-toolbelt/current/dst" + mkdir -p /var/lib/snapd/mount + echo "/snap/snapd-hacker-toolbelt/current/src /snap/snapd-hacker-toolbelt/current/dst none bind,rw 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab +execute: | + cd / + echo "We can now look at the .id file in the destination directory" + [ "$(/snap/bin/snapd-hacker-toolbelt.busybox cat /snap/snapd-hacker-toolbelt/current/dst/.id)" = "source" ] + echo "As well as the current mount points" + # FIXME: this doesn't show 'rw', bind mounts confuse most tools and it + # seems that busybox is not any different here. + /snap/bin/snapd-hacker-toolbelt.busybox mount | grep snapd-hacker-toolbelt +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab diff --git a/cmd/snap-confine/spread-tests/main/mount-usr-src/task.yaml b/cmd/snap-confine/spread-tests/main/mount-usr-src/task.yaml new file mode 100644 index 00000000..98a7b4fd --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-usr-src/task.yaml @@ -0,0 +1,17 @@ +summary: Check for https://bugs.launchpad.net/snap-confine/+bug/1597842 +# This is blacklisted on debian because debian doesn't use apparmor yet +systems: [-debian-8] +details: | + The snappy execution environment should contain the /usr/src directory + from the host filesystem when running on a classic distribution. +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "and having connected the mount-observe interface" + snap connect snapd-hacker-toolbelt:mount-observe ubuntu-core:mount-observe +execute: | + cd / + echo "We can ensure that /usr/src is mounted" + /snap/bin/snapd-hacker-toolbelt.busybox cat /proc/self/mounts | grep ' /usr/src ' +restore: | + snap remove snapd-hacker-toolbelt diff --git a/cmd/snap-confine/spread-tests/main/test-seccomp-compat/task.yaml b/cmd/snap-confine/spread-tests/main/test-seccomp-compat/task.yaml new file mode 100644 index 00000000..5a5c189e --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/test-seccomp-compat/task.yaml @@ -0,0 +1,16 @@ +summary: Check that basic install works +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +# +# This test only makes sense on x86_64 as it can execute i386 code in addition +# to native x86_64 code). +systems: [-debian-8, -ubuntu-16.04-32] +prepare: | + snap install --edge test-seccomp-compat +execute: | + cd / + echo Run the 64 bit binary + test-seccomp-compat.true64 + echo Run the 32 bit binary + test-seccomp-compat.true32 +restore: | + snap remove test-seccomp-compat diff --git a/cmd/snap-confine/spread-tests/main/test-snap-runs/task.yaml b/cmd/snap-confine/spread-tests/main/test-snap-runs/task.yaml new file mode 100644 index 00000000..857c863c --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/test-snap-runs/task.yaml @@ -0,0 +1,15 @@ +summary: Check that basic install works +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + snap install snapd-hacker-toolbelt +execute: | + cd / + echo Run some hello-world stuff + snapd-hacker-toolbelt.busybox echo "Hello World" | grep Hello + snapd-hacker-toolbelt.busybox env | grep SNAP_NAME=snapd-hacker-toolbelt + echo Ensure that we get an error if we try to abuse the sandbox + if snapd-hacker-toolbelt.busybox touch /var/tmp/evil; then exit 1; fi + dmesg -c +restore: | + snap remove snapd-hacker-toolbelt diff --git a/cmd/snap-confine/spread-tests/main/ubuntu-core-launcher-exists/task.yaml b/cmd/snap-confine/spread-tests/main/ubuntu-core-launcher-exists/task.yaml new file mode 100644 index 00000000..236b91de --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/ubuntu-core-launcher-exists/task.yaml @@ -0,0 +1,6 @@ +summary: Check that ubuntu-core-launcher executes correctly +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +execute: | + echo "ubuntu-core-launcher is installed and responds to --help" + ubuntu-core-launcher --help 2>&1 | grep -F -q 'Usage: ubuntu-core-launcher ' diff --git a/cmd/snap-confine/spread-tests/main/user-data-dir-created/task.yaml b/cmd/snap-confine/spread-tests/main/user-data-dir-created/task.yaml new file mode 100644 index 00000000..dc2b69aa --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/user-data-dir-created/task.yaml @@ -0,0 +1,23 @@ +summary: Ensure that SNAP_USER_DATA directory is created by snap-confine +# This is blacklisted on debian because debian doesn't use apparmor yet +systems: [-debian-8] +details: | + A regression was found in snap-confine where the new code path in snapd was + not active yet but the corresponding code path in snap-confine was already + removed. This resulted in the $SNAP_USER_DATA directory not being created + at runtime. + This test checks that it is actually created +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "Having removed the SNAP_USER_DATA directory" + rm -rf "$HOME/snap/snapd-hacker-toolbelt/" +execute: | + cd / + echo "We can now run snapd-hacker-toolbelt.busybox true" + /snap/bin/snapd-hacker-toolbelt.busybox true + echo "And see that the SNAP_USER_DATA directory was created" + test -d $HOME/snap/snapd-hacker-toolbelt +restore: | + snap remove snapd-hacker-toolbelt + rm -rf "$HOME/snap/snapd-hacker-toolbelt/" diff --git a/cmd/snap-confine/spread-tests/main/user-xdg-runtime-dir-created/task.yaml b/cmd/snap-confine/spread-tests/main/user-xdg-runtime-dir-created/task.yaml new file mode 100644 index 00000000..b15d7bdd --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/user-xdg-runtime-dir-created/task.yaml @@ -0,0 +1,21 @@ +summary: Ensure that XDG_RUNTIME_DIR directory is created by snap-confine +# This is blacklisted on debian because debian doesn't use apparmor yet +systems: [-debian-8] +details: | + This test checks that XDG_RUNTIME_DIR is actually created +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "Having removed the XDG_RUNTIME_DIR directory" + rm -rf "/run/user/`id -u`/snapd-hacker-toolbelt" +execute: | + cd / + echo "FIXME: export XDG_RUNTIME_DIR for now until snapd does it" + export XDG_RUNTIME_DIR="/run/user/`id -u`/snapd-hacker-toolbelt" + echo "We can now run snapd-hacker-toolbelt.busybox true" + /snap/bin/snapd-hacker-toolbelt.busybox true + echo "And see that the XDG_RUNTIME_DIR directory was created" + test -d /run/user/`id -u`/snapd-hacker-toolbelt +restore: | + snap remove snapd-hacker-toolbelt + rm -rf "/run/user/`id -u`/snapd-hacker-toolbelt" diff --git a/cmd/snap-confine/spread-tests/main/version-switch/task.yaml b/cmd/snap-confine/spread-tests/main/version-switch/task.yaml new file mode 100644 index 00000000..3efc6368 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/version-switch/task.yaml @@ -0,0 +1,3 @@ +summary: snap-confine supports the --version switch +prepare: | + [ "$(ubuntu-core-launcher --version)" = "snap-confine $(cat $SPREAD_PATH/VERSION)" ] diff --git a/cmd/snap-confine/spread-tests/regression/lp-1599608/task.yaml b/cmd/snap-confine/spread-tests/regression/lp-1599608/task.yaml new file mode 100644 index 00000000..52a7fd09 --- /dev/null +++ b/cmd/snap-confine/spread-tests/regression/lp-1599608/task.yaml @@ -0,0 +1,69 @@ +summary: Check that execle doesn't regress +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +details: | + The setup for this test is unorthodox because by the time the cgroup code is + executed, the mounts are in place and /lib/udev/snappy-app-dev from the core + snap is used. Unfortunately, simple bind mounts over + /snap/ubuntu-core/current/lib/udev don't work and the core snap must be + unpacked, lib/udev/snappy-app-dev modified to be tested, repacked and mounted. + We unmount the core snap and move it aside to avoid both the original and the + updated core snap from being mounted on the same mount point, which confuses + the kernel. +prepare: | + echo "This test is disabled because it causes failures for subsequent tests" + echo "it seems to unmount ubuntu-core snap and not re-mount the original one correctly" + exit 0 + cd / + echo "Install hello-world" + snap install hello-world + systemctl stop snapd.refresh.timer snapd.service snapd.socket + # all of this ls madness can go away when we have remote environment + # variables + echo "Unmount original core snap" + umount $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1) + mv $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1).orig + echo "Create modified core snap for snappy-app-dev" + unsquashfs $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1) + echo 'echo PATH=$PATH > /run/udev/spread-test.out' >> ./squashfs-root/lib/udev/snappy-app-dev + echo 'echo TESTVAR=$TESTVAR >> /run/udev/spread-test.out' >> ./squashfs-root/lib/udev/snappy-app-dev + mksquashfs ./squashfs-root $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1 | sed 's/.orig//') -comp xz + if [ ! -e $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) ]; then exit 1; fi + echo "Mount modified core snap" + mount $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1) + systemctl start snapd.refresh.timer snapd.service snapd.socket +execute: | + exit 0 + cd / + echo "Add a udev tag so affected code branch is exercised" + echo 'KERNEL=="uinput", TAG+="snap_hello-world_env"' > /etc/udev/rules.d/70-spread-test.rules + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + PATH=/foo:$PATH TESTVAR=bar hello-world.env | grep PATH + cat /run/udev/spread-test.out + echo "Ensure user-specified PATH is not used" + ! grep 'PATH=/foo' /run/udev/spread-test.out + echo "Ensure environment is clean" + ! grep 'TESTVAR=bar' /run/udev/spread-test.out +restore: | + exit 0 + echo "Remove hello-world" + snap remove hello-world + systemctl stop snapd.refresh.timer snapd.service snapd.socket + echo "Unmount the modified core snap" + # all of this ls madness can go away when we have remote environment + # variables + umount $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1) + if [ "x"$(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1) != "x" ]; then mv -f $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1) $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1 | sed 's/.orig//') ; fi + echo "Mount the original core snap" + mount $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1) + rm -rf /squashfs-root + rm -f /run/udev/spread-test.out + rm -f /etc/udev/rules.d/70-spread-test.rules + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + systemctl start snapd.refresh.timer snapd.service snapd.socket diff --git a/cmd/snap-confine/spread-tests/release.sh b/cmd/snap-confine/spread-tests/release.sh new file mode 100755 index 00000000..b1da0769 --- /dev/null +++ b/cmd/snap-confine/spread-tests/release.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# This script creates a new release tarball +set -xue + +# Sanity check, are we in the top-level directory of the tree? +test -f configure.ac || ( echo 'this script must be executed from the top-level of the tree' && exit 1) + +# Record where the top level directory is +top_dir=$(pwd) + +# Create source distribution tarball and place it in the top-level directory. +create_dist_tarball() { + # Load the version number from a dedicated file + local pkg_version= + pkg_version="$(cat "$top_dir/VERSION")" + + # Ensure that build system is up-to-date and ready + autoreconf -f -i + # XXX: This fixes somewhat odd error when configure below (in an empty directory) fails with: + # configure: error: source directory already configured; run "make distclean" there first + test -f Makefile && make distclean + + # Create a scratch space to run configure + scratch_dir="$(mktemp -d)" + trap 'rm -rf "$scratch_dir"' EXIT + + # Configure the project in a scratch directory + cd "$scratch_dir" + "$top_dir/configure" --prefix=/usr + + # Create the distribution tarball + make dist + + # Ensure we got the tarball we were expecting to see + test -f "snap-confine-$pkg_version.tar.gz" + + # Move it to the top-level directory + mv "snap-confine-$pkg_version.tar.gz" "$top_dir/" +} + +create_dist_tarball diff --git a/cmd/snap-confine/spread-tests/spread-prepare.sh b/cmd/snap-confine/spread-tests/spread-prepare.sh new file mode 100755 index 00000000..cc9a073e --- /dev/null +++ b/cmd/snap-confine/spread-tests/spread-prepare.sh @@ -0,0 +1,173 @@ +#!/bin/sh +# This script is started by spread to prepare the execution environment +set -xue + +# Sanity check, are we in the top-level directory of the tree? +test -f configure.ac || ( echo 'this script must be executed from the top-level of the tree' && exit 1) + +# Record where the top level directory is +top_dir=$(pwd) + +# Record the current distribution release data to know what to do +release_ID="$( . /etc/os-release && echo "${ID:-linux}" )" +release_VERSION_ID="$( . /etc/os-release && echo "${VERSION_ID:-}" )" + + +build_debian_or_ubuntu_package() { + local pkg_version + local distro_packaging_git_branch + local distro_packaging_git + local distro_archive + local distro_codename + local sbuild_createchroot_extra="" + pkg_version="$(cat "$top_dir/VERSION")" + + if [ ! -f "$top_dir/spread-tests/distros/$release_ID.$release_VERSION_ID" ] || \ + [ ! -f "$top_dir/spread-tests/distros/$release_ID.common" ]; then + echo "Distribution: $release_ID (release $release_VERSION_ID) is not supported" + echo "please read this script and create new files in spread-test/distros" + exit 1 + fi + + # source the distro specific vars + . "$top_dir/spread-tests/distros/$release_ID.$release_VERSION_ID" + . "$top_dir/spread-tests/distros/$release_ID.common" + + # sanity check, ensure that essential variables were defined + test -n "$distro_packaging_git_branch" + test -n "$distro_packaging_git" + test -n "$distro_archive" + test -n "$distro_codename" + + # Create a scratch space + scratch_dir="$(mktemp -d)" + trap 'rm -rf "$scratch_dir"' EXIT + + # Do everything in the scratch directory + cd "$scratch_dir" + + # Fetch the current Ubuntu packaging for the appropriate release + git clone -b "$distro_packaging_git_branch" "$distro_packaging_git" distro-packaging + + # Install all the build dependencies declared by the package. + apt-get install --quiet -y gdebi-core + apt-get install --quiet -y $(gdebi --quiet --apt-line ./distro-packaging/debian/control) + + # Generate a new upstream tarball from the current state of the tree + ( cd "$top_dir" && spread-tests/release.sh ) + + # Prepare the .orig tarball and unpackaged source tree + cp "$top_dir/snap-confine-$pkg_version.tar.gz" "snap-confine_$pkg_version.orig.tar.gz" + tar -zxf "snap-confine_$pkg_version.orig.tar.gz" + + # Apply the debian directory from downstream packaging to form a complete source package + mv "distro-packaging/debian" "snap-confine-$pkg_version/debian" + rm -rf distro-packaging + + # Add an automatically-generated changelog entry + # The --controlmaint takes the maintainer details from debian/control + ( cd "snap-confine-$pkg_version" && dch --controlmaint --newversion "${pkg_version}-1" "Automatic CI build") + + # Build an unsigned source package + ( cd "snap-confine-$pkg_version" && dpkg-buildpackage -uc -us -S ) + + # Copy source package files to the top-level directory (this helps for + # interactive debugging since the package is available right there) + cp ./*.dsc ./*.debian.tar.* ./*.orig.tar.gz "$top_dir/" + + # Ensure that we have a sbuild chroot ready + if ! schroot -l | grep "chroot:${distro_codename}-.*-sbuild"; then + sbuild-createchroot \ + --include=eatmydata \ + "--make-sbuild-tarball=/var/lib/sbuild/${distro_codename}-amd64.tar.gz" \ + "$sbuild_createchroot_extra" \ + "$distro_codename" "$(mktemp -d)" \ + "$distro_archive" + fi + + # Build a binary package in a clean chroot. + # NOTE: nocheck is because the package still includes old unit tests that + # are deeply integrated into how ubuntu apparmor denials are logged. This + # should be removed once those test are migrated to spread testes. + DEB_BUILD_OPTIONS=nocheck sbuild \ + --arch-all \ + --dist="$distro_codename" \ + --batch \ + "snap-confine_${pkg_version}-1.dsc" + + # Copy all binary packages to the top-level directory + cp ./*.deb "$top_dir/" +} + + +# Apply tweaks +case "$release_ID" in + ubuntu) + # apt update is hanging on security.ubuntu.com with IPv6. + sysctl -w net.ipv6.conf.all.disable_ipv6=1 + trap "sysctl -w net.ipv6.conf.all.disable_ipv6=0" EXIT + ;; +esac + +# Install all the build dependencies +case "$release_ID" in + ubuntu|debian) + # treat APT_PROXY as a location of apt-cacher-ng to use + if [ -n "${APT_PROXY:-}" ]; then + printf 'Acquire::http::Proxy "%s";\n' "$APT_PROXY" > /etc/apt/apt.conf.d/00proxy + fi + # cope with unexpected /etc/apt/apt.conf.d/95cloud-init-proxy that may be in the image + rm -f /etc/apt/apt.conf.d/95cloud-init-proxy || : + # trusty support is under development right now + # we special-case the release until we have officially landed + if [ "$release_ID" = "ubuntu" ] && [ "$release_VERSION_ID" = "14.04" ]; then + add-apt-repository ppa:thomas-voss/trusty + fi + apt-get update + apt-get dist-upgrade -y + if [ "$release_ID" = "ubuntu" ] && [ "$release_VERSION_ID" = "14.04" ]; then + apt-get install -y systemd + # starting systemd manually is working around + # systemd not running as PID 1 on trusty systems. + service systemd start + fi + # On Debian and derivatives we need the following things: + # - sbuild -- to build the binary package with extra hygiene + # - devscripts -- to modify the changelog automatically + # - git -- to clone native downstream packaging + apt-get install --quiet -y sbuild devscripts git + # XXX: Taken from https://wiki.debian.org/sbuild + mkdir -p /root/.gnupg + # NOTE: We cannot use sbuild-update --keygen as virtual machines lack + # the necessary entropy to generate keys before the spread timeout + # kicks in. Instead we just copy pre-made, insecure keys from the + # source repository. + mkdir -p /var/lib/sbuild/apt-keys/ + cp -a "$top_dir/spread-tests/data/apt-keys/"* /var/lib/sbuild/apt-keys/ + sbuild-adduser "$LOGNAME" + ;; + *) + echo "unsupported distribution: $release_ID" + echo "patch spread-prepare to teach it about how to install build dependencies" + exit 1 + ;; +esac + +# Build and install the native package using downstream packaging and the fresh upstream tarball +case "$release_ID" in + ubuntu|debian) + build_debian_or_ubuntu_package "$release_ID" "$release_VERSION_ID" + # Install the freshly-built packages + dpkg -i snap-confine_*.deb || apt-get -f install -y + dpkg -i ubuntu-core-launcher_*.deb || apt-get -f install -y + # Install snapd (testes require it) + apt-get install -y snapd + ;; + *) + echo "unsupported distribution: $release_ID" + exit 1 + ;; +esac + +# Install the core snap +snap list | grep -q ubuntu-core || snap install ubuntu-core diff --git a/cmd/snap-confine/tests/Makefile.am b/cmd/snap-confine/tests/Makefile.am new file mode 100644 index 00000000..9b481797 --- /dev/null +++ b/cmd/snap-confine/tests/Makefile.am @@ -0,0 +1,38 @@ +TESTS = + +all_tests = \ + test_bad_seccomp_filter_args \ + test_bad_seccomp_filter_args_clone \ + test_bad_seccomp_filter_args_null \ + test_bad_seccomp_filter_args_prctl \ + test_bad_seccomp_filter_args_prio \ + test_bad_seccomp_filter_args_socket \ + test_bad_seccomp_filter_length \ + test_bad_seccomp_filter_missing_trailing_newline \ + test_complain \ + test_complain_missed \ + test_noprofile \ + test_restrictions \ + test_restrictions_working \ + test_restrictions_working_args \ + test_restrictions_working_args_clone \ + test_restrictions_working_args_prctl \ + test_restrictions_working_args_prio \ + test_restrictions_working_args_socket \ + test_unrestricted \ + test_unrestricted_missed \ + test_whitelist + +EXTRA_DIST = $(all_tests) common.sh + +if SECCOMP +if CONFINEMENT_TESTS +TESTS += $(all_tests) +endif +endif + +check: ../snap-confine + +.PHONY: check-syntax +check-syntax: + shellcheck --format=gcc $(wildcard $(srcdir)/test_*) common.sh diff --git a/cmd/snap-confine/tests/common.sh b/cmd/snap-confine/tests/common.sh new file mode 100644 index 00000000..0959582d --- /dev/null +++ b/cmd/snap-confine/tests/common.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +get_common_syscalls() { + cat <"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "setpriority $i" >>"$TMP"/snap.name.app + + if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + cat "$TMP"/snap.name.app + FAIL + fi + + # all good + PASS +done + +# > SC_ARGS_MAXLENGTH in seccomp.c +for i in '- - - - - - 7' '1 2 3 4 5 6 7' ; do + printf "Test bad seccomp arg filtering (too many args (> 6): %s)" "$i" + cat "$TMP"/tmpl >"$TMP"/snap.name.app + echo "mbind $i" >>"$TMP"/snap.name.app + + if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + FAIL + else + PASS + fi +done diff --git a/cmd/snap-confine/tests/test_bad_seccomp_filter_args_clone b/cmd/snap-confine/tests/test_bad_seccomp_filter_args_clone new file mode 100755 index 00000000..200c770c --- /dev/null +++ b/cmd/snap-confine/tests/test_bad_seccomp_filter_args_clone @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "setns - $i" >>"$TMP"/snap.name.app + + if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + cat "$TMP"/snap.name.app + FAIL + fi + + # all good + PASS +done diff --git a/cmd/snap-confine/tests/test_bad_seccomp_filter_args_null b/cmd/snap-confine/tests/test_bad_seccomp_filter_args_null new file mode 100755 index 00000000..a16c3ea3 --- /dev/null +++ b/cmd/snap-confine/tests/test_bad_seccomp_filter_args_null @@ -0,0 +1,36 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app +printf "socket S\0CK_STREAM\n" >>"$TMP"/snap.name.app + +if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + cat "$TMP"/snap.name.app + FAIL +else + PASS +fi + +# an embedded null that is after a valid arg stops processing of the arg +# (limitation of fgets() implementation) +printf "Test ok seccomp arg filtering (socket SOCK_STREAM\\\0bad stuff)" +cat "$TMP"/tmpl >"$TMP"/snap.name.app +printf "socket SOCK_STREAM\0bad stuff\n" >>"$TMP"/snap.name.app + +if $L snap.name.app /bin/true 2>/dev/null; then + PASS +else + cat "$TMP"/snap.name.app + FAIL +fi diff --git a/cmd/snap-confine/tests/test_bad_seccomp_filter_args_prctl b/cmd/snap-confine/tests/test_bad_seccomp_filter_args_prctl new file mode 100755 index 00000000..94214640 --- /dev/null +++ b/cmd/snap-confine/tests/test_bad_seccomp_filter_args_prctl @@ -0,0 +1,40 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "prctl $i" >>"$TMP"/snap.name.app + + if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + cat "$TMP"/snap.name.app + FAIL + fi + + # all good + PASS +done + +for i in 'PR_CAP_AMBIENT_RAIS' 'PR_CAP_AMBIENT_RAISEE' ; do + printf "Test bad seccomp arg filtering (prctl PR_CAP_AMBIENT %s)" "$i" + cat "$TMP"/tmpl >"$TMP"/snap.name.app + echo "prctl PR_CAP_AMBIENT $i" >>"$TMP"/snap.name.app + + if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + cat "$TMP"/snap.name.app + FAIL + fi + + # all good + PASS +done diff --git a/cmd/snap-confine/tests/test_bad_seccomp_filter_args_prio b/cmd/snap-confine/tests/test_bad_seccomp_filter_args_prio new file mode 100755 index 00000000..2ecbf246 --- /dev/null +++ b/cmd/snap-confine/tests/test_bad_seccomp_filter_args_prio @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <= 0)" "$i" + cat "$TMP"/tmpl >"$TMP"/snap.name.app + echo "setpriority $i 0 >=0" >>"$TMP"/snap.name.app + + if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + cat "$TMP"/snap.name.app + FAIL + fi + + # all good + PASS +done diff --git a/cmd/snap-confine/tests/test_bad_seccomp_filter_args_socket b/cmd/snap-confine/tests/test_bad_seccomp_filter_args_socket new file mode 100755 index 00000000..0fc22155 --- /dev/null +++ b/cmd/snap-confine/tests/test_bad_seccomp_filter_args_socket @@ -0,0 +1,40 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "socket $i" >>"$TMP"/snap.name.app + + if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + cat "$TMP"/snap.name.app + FAIL + fi + + # all good + PASS +done + +for i in 'SOCK_STREA' 'SOCK_STREAMM' ; do + printf "Test bad seccomp arg filtering (socket - %s)" "$i" + cat "$TMP"/tmpl >"$TMP"/snap.name.app + echo "socket - $i" >>"$TMP"/snap.name.app + + if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + cat "$TMP"/snap.name.app + FAIL + fi + + # all good + PASS +done diff --git a/cmd/snap-confine/tests/test_bad_seccomp_filter_length b/cmd/snap-confine/tests/test_bad_seccomp_filter_length new file mode 100755 index 00000000..652cb422 --- /dev/null +++ b/cmd/snap-confine/tests/test_bad_seccomp_filter_length @@ -0,0 +1,20 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +printf "Test seccomp filter (bad - too long)" + +cat >"$TMP"/snap.name.app </dev/null; then + # true returned successfully, length test failed + FAIL +fi + +# all good +PASS diff --git a/cmd/snap-confine/tests/test_bad_seccomp_filter_missing_trailing_newline b/cmd/snap-confine/tests/test_bad_seccomp_filter_missing_trailing_newline new file mode 100755 index 00000000..47f006a1 --- /dev/null +++ b/cmd/snap-confine/tests/test_bad_seccomp_filter_missing_trailing_newline @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +printf "Test seccomp filter (bad - no trailing newline)" + +printf "missingnewline" > "$TMP"/snap.name.app + +if "$L" snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, length test failed + FAIL +fi + +# all good +PASS diff --git a/cmd/snap-confine/tests/test_complain b/cmd/snap-confine/tests/test_complain new file mode 100755 index 00000000..ac602d62 --- /dev/null +++ b/cmd/snap-confine/tests/test_complain @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +cat >"$TMP/snap.name.app" </dev/null; then + PASS +else + FAIL +fi diff --git a/cmd/snap-confine/tests/test_complain_missed b/cmd/snap-confine/tests/test_complain_missed new file mode 100755 index 00000000..547df093 --- /dev/null +++ b/cmd/snap-confine/tests/test_complain_missed @@ -0,0 +1,30 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +cat >"$TMP/snap.name.app" </dev/null; then + # true returned successfully, our filtering is broken! + FAIL +else + # true returned a error code, check dmesg + if dmesg|tail -n1|grep -q "audit"; then + PASS + else + FAIL + fi +fi diff --git a/cmd/snap-confine/tests/test_noprofile b/cmd/snap-confine/tests/test_noprofile new file mode 100755 index 00000000..8b2abaab --- /dev/null +++ b/cmd/snap-confine/tests/test_noprofile @@ -0,0 +1,12 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +printf "Test that a non-existing profile causes the launcher to not start" +if ! "$L" snap.name.app /bin/ls >"$TMP/testlog" 2>&1 ; then + PASS +else + FAIL +fi diff --git a/cmd/snap-confine/tests/test_restrictions b/cmd/snap-confine/tests/test_restrictions new file mode 100755 index 00000000..e0f4fbfa --- /dev/null +++ b/cmd/snap-confine/tests/test_restrictions @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +cat >"$TMP/snap.name.app" </dev/null; then + # true returned successfully, our filtering is broken! + FAIL +else + # true returned a error code, check dmesg + if dmesg|tail -n1|grep -q "audit"; then + PASS + else + FAIL + fi +fi diff --git a/cmd/snap-confine/tests/test_restrictions_working b/cmd/snap-confine/tests/test_restrictions_working new file mode 100755 index 00000000..765977a8 --- /dev/null +++ b/cmd/snap-confine/tests/test_restrictions_working @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP/snap.name.app" +cat >>"$TMP/snap.name.app" </dev/null; then + PASS +else + dmesg|tail -n1 + FAIL +fi diff --git a/cmd/snap-confine/tests/test_restrictions_working_args b/cmd/snap-confine/tests/test_restrictions_working_args new file mode 100755 index 00000000..5f02c2c6 --- /dev/null +++ b/cmd/snap-confine/tests/test_restrictions_working_args @@ -0,0 +1,78 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <=0' '- 0 >0' '- 0 <11' '- 0 <=10' ; do + cat "$TMP"/tmpl >"$TMP"/snap.name.app + echo "setpriority $i" >>"$TMP"/snap.name.app + + printf "Test good seccomp arg filtering (setpriority %s)" "$i" + # ensure that the command "true" can run with the right filter + if $L snap.name.app /usr/bin/nice -n 10 /bin/true ; then + PASS + else + dmesg|tail -n1 + FAIL + fi +done + +cat "$TMP"/tmpl >"$TMP"/snap.name.app +{ + echo "setpriority - - 10" + echo "setpriority - - <=9" + echo "setpriority - - >=11" +} >>"$TMP"/snap.name.app + +printf "Test good seccomp arg filtering (cumulative setpriority)" +# ensure that the command "true" can run with the right filter +if $L snap.name.app /usr/bin/nice -n 10 /bin/true ; then + PASS +else + dmesg|tail -n1 + FAIL +fi + +cat "$TMP"/tmpl >"$TMP"/snap.name.app +echo "setpriority - - <=9" >>"$TMP"/snap.name.app +echo "setpriority - - >=11" >>"$TMP"/snap.name.app + +printf "Test good seccomp arg filtering (cumulative setpriority blocks (ge/le))" +if $L snap.name.app /usr/bin/nice -n 10 /bin/true 2>/dev/null ; then + FAIL +else + PASS +fi + +cat "$TMP"/tmpl >"$TMP"/snap.name.app +echo "setpriority - - <10" >>"$TMP"/snap.name.app +echo "setpriority - - >10" >>"$TMP"/snap.name.app + +printf "Test good seccomp arg filtering (cumulative setpriority blocks (gt/lt))" +if $L snap.name.app /usr/bin/nice -n 10 /bin/true 2>/dev/null ; then + FAIL +else + PASS +fi + +# <= SC_ARGS_MAXLENGTH in seccomp.c +for i in '1' '- 2' '- - 3' '- - - 4' '- - - - 5' '- - - - - 6' '1 2 3 4 5 6' ; do + cat "$TMP"/tmpl >"$TMP"/snap.name.app + echo "mbind $i" >>"$TMP"/snap.name.app + + printf "Test good seccomp arg filtering (mbind %s)" "$i" + # ensure that the command "true" can run with the right filter + if $L snap.name.app /bin/true ; then + PASS + else + dmesg|tail -n1 + FAIL + fi +done + diff --git a/cmd/snap-confine/tests/test_restrictions_working_args_clone b/cmd/snap-confine/tests/test_restrictions_working_args_clone new file mode 100755 index 00000000..402935e1 --- /dev/null +++ b/cmd/snap-confine/tests/test_restrictions_working_args_clone @@ -0,0 +1,24 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "setns - $i" >>"$TMP"/snap.name.app + + printf "Test good seccomp arg filtering (setns - %s)" "$i" + # ensure that the command "true" can run with the right filter + if $L snap.name.app /bin/true ; then + PASS + else + dmesg|tail -n1 + FAIL + fi +done diff --git a/cmd/snap-confine/tests/test_restrictions_working_args_prctl b/cmd/snap-confine/tests/test_restrictions_working_args_prctl new file mode 100755 index 00000000..c278073b --- /dev/null +++ b/cmd/snap-confine/tests/test_restrictions_working_args_prctl @@ -0,0 +1,38 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "prctl $i" >>"$TMP"/snap.name.app + + printf "Test good seccomp arg filtering (prctl %s)" "$i" + # ensure that the command "true" can run with the right filter + if $L snap.name.app /bin/true ; then + PASS + else + dmesg|tail -n1 + FAIL + fi + + if [ "$i" = "PR_CAP_AMBIENT" ]; then + for j in PR_CAP_AMBIENT_RAISE PR_CAP_AMBIENT_LOWER PR_CAP_AMBIENT_IS_SET PR_CAP_AMBIENT_CLEAR_ALL ; do + cat "$TMP"/tmpl >"$TMP"/snap.name.app + echo "prctl $i $j" >>"$TMP"/snap.name.app + printf "Test good seccomp arg filtering (prctl %s %s)" "$i" "$j" + if $L snap.name.app /bin/true ; then + PASS + else + dmesg|tail -n1 + FAIL + fi + done + fi +done diff --git a/cmd/snap-confine/tests/test_restrictions_working_args_prio b/cmd/snap-confine/tests/test_restrictions_working_args_prio new file mode 100755 index 00000000..99c4b562 --- /dev/null +++ b/cmd/snap-confine/tests/test_restrictions_working_args_prio @@ -0,0 +1,24 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "setpriority $i - -" >>"$TMP"/snap.name.app + + printf "Test good seccomp arg filtering (setpriority %s - -)" "$i" + # ensure that the command "true" can run with the right filter + if $L snap.name.app /bin/true ; then + PASS + else + dmesg|tail -n1 + FAIL + fi +done diff --git a/cmd/snap-confine/tests/test_restrictions_working_args_socket b/cmd/snap-confine/tests/test_restrictions_working_args_socket new file mode 100755 index 00000000..3b1c81a9 --- /dev/null +++ b/cmd/snap-confine/tests/test_restrictions_working_args_socket @@ -0,0 +1,38 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "socket $i" >>"$TMP"/snap.name.app + + printf "Test good seccomp arg filtering (socket %s)" "$i" + # ensure that the command "true" can run with the right filter + if $L snap.name.app /bin/true ; then + PASS + else + dmesg|tail -n1 + FAIL + fi + + for j in SOCK_STREAM SOCK_DGRAM SOCK_SEQPACKET SOCK_RAW SOCK_RDM SOCK_PACKET ; do + cat "$TMP"/tmpl >"$TMP"/snap.name.app + echo "socket $i $j" >>"$TMP"/snap.name.app + + printf "Test good seccomp arg filtering (socket %s %s)" "$i" "$j" + # ensure that the command "true" can run with the right filter + if $L snap.name.app /bin/true ; then + PASS + else + dmesg|tail -n1 + FAIL + fi + done +done diff --git a/cmd/snap-confine/tests/test_unrestricted b/cmd/snap-confine/tests/test_unrestricted new file mode 100755 index 00000000..71be1a90 --- /dev/null +++ b/cmd/snap-confine/tests/test_unrestricted @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +cat >"$TMP/snap.name.app" </dev/null; then + PASS +else + FAIL +fi diff --git a/cmd/snap-confine/tests/test_unrestricted_missed b/cmd/snap-confine/tests/test_unrestricted_missed new file mode 100755 index 00000000..3e1c679f --- /dev/null +++ b/cmd/snap-confine/tests/test_unrestricted_missed @@ -0,0 +1,30 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +cat >"$TMP/snap.name.app" </dev/null; then + # true returned successfully, our filtering is broken! + FAIL +else + # true returned a error code, check dmesg + if dmesg|tail -n1|grep -q "audit"; then + PASS + else + FAIL + fi +fi diff --git a/cmd/snap-confine/tests/test_whitelist b/cmd/snap-confine/tests/test_whitelist new file mode 100755 index 00000000..248603ac --- /dev/null +++ b/cmd/snap-confine/tests/test_whitelist @@ -0,0 +1,39 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/common.sh" + +printf "Test appname whitelist" + +cat >"$TMP/snap.name.app" </dev/null; then + FAIL + else + PASS + fi +done + +printf "Test bad appname whitelist - 'appname space'" +if "$L" 'appname space' /bin/true 2>/dev/null; then + # true returned successfully, our appname whitelist is broken! + FAIL +else + PASS +fi diff --git a/cmd/snap-confine/udev-support.c b/cmd/snap-confine/udev-support.c new file mode 100644 index 00000000..38b28ab3 --- /dev/null +++ b/cmd/snap-confine/udev-support.c @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2015-2016 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 . + * + */ +#include "config.h" +#include "udev-support.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "utils.h" +#include "snap.h" + +void run_snappy_app_dev_add(struct snappy_udev *udev_s, const char *path) +{ + if (udev_s == NULL) + die("snappy_udev is NULL"); + if (udev_s->udev == NULL) + die("snappy_udev->udev is NULL"); + if (udev_s->tagname_len == 0 + || udev_s->tagname_len >= MAX_BUF + || strnlen(udev_s->tagname, MAX_BUF) != udev_s->tagname_len + || udev_s->tagname[udev_s->tagname_len] != '\0') + die("snappy_udev->tagname has invalid length"); + + debug("%s: %s %s", __func__, path, udev_s->tagname); + + struct udev_device *d = + udev_device_new_from_syspath(udev_s->udev, path); + if (d == NULL) + die("can not find %s", path); + dev_t devnum = udev_device_get_devnum(d); + udev_device_unref(d); + + int status = 0; + pid_t pid = fork(); + if (pid < 0) { + die("could not fork"); + } + if (pid == 0) { + uid_t real_uid, effective_uid, saved_uid; + if (getresuid(&real_uid, &effective_uid, &saved_uid) != 0) + die("could not find user IDs"); + // can't update the cgroup unless the real_uid is 0, euid as + // 0 is not enough + if (real_uid != 0 && effective_uid == 0) + if (setuid(0) != 0) + die("setuid failed"); + char buf[64]; + // pass snappy-add-dev an empty environment so the + // user-controlled environment can't be used to subvert + // snappy-add-dev + char *env[] = { NULL }; + unsigned major = MAJOR(devnum); + unsigned minor = MINOR(devnum); + must_snprintf(buf, sizeof(buf), "%u:%u", major, minor); + execle("/lib/udev/snappy-app-dev", "/lib/udev/snappy-app-dev", + "add", udev_s->tagname, path, buf, NULL, env); + die("execl failed"); + } + if (waitpid(pid, &status, 0) < 0) + die("waitpid failed"); + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) + die("child exited with status %i", WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + die("child died with signal %i", WTERMSIG(status)); +} + +/* + * snappy_udev_init() - setup the snappy_udev structure. Return 0 if devices + * are assigned, else return -1. Callers should use snappy_udev_cleanup() to + * cleanup. + */ +int snappy_udev_init(const char *security_tag, struct snappy_udev *udev_s) +{ + debug("%s", __func__); + int rc = 0; + + // extra paranoia + if (!verify_security_tag(security_tag)) + die("security tag %s not allowed", security_tag); + + udev_s->tagname[0] = '\0'; + udev_s->tagname_len = 0; + // TAG+="snap_" (udev doesn't like '.' in the tag name) + udev_s->tagname_len = must_snprintf(udev_s->tagname, MAX_BUF, + "%s", security_tag); + for (int i = 0; i < udev_s->tagname_len; i++) + if (udev_s->tagname[i] == '.') + udev_s->tagname[i] = '_'; + + udev_s->udev = udev_new(); + if (udev_s->udev == NULL) + die("udev_new failed"); + + udev_s->devices = udev_enumerate_new(udev_s->udev); + if (udev_s->devices == NULL) + die("udev_enumerate_new failed"); + + if (udev_enumerate_add_match_tag(udev_s->devices, udev_s->tagname) != 0) + die("udev_enumerate_add_match_tag"); + + if (udev_enumerate_scan_devices(udev_s->devices) != 0) + die("udev_enumerate_scan failed"); + + udev_s->assigned = udev_enumerate_get_list_entry(udev_s->devices); + if (udev_s->assigned == NULL) + rc = -1; + + return rc; +} + +void snappy_udev_cleanup(struct snappy_udev *udev_s) +{ + // udev_s->assigned does not need to be unreferenced since it is a + // pointer into udev_s->devices + if (udev_s->devices != NULL) + udev_enumerate_unref(udev_s->devices); + if (udev_s->udev != NULL) + udev_unref(udev_s->udev); +} + +void setup_devices_cgroup(const char *security_tag, struct snappy_udev *udev_s) +{ + debug("%s", __func__); + // Devices that must always be present + const char *static_devices[] = { + "/sys/class/mem/null", + "/sys/class/mem/full", + "/sys/class/mem/zero", + "/sys/class/mem/random", + "/sys/class/mem/urandom", + "/sys/class/tty/tty", + "/sys/class/tty/console", + "/sys/class/tty/ptmx", + NULL, + }; + + // extra paranoia + if (!verify_security_tag(security_tag)) + die("security tag %s not allowed", security_tag); + if (udev_s == NULL) + die("snappy_udev is NULL"); + if (udev_s->udev == NULL) + die("snappy_udev->udev is NULL"); + if (udev_s->devices == NULL) + die("snappy_udev->devices is NULL"); + if (udev_s->assigned == NULL) + die("snappy_udev->assigned is NULL"); + if (udev_s->tagname_len == 0 + || udev_s->tagname_len >= MAX_BUF + || strnlen(udev_s->tagname, MAX_BUF) != udev_s->tagname_len + || udev_s->tagname[udev_s->tagname_len] != '\0') + die("snappy_udev->tagname has invalid length"); + + // create devices cgroup controller + char cgroup_dir[PATH_MAX]; + + must_snprintf(cgroup_dir, sizeof(cgroup_dir), + "/sys/fs/cgroup/devices/%s/", security_tag); + + if (mkdir(cgroup_dir, 0755) < 0 && errno != EEXIST) + die("mkdir failed"); + + // move ourselves into it + char cgroup_file[PATH_MAX]; + must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir, + "tasks"); + + char buf[128]; + must_snprintf(buf, sizeof(buf), "%i", getpid()); + write_string_to_file(cgroup_file, buf); + + // deny by default. Write 'a' to devices.deny to remove all existing + // devices that were added in previous launcher invocations, then add + // the static and assigned devices. This ensures that at application + // launch the cgroup only has what is currently assigned. + must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir, + "devices.deny"); + write_string_to_file(cgroup_file, "a"); + + // add the common devices + for (int i = 0; static_devices[i] != NULL; i++) + run_snappy_app_dev_add(udev_s, static_devices[i]); + + // add the assigned devices + while (udev_s->assigned != NULL) { + const char *path = udev_list_entry_get_name(udev_s->assigned); + if (path == NULL) + die("udev_list_entry_get_name failed"); + run_snappy_app_dev_add(udev_s, path); + udev_s->assigned = udev_list_entry_get_next(udev_s->assigned); + } +} diff --git a/cmd/snap-confine/udev-support.h b/cmd/snap-confine/udev-support.h new file mode 100644 index 00000000..6ad59ae4 --- /dev/null +++ b/cmd/snap-confine/udev-support.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015-2016 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 . + * + */ + +#ifndef SNAP_CONFINE_UDEV_SUPPORT_H +#define SNAP_CONFINE_UDEV_SUPPORT_H + +#include + +#include + +#define MAX_BUF 1000 + +struct snappy_udev { + struct udev *udev; + struct udev_enumerate *devices; + struct udev_list_entry *assigned; + char tagname[MAX_BUF]; + size_t tagname_len; +}; + +void run_snappy_app_dev_add(struct snappy_udev *udev_s, const char *path); +int snappy_udev_init(const char *security_tag, struct snappy_udev *udev_s); +void snappy_udev_cleanup(struct snappy_udev *udev_s); +void setup_devices_cgroup(const char *security_tag, struct snappy_udev *udev_s); + +#endif diff --git a/cmd/snap-confine/unit-tests-main.c b/cmd/snap-confine/unit-tests-main.c new file mode 100644 index 00000000..f2c0993d --- /dev/null +++ b/cmd/snap-confine/unit-tests-main.c @@ -0,0 +1,22 @@ +/* + * Copyright (C) 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 . + * + */ +#include "unit-tests.h" + +int main(int argc, char **argv) +{ + return sc_run_unit_tests(&argc, &argv); +} diff --git a/cmd/snap-confine/unit-tests.c b/cmd/snap-confine/unit-tests.c new file mode 100644 index 00000000..783809d3 --- /dev/null +++ b/cmd/snap-confine/unit-tests.c @@ -0,0 +1,36 @@ +/* + * Copyright (C) 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 . + * + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "unit-tests.h" +#include + +static void simple_test_case(void) +{ + g_assert(g_bit_storage(1) == 1); + g_assert_cmpint(g_bit_storage(1), ==, 1); +} + +int sc_run_unit_tests(int *argc, char ***argv) +{ + g_test_init(argc, argv, NULL); + g_test_set_nonfatal_assertions(); + g_test_add_func("/Simple Test Case", simple_test_case); + return g_test_run(); +} diff --git a/cmd/snap-confine/unit-tests.h b/cmd/snap-confine/unit-tests.h new file mode 100644 index 00000000..31414ad5 --- /dev/null +++ b/cmd/snap-confine/unit-tests.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#ifndef SNAP_CONFINE_UNIT_TESTS_H +#define SNAP_CONFINE_UNIT_TESTS_H + +/** + * Run unit tests and exit. + * + * The function inspects and modifies command line arguments. + * Internally it is using glib-test functions. + */ +int sc_run_unit_tests(int *argc, char ***argv); + +#endif // SNAP_CONFINE_SANITY_H diff --git a/cmd/snap-confine/user-support.c b/cmd/snap-confine/user-support.c new file mode 100644 index 00000000..9898c553 --- /dev/null +++ b/cmd/snap-confine/user-support.c @@ -0,0 +1,65 @@ +/* + * Copyright (C) 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 . + * + */ +#include "config.h" +#include "user-support.h" + +#include +#include +#include + +#include "utils.h" + +void setup_user_data() +{ + const char *user_data = getenv("SNAP_USER_DATA"); + + if (user_data == NULL) + return; + + // Only support absolute paths. + if (user_data[0] != '/') { + die("user data directory must be an absolute path"); + } + + debug("creating user data directory: %s", user_data); + if (sc_nonfatal_mkpath(user_data, 0755) < 0) { + die("cannot create user data directory: %s", user_data); + }; +} + +void setup_user_xdg_runtime_dir() +{ + const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); + + if (xdg_runtime_dir == NULL) + return; + // Only support absolute paths. + if (xdg_runtime_dir[0] != '/') { + die("XDG_RUNTIME_DIR must be an absolute path"); + } + + errno = 0; + debug("creating user XDG_RUNTIME_DIR directory: %s", xdg_runtime_dir); + if (sc_nonfatal_mkpath(xdg_runtime_dir, 0755) < 0) { + die("cannot create user XDG_RUNTIME_DIR directory: %s", + xdg_runtime_dir); + } + // if successfully created the directory (ie, not EEXIST), then chmod it. + if (errno == 0 && chmod(xdg_runtime_dir, 0700) != 0) { + die("cannot change permissions of user XDG_RUNTIME_DIR directory to 0700"); + } +} diff --git a/cmd/snap-confine/user-support.h b/cmd/snap-confine/user-support.h new file mode 100644 index 00000000..32f4e2f3 --- /dev/null +++ b/cmd/snap-confine/user-support.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 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 . + * + */ + +#ifndef SNAP_CONFINE_USER_SUPPORT_H +#define SNAP_CONFINE_USER_SUPPORT_H + +void setup_user_data(); +void setup_user_xdg_runtime_dir(); +void mkpath(const char *const path); + +#endif diff --git a/cmd/snap-confine/utils-test.c b/cmd/snap-confine/utils-test.c new file mode 100644 index 00000000..b029db75 --- /dev/null +++ b/cmd/snap-confine/utils-test.c @@ -0,0 +1,174 @@ +/* + * Copyright (C) 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 . + * + */ + +#include "utils.h" +#include "utils.c" + +#include + +static void test_str2bool() +{ + int err; + bool value; + + err = str2bool("yes", &value); + g_assert_cmpint(err, ==, 0); + g_assert_true(value); + + err = str2bool("1", &value); + g_assert_cmpint(err, ==, 0); + g_assert_true(value); + + err = str2bool("no", &value); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + err = str2bool("0", &value); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + err = str2bool("", &value); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + err = str2bool(NULL, &value); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + err = str2bool("flower", &value); + g_assert_cmpint(err, ==, -1); + g_assert_cmpint(errno, ==, EINVAL); + + err = str2bool("yes", NULL); + g_assert_cmpint(err, ==, -1); + g_assert_cmpint(errno, ==, EFAULT); +} + +static void test_die() +{ + if (g_test_subprocess()) { + errno = 0; + die("death message"); + g_test_message("expected die not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("death message\n"); +} + +static void test_die_with_errno() +{ + if (g_test_subprocess()) { + errno = EPERM; + die("death message"); + g_test_message("expected die not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("death message: Operation not permitted\n"); +} + +/** + * Perform the rest of testing in a ephemeral directory. + * + * Create a temporary directory, move the current process there and undo those + * operations at the end of the test. If any additional directories or files + * are created in this directory they must be removed by the caller. + **/ +static void g_test_in_ephemeral_dir() +{ + gchar *temp_dir = g_dir_make_tmp(NULL, NULL); + gchar *orig_dir = g_get_current_dir(); + int err = chdir(temp_dir); + g_assert_cmpint(err, ==, 0); + + g_test_queue_destroy((GDestroyNotify) rmdir, temp_dir); + g_test_queue_free(temp_dir); + g_test_queue_destroy((GDestroyNotify) chdir, orig_dir); + g_test_queue_free(orig_dir); +} + +/** + * Test sc_nonfatal_mkpath() given two directories. + **/ +static void _test_sc_nonfatal_mkpath(const gchar * dirname, + const gchar * subdirname) +{ + // Check that directory does not exist. + g_assert_false(g_file_test(dirname, G_FILE_TEST_EXISTS | + G_FILE_TEST_IS_DIR)); + // Use sc_nonfatal_mkpath to create the directory and ensure that it worked + // as expected. + g_test_queue_destroy((GDestroyNotify) rmdir, (char *)dirname); + int err = sc_nonfatal_mkpath(dirname, 0755); + g_assert_cmpint(err, ==, 0); + g_assert_cmpint(errno, ==, 0); + g_assert_true(g_file_test(dirname, G_FILE_TEST_EXISTS | + G_FILE_TEST_IS_REGULAR)); + // Use same function again to try to create the same directory and ensure + // that it didn't fail and properly retained EEXIST in errno. + err = sc_nonfatal_mkpath(dirname, 0755); + g_assert_cmpint(err, ==, 0); + g_assert_cmpint(errno, ==, EEXIST); + // Now create a sub-directory of the original directory and observe the + // results. We should no longer see errno of EEXIST! + g_test_queue_destroy((GDestroyNotify) rmdir, (char *)subdirname); + err = sc_nonfatal_mkpath(subdirname, 0755); + g_assert_cmpint(err, ==, 0); + g_assert_cmpint(errno, ==, 0); +} + +/** + * Test that sc_nonfatal_mkpath behaves when using relative paths. + **/ +static void test_sc_nonfatal_mkpath__relative() +{ + g_test_in_ephemeral_dir(); + gchar *current_dir = g_get_current_dir(); + g_test_queue_free(current_dir); + gchar *dirname = g_build_path("/", current_dir, "foo", NULL); + g_test_queue_free(dirname); + gchar *subdirname = g_build_path("/", current_dir, "foo", "bar", NULL); + g_test_queue_free(subdirname); + _test_sc_nonfatal_mkpath(dirname, subdirname); +} + +/** + * Test that sc_nonfatal_mkpath behaves when using absolute paths. + **/ +static void test_sc_nonfatal_mkpath__absolute() +{ + g_test_in_ephemeral_dir(); + const char *dirname = "foo"; + const char *subdirname = "foo/bar"; + _test_sc_nonfatal_mkpath(dirname, subdirname); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/utils/str2bool", test_str2bool); + g_test_add_func("/utils/die", test_die); + g_test_add_func("/utils/die_with_errno", test_die_with_errno); + g_test_add_func("/utils/sc_nonfatal_mkpath/relative", + test_sc_nonfatal_mkpath__relative); + g_test_add_func("/utils/sc_nonfatal_mkpath/absolute", + test_sc_nonfatal_mkpath__absolute); +} diff --git a/cmd/snap-confine/utils.c b/cmd/snap-confine/utils.c new file mode 100644 index 00000000..25144230 --- /dev/null +++ b/cmd/snap-confine/utils.c @@ -0,0 +1,219 @@ +/* + * Copyright (C) 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 . + * + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils.h" +#include "cleanup-funcs.h" + +void die(const char *msg, ...) +{ + int saved_errno = errno; + va_list va; + va_start(va, msg); + vfprintf(stderr, msg, va); + va_end(va); + + if (errno != 0) { + fprintf(stderr, ": %s\n", strerror(saved_errno)); + } else { + fprintf(stderr, "\n"); + } + exit(1); +} + +bool error(const char *msg, ...) +{ + va_list va; + va_start(va, msg); + vfprintf(stderr, msg, va); + va_end(va); + + return false; +} + +struct sc_bool_name { + const char *text; + bool value; +}; + +static const struct sc_bool_name sc_bool_names[] = { + {"yes", true}, + {"no", false}, + {"1", true}, + {"0", false}, + {"", false}, +}; + +/** + * Convert string to a boolean value. + * + * The return value is 0 in case of success or -1 when the string cannot be + * converted correctly. In such case errno is set to indicate the problem and + * the value is not written back to the caller-supplied pointer. + **/ +static int str2bool(const char *text, bool * value) +{ + if (value == NULL) { + errno = EFAULT; + return -1; + } + if (text == NULL) { + *value = false; + return 0; + } + for (int i = 0; i < sizeof sc_bool_names / sizeof *sc_bool_names; ++i) { + if (strcmp(text, sc_bool_names[i].text) == 0) { + *value = sc_bool_names[i].value; + return 0; + } + } + errno = EINVAL; + return -1; +} + +/** + * Get an environment variable and convert it to a boolean. + * + * Supported values are those of str2bool(), namely "yes", "no" as well as "1" + * and "0". All other values are treated as false and a diagnostic message is + * printed to stderr. + **/ +static bool getenv_bool(const char *name) +{ + const char *str_value = getenv(name); + bool value; + if (str2bool(str_value, &value) < 0) { + if (errno == EINVAL) { + fprintf(stderr, + "WARNING: unrecognized value of environment variable %s (expected yes/no or 1/0)\n", + name); + return false; + } else { + die("cannot convert value of environment variable %s to a boolean", name); + } + } + return value; +} + +void debug(const char *msg, ...) +{ + if (getenv_bool("SNAP_CONFINE_DEBUG")) { + va_list va; + va_start(va, msg); + fprintf(stderr, "DEBUG: "); + vfprintf(stderr, msg, va); + fprintf(stderr, "\n"); + va_end(va); + } +} + +void write_string_to_file(const char *filepath, const char *buf) +{ + debug("write_string_to_file %s %s", filepath, buf); + FILE *f = fopen(filepath, "w"); + if (f == NULL) + die("fopen %s failed", filepath); + if (fwrite(buf, strlen(buf), 1, f) != 1) + die("fwrite failed"); + if (fflush(f) != 0) + die("fflush failed"); + if (fclose(f) != 0) + die("fclose failed"); +} + +int must_snprintf(char *str, size_t size, const char *format, ...) +{ + int n; + + va_list va; + va_start(va, format); + n = vsnprintf(str, size, format, va); + va_end(va); + + if (n < 0 || n >= size) + die("failed to snprintf %s", str); + + return n; +} + +int sc_nonfatal_mkpath(const char *const path, mode_t mode) +{ + // If asked to create an empty path, return immediately. + if (strlen(path) == 0) { + return 0; + } + // We're going to use strtok_r, which needs to modify the path, so we'll + // make a copy of it. + char *path_copy __attribute__ ((cleanup(sc_cleanup_string))) = NULL; + path_copy = strdup(path); + if (path_copy == NULL) { + return -1; + } + // Open flags to use while we walk the user data path: + // - Don't follow symlinks + // - Don't allow child access to file descriptor + // - Only open a directory (fail otherwise) + const int open_flags = O_NOFOLLOW | O_CLOEXEC | O_DIRECTORY; + + // We're going to create each path segment via openat/mkdirat calls instead + // of mkdir calls, to avoid following symlinks and placing the user data + // directory somewhere we never intended for it to go. The first step is to + // get an initial file descriptor. + int fd __attribute__ ((cleanup(sc_cleanup_close))) = AT_FDCWD; + if (path_copy[0] == '/') { + fd = open("/", open_flags); + if (fd < 0) { + return -1; + } + } + // strtok_r needs a pointer to keep track of where it is in the string. + char *path_walker = NULL; + + // Initialize tokenizer and obtain first path segment. + char *path_segment = strtok_r(path_copy, "/", &path_walker); + while (path_segment) { + // Try to create the directory. It's okay if it already existed, but + // return with error on any other error. Reset errno before attempting + // this as it may stay stale (errno is not reset if mkdirat(2) returns + // successfully). + errno = 0; + if (mkdirat(fd, path_segment, mode) < 0 && errno != EEXIST) { + return -1; + } + // Open the parent directory we just made (and close the previous one + // (but not the special value AT_FDCWD) so we can continue down the + // path. + int previous_fd = fd; + fd = openat(fd, path_segment, open_flags); + if (previous_fd != AT_FDCWD && close(previous_fd) != 0) { + return -1; + } + if (fd < 0) { + return -1; + } + // Obtain the next path segment. + path_segment = strtok_r(NULL, "/", &path_walker); + } + return 0; +} diff --git a/cmd/snap-confine/utils.h b/cmd/snap-confine/utils.h new file mode 100644 index 00000000..35d805be --- /dev/null +++ b/cmd/snap-confine/utils.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 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 . + * + */ +#include +#include + +#ifndef CORE_LAUNCHER_UTILS_H +#define CORE_LAUNCHER_UTILS_H + +__attribute__ ((noreturn)) + __attribute__ ((format(printf, 1, 2))) +void die(const char *fmt, ...); + +__attribute__ ((format(printf, 1, 2))) +bool error(const char *fmt, ...); + +__attribute__ ((format(printf, 1, 2))) +void debug(const char *fmt, ...); + +void write_string_to_file(const char *filepath, const char *buf); + +// snprintf version that dies on any error condition +__attribute__ ((format(printf, 3, 4))) +int must_snprintf(char *str, size_t size, const char *format, ...); + +/** + * Safely create a given directory. + * + * NOTE: non-fatal functions don't die on errors. It is the responsibility of + * the caller to call die() or handle the error appropriately. + * + * This function behaves like "mkdir -p" (recursive mkdir) with the exception + * that each directory is carefully created in a way that avoids symlink + * attacks. The preceding directory is kept openat(2) (along with O_DIRECTORY) + * and the next directory is created using mkdirat(2), this sequence continues + * while there are more directories to process. + * + * The function returns -1 in case of any error. + **/ +__attribute__ ((warn_unused_result)) +int sc_nonfatal_mkpath(const char *const path, mode_t mode); +#endif diff --git a/cmd/snap-confine/verify-executable-name-test.c b/cmd/snap-confine/verify-executable-name-test.c new file mode 100644 index 00000000..580dde35 --- /dev/null +++ b/cmd/snap-confine/verify-executable-name-test.c @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 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 . + * + */ + +#include "snap.h" +#include "snap.c" + +#include + +static void test_verify_security_tag() +{ + // First, test the names we know are good + g_assert_true(verify_security_tag("snap.name.app")); + g_assert_true(verify_security_tag + ("snap.network-manager.NetworkManager")); + g_assert_true(verify_security_tag("snap.f00.bar-baz1")); + g_assert_true(verify_security_tag("snap.foo.hook.bar")); + g_assert_true(verify_security_tag("snap.foo.hook.bar-baz")); + + // Now, test the names we know are bad + g_assert_false(verify_security_tag("pkg-foo.bar.0binary-bar+baz")); + g_assert_false(verify_security_tag("pkg-foo_bar_1.1")); + g_assert_false(verify_security_tag("appname/..")); + g_assert_false(verify_security_tag("snap")); + g_assert_false(verify_security_tag("snap.")); + g_assert_false(verify_security_tag("snap.name")); + g_assert_false(verify_security_tag("snap.name.")); + g_assert_false(verify_security_tag("snap.name.app.")); + g_assert_false(verify_security_tag("snap.name.hook.")); + g_assert_false(verify_security_tag("snap!name.app")); + g_assert_false(verify_security_tag("snap.-name.app")); + g_assert_false(verify_security_tag("snap.name!app")); + g_assert_false(verify_security_tag("snap.name.-app")); + g_assert_false(verify_security_tag("snap.name.app!hook.foo")); + g_assert_false(verify_security_tag("snap.name.app.hook!foo")); + g_assert_false(verify_security_tag("snap.name.app.hook.-foo")); + g_assert_false(verify_security_tag("snap.name.app.hook.f00")); + g_assert_false(verify_security_tag("sna.pname.app")); + g_assert_false(verify_security_tag("snap.n@me.app")); + g_assert_false(verify_security_tag("SNAP.name.app")); + g_assert_false(verify_security_tag("snap.Name.app")); + g_assert_false(verify_security_tag("snap.0name.app")); + g_assert_false(verify_security_tag("snap.-name.app")); + g_assert_false(verify_security_tag("snap.name.@app")); + g_assert_false(verify_security_tag(".name.app")); + g_assert_false(verify_security_tag("snap..name.app")); + g_assert_false(verify_security_tag("snap.name..app")); + g_assert_false(verify_security_tag("snap.name.app..")); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/snap/verify_security_tag", test_verify_security_tag); +} diff --git a/cmd/snap-exec/main.go b/cmd/snap-exec/main.go new file mode 100644 index 00000000..8cbddb73 --- /dev/null +++ b/cmd/snap-exec/main.go @@ -0,0 +1,184 @@ +// -*- 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/snap" +) + +// for the tests +var syscallExec = syscall.Exec + +// commandline args +var opts struct { + Command string `long:"command" description:"use a different command like {stop,post-stop} from the app"` + Hook string `long:"hook" description:"hook to run" hidden:"yes"` +} + +func main() { + if err := run(); err != nil { + fmt.Printf("cannot snap-exec: %s\n", err) + os.Exit(1) + } +} + +func parseArgs(args []string) (app string, appArgs []string, err error) { + parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) + rest, err := parser.ParseArgs(args) + if err != nil { + return "", nil, err + } + if len(rest) == 0 { + return "", nil, fmt.Errorf("need the application to run as argument") + } + + // Catch some invalid parameter combinations, provide helpful errors + if opts.Hook != "" && opts.Command != "" { + return "", nil, fmt.Errorf("cannot use --hook and --command together") + } + if opts.Hook != "" && len(rest) > 1 { + return "", nil, fmt.Errorf("too many arguments for hook %q: %s", opts.Hook, strings.Join(rest, " ")) + } + + return rest[0], rest[1:], nil +} + +func run() error { + snapApp, extraArgs, err := parseArgs(os.Args[1:]) + if err != nil { + return err + } + + // the SNAP_REVISION is set by `snap run` - we can not (easily) + // find it in `snap-exec` because `snap-exec` is run inside the + // confinement and (generally) can not talk to snapd + revision := os.Getenv("SNAP_REVISION") + + // Now actually handle the dispatching + if opts.Hook != "" { + return snapExecHook(snapApp, revision, opts.Hook) + } + + return snapExecApp(snapApp, revision, opts.Command, extraArgs) +} + +func findCommand(app *snap.AppInfo, command string) (string, error) { + var cmd string + switch command { + case "shell": + cmd = "/bin/bash" + case "stop": + cmd = app.StopCommand + case "post-stop": + cmd = app.PostStopCommand + case "": + cmd = app.Command + default: + return "", fmt.Errorf("cannot use %q command", command) + } + + if cmd == "" { + return "", fmt.Errorf("no %q command found for %q", command, app.Name) + } + return cmd, nil +} + +func snapExecApp(snapApp, revision, command string, args []string) error { + rev, err := snap.ParseRevision(revision) + if err != nil { + return fmt.Errorf("cannot parse revision %q: %s", revision, err) + } + + snapName, appName := snap.SplitSnapApp(snapApp) + info, err := snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: rev, + }) + if err != nil { + return fmt.Errorf("cannot read info for %q: %s", snapName, err) + } + + app := info.Apps[appName] + if app == nil { + return fmt.Errorf("cannot find app %q in %q", appName, snapName) + } + + cmdAndArgs, err := findCommand(app, command) + if err != nil { + return err + } + // strings.Split() is ok here because we validate all app fields + // and the whitelist is pretty strict (see + // snap/validate.go:appContentWhitelist) + cmdArgv := strings.Split(cmdAndArgs, " ") + cmd := cmdArgv[0] + cmdArgs := cmdArgv[1:] + + // build the environment from the yaml + env := append(os.Environ(), app.Env()...) + + // run the command + fullCmd := filepath.Join(app.Snap.MountDir(), cmd) + if command == "shell" { + fullCmd = "/bin/bash" + cmdArgs = nil + } + fullCmdArgs := []string{fullCmd} + fullCmdArgs = append(fullCmdArgs, cmdArgs...) + fullCmdArgs = append(fullCmdArgs, args...) + if err := syscallExec(fullCmd, fullCmdArgs, env); err != nil { + return fmt.Errorf("cannot exec %q: %s", fullCmd, err) + } + // this is never reached except in tests + return nil +} + +func snapExecHook(snapName, revision, hookName string) error { + rev, err := snap.ParseRevision(revision) + if err != nil { + return err + } + + info, err := snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: rev, + }) + if err != nil { + return err + } + + hook := info.Hooks[hookName] + if hook == nil { + return fmt.Errorf("cannot find hook %q in %q", hookName, snapName) + } + + // build the environment + env := append(os.Environ(), hook.Env()...) + + // run the hook + hookPath := filepath.Join(hook.Snap.HooksDir(), hook.Name) + return syscallExec(hookPath, []string{hookPath}, env) +} diff --git a/cmd/snap-exec/main_test.go b/cmd/snap-exec/main_test.go new file mode 100644 index 00000000..1e59ce5d --- /dev/null +++ b/cmd/snap-exec/main_test.go @@ -0,0 +1,311 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "syscall" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type snapExecSuite struct{} + +var _ = Suite(&snapExecSuite{}) + +func (s *snapExecSuite) SetUpTest(c *C) { + // clean previous parse runs + opts.Command = "" + opts.Hook = "" +} + +func (s *snapExecSuite) TearDown(c *C) { + syscallExec = syscall.Exec + dirs.SetRootDir("/") +} + +var mockYaml = []byte(`name: snapname +version: 1.0 +apps: + app: + command: run-app cmd-arg1 + stop-command: stop-app + post-stop-command: post-stop-app + environment: + LD_LIBRARY_PATH: /some/path + nostop: + command: nostop +`) + +var mockHookYaml = []byte(`name: snapname +version: 1.0 +hooks: + configure: +`) + +var mockContents = "" + +var binaryTemplate = `#!/bin/sh +echo "$(basename $0)" >> %[1]q +for arg in "$@"; do +echo "$arg" >> %[1]q +done +printf "\n" >> %[1]q` + +func (s *snapExecSuite) TestInvalidCombinedParameters(c *C) { + invalidParameters := []string{"--hook=hook-name", "--command=command-name", "snap-name"} + _, _, err := parseArgs(invalidParameters) + c.Check(err, ErrorMatches, ".*cannot use --hook and --command together.*") +} + +func (s *snapExecSuite) TestInvalidExtraParameters(c *C) { + invalidParameters := []string{"--hook=hook-name", "snap-name", "foo", "bar"} + _, _, err := parseArgs(invalidParameters) + c.Check(err, ErrorMatches, ".*too many arguments for hook \"hook-name\": snap-name foo bar.*") +} + +func (s *snapExecSuite) TestFindCommand(c *C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, IsNil) + + for _, t := range []struct { + cmd string + expected string + }{ + {cmd: "", expected: `run-app cmd-arg1`}, + {cmd: "stop", expected: "stop-app"}, + {cmd: "post-stop", expected: "post-stop-app"}, + } { + cmd, err := findCommand(info.Apps["app"], t.cmd) + c.Check(err, IsNil) + c.Check(cmd, Equals, t.expected) + } +} + +func (s *snapExecSuite) TestFindCommandInvalidCommand(c *C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, IsNil) + + _, err = findCommand(info.Apps["app"], "xxx") + c.Check(err, ErrorMatches, `cannot use "xxx" command`) +} + +func (s *snapExecSuite) TestFindCommandNoCommand(c *C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, IsNil) + + _, err = findCommand(info.Apps["nostop"], "stop") + c.Check(err, ErrorMatches, `no "stop" command found for "nostop"`) +} + +func (s *snapExecSuite) TestSnapExecAppIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + execEnv := []string{} + syscallExec = func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + execEnv = env + return nil + } + + // launch and verify its run the right way + err := snapExecApp("snapname.app", "42", "stop", []string{"arg1", "arg2"}) + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/stop-app", dirs.SnapMountDir)) + c.Check(execArgs, DeepEquals, []string{execArgv0, "arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path\n") +} + +func (s *snapExecSuite) TestSnapExecHookIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockHookYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + syscallExec = func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + return nil + } + + // launch and verify it ran correctly + err := snapExecHook("snapname", "42", "configure") + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/meta/hooks/configure", dirs.SnapMountDir)) + c.Check(execArgs, DeepEquals, []string{execArgv0}) +} + +func (s *snapExecSuite) TestSnapExecHookMissingHookIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockHookYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + err := snapExecHook("snapname", "42", "missing-hook") + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, "cannot find hook \"missing-hook\" in \"snapname\"") +} + +func (s *snapExecSuite) TestSnapExecIgnoresUnknownArgs(c *C) { + snapApp, rest, err := parseArgs([]string{"--command=shell", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, IsNil) + c.Assert(opts.Command, Equals, "shell") + c.Assert(snapApp, DeepEquals, "snapname.app") + c.Assert(rest, DeepEquals, []string{"--arg1", "arg2"}) +} + +func (s *snapExecSuite) TestSnapExecErrorsOnUnknown(c *C) { + _, _, err := parseArgs([]string{"--command=shell", "--unknown", "snapname.app", "--arg1", "arg2"}) + c.Check(err, ErrorMatches, "unknown flag `unknown'") +} + +func (s *snapExecSuite) TestSnapExecErrorsOnMissingSnapApp(c *C) { + _, _, err := parseArgs([]string{"--command=shell"}) + c.Check(err, ErrorMatches, "need the application to run as argument") +} + +func (s *snapExecSuite) TestSnapExecAppRealIntegration(c *C) { + // we need a lot of mocks + dirs.SetRootDir(c.MkDir()) + + oldOsArgs := os.Args + defer func() { os.Args = oldOsArgs }() + + os.Setenv("SNAP_REVISION", "42") + defer os.Unsetenv("SNAP_REVISION") + + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + canaryFile := filepath.Join(c.MkDir(), "canary.txt") + script := fmt.Sprintf("%s/snapname/42/run-app", dirs.SnapMountDir) + err := ioutil.WriteFile(script, []byte(fmt.Sprintf(binaryTemplate, canaryFile)), 0755) + c.Assert(err, IsNil) + + // we can not use the real syscall.execv here because it would + // replace the entire test :) + syscallExec = actuallyExec + + // run it + os.Args = []string{"snap-exec", "snapname.app", "foo", "--bar=baz", "foobar"} + err = run() + c.Assert(err, IsNil) + + output, err := ioutil.ReadFile(canaryFile) + c.Assert(err, IsNil) + c.Assert(string(output), Equals, `run-app +cmd-arg1 +foo +--bar=baz +foobar + +`) +} + +func (s *snapExecSuite) TestSnapExecHookRealIntegration(c *C) { + // we need a lot of mocks + dirs.SetRootDir(c.MkDir()) + + oldOsArgs := os.Args + defer func() { os.Args = oldOsArgs }() + + os.Setenv("SNAP_REVISION", "42") + defer os.Unsetenv("SNAP_REVISION") + + canaryFile := filepath.Join(c.MkDir(), "canary.txt") + + testSnap := snaptest.MockSnap(c, string(mockHookYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + hookPath := filepath.Join("meta", "hooks", "configure") + hookPathAndContents := []string{hookPath, fmt.Sprintf(binaryTemplate, canaryFile)} + snaptest.PopulateDir(testSnap.MountDir(), [][]string{hookPathAndContents}) + hookPath = filepath.Join(testSnap.MountDir(), hookPath) + c.Assert(os.Chmod(hookPath, 0755), IsNil) + + // we can not use the real syscall.execv here because it would + // replace the entire test :) + syscallExec = actuallyExec + + // run it + os.Args = []string{"snap-exec", "--hook=configure", "snapname"} + err := run() + c.Assert(err, IsNil) + + output, err := ioutil.ReadFile(canaryFile) + c.Assert(err, IsNil) + c.Assert(string(output), Equals, "configure\n\n") +} + +func actuallyExec(argv0 string, argv []string, env []string) error { + cmd := exec.Command(argv[0], argv[1:]...) + cmd.Env = env + output, err := cmd.CombinedOutput() + if len(output) > 0 { + return fmt.Errorf("Expected output length to be 0, it was %d", len(output)) + } + return err +} + +func (s *snapExecSuite) TestSnapExecShellIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + execEnv := []string{} + syscallExec = func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + execEnv = env + return nil + } + + // launch and verify its run the right way + err := snapExecApp("snapname.app", "42", "shell", []string{"-c", "echo foo"}) + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, "/bin/bash") + c.Check(execArgs, DeepEquals, []string{execArgv0, "-c", "echo foo"}) + c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path\n") +} diff --git a/cmd/snap/cmd_abort.go b/cmd/snap/cmd_abort.go new file mode 100644 index 00000000..46460d31 --- /dev/null +++ b/cmd/snap/cmd_abort.go @@ -0,0 +1,60 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package main + +import ( + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +type cmdAbort struct { + Positional struct { + ID changeID + } `positional-args:"yes" required:"yes"` +} + +var shortAbortHelp = i18n.G("Abort a pending change") + +var longAbortHelp = i18n.G(` +The abort command attempts to abort a change that still has pending tasks. +`) + +func init() { + addCommand("abort", + shortAbortHelp, + longAbortHelp, + func() flags.Commander { + return &cmdAbort{} + }, + nil, + []argDesc{{name: i18n.G("")}}, + ) +} + +func (x *cmdAbort) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + cli := Client() + _, err := cli.Abort(string(x.Positional.ID)) + return err +} diff --git a/cmd/snap/cmd_ack.go b/cmd/snap/cmd_ack.go new file mode 100644 index 00000000..b6e3ffca --- /dev/null +++ b/cmd/snap/cmd_ack.go @@ -0,0 +1,75 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdAck struct { + AckOptions struct { + AssertionFile flags.Filename + } `positional-args:"true" required:"true"` +} + +var shortAckHelp = i18n.G("Adds an assertion to the system") +var longAckHelp = i18n.G(` +The ack command tries to add an assertion to the system assertion database. + +The assertion may also be a newer revision of a preexisting assertion that it +will replace. + +To succeed the assertion must be valid, its signature verified with a known +public key and the assertion consistent with and its prerequisite in the +database. +`) + +func init() { + addCommand("ack", shortAckHelp, longAckHelp, func() flags.Commander { + return &cmdAck{} + }, nil, []argDesc{{ + name: i18n.G(""), + desc: i18n.G("Assertion file"), + }}) +} + +func ackFile(assertFile string) error { + assertData, err := ioutil.ReadFile(assertFile) + if err != nil { + return err + } + + return Client().Ack(assertData) +} + +func (x *cmdAck) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + if err := ackFile(string(x.AckOptions.AssertionFile)); err != nil { + return fmt.Errorf("cannot assert: %v", err) + } + return nil +} diff --git a/cmd/snap/cmd_alias.go b/cmd/snap/cmd_alias.go new file mode 100644 index 00000000..b67980c8 --- /dev/null +++ b/cmd/snap/cmd_alias.go @@ -0,0 +1,77 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdAlias struct { + Reset bool `long:"reset"` + + Positionals struct { + Snap installedSnapName `required:"yes"` + Aliases []string `required:"yes"` + } `positional-args:"true"` +} + +// TODO: implement a Completer for aliases + +var shortAliasHelp = i18n.G("Enables the given aliases") +var longAliasHelp = i18n.G(` +The alias command enables the given application aliases defined by the snap. + +Once enabled the respective application commands can be invoked just using the aliases. +`) + +func init() { + addCommand("alias", shortAliasHelp, longAliasHelp, func() flags.Commander { + return &cmdAlias{} + }, map[string]string{ + "reset": i18n.G("Reset the aliases to their default state, enabled for automatic aliases, disabled otherwise"), + }, []argDesc{ + {name: ""}, + {name: i18n.G("")}, + }) +} + +func (x *cmdAlias) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapName := string(x.Positionals.Snap) + aliases := x.Positionals.Aliases + + cli := Client() + op := cli.Alias + if x.Reset { + op = cli.ResetAliases + } + id, err := op(snapName, aliases) + if err != nil { + return err + } + + _, err = wait(cli, id) + return err +} diff --git a/cmd/snap/cmd_alias_test.go b/cmd/snap/cmd_alias_test.go new file mode 100644 index 00000000..c4deb91b --- /dev/null +++ b/cmd/snap/cmd_alias_test.go @@ -0,0 +1,99 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestAliasHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] alias [alias-OPTIONS] [] [...] + +The alias command enables the given application aliases defined by the snap. + +Once enabled the respective application commands can be invoked just using the +aliases. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message + +[alias command options] + --reset Reset the aliases to their default state, enabled for + automatic aliases, disabled otherwise +` + rest, err := Parser().ParseArgs([]string{"alias", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestAlias(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/aliases": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "alias", + "snap": "alias-snap", + "aliases": []interface{}{"alias1", "alias2"}, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"alias", "alias-snap", "alias1", "alias2"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestAliasReset(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/aliases": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "reset", + "snap": "alias-snap", + "aliases": []interface{}{"alias1", "alias2"}, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"alias", "--reset", "alias-snap", "alias1", "alias2"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} diff --git a/cmd/snap/cmd_aliases.go b/cmd/snap/cmd_aliases.go new file mode 100644 index 00000000..8fba80a6 --- /dev/null +++ b/cmd/snap/cmd_aliases.go @@ -0,0 +1,138 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + "sort" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type cmdAliases struct { + Positionals struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"true"` +} + +var shortAliasesHelp = i18n.G("Lists aliases in the system") +var longAliasesHelp = i18n.G(` +The aliases command lists all aliases available in the system and their status. + +$ snap aliases + +Lists only the aliases defined by the specified snap. + +An alias noted as undefined means it was explicitly enabled or disabled but is +not defined in the current revision of the snap; possibly temporarely (e.g +because of a revert), if not this can be cleared with snap alias --reset. +`) + +func init() { + addCommand("aliases", shortAliasesHelp, longAliasesHelp, func() flags.Commander { + return &cmdAliases{} + }, nil, nil) +} + +type aliasInfo struct { + Snap string + App string + Alias string + Status string +} + +type aliasInfos []*aliasInfo + +func (infos aliasInfos) Len() int { return len(infos) } +func (infos aliasInfos) Swap(i, j int) { infos[i], infos[j] = infos[j], infos[i] } +func (infos aliasInfos) Less(i, j int) bool { + if infos[i].Snap < infos[j].Snap { + return true + } + if infos[i].Snap == infos[j].Snap { + if infos[i].App != "" { + if infos[j].App == "" { + return true + } + if infos[i].App < infos[j].App { + return true + } + } + if infos[i].App == infos[j].App { + if infos[i].Alias < infos[j].Alias { + return true + } + } + } + return false +} + +func (x *cmdAliases) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + allStatuses, err := Client().Aliases() + if err == nil { + w := tabWriter() + fmt.Fprintln(w, i18n.G("App\tAlias\tNotes")) + defer w.Flush() + var infos aliasInfos + filterSnap := string(x.Positionals.Snap) + if filterSnap != "" { + allStatuses = map[string]map[string]client.AliasStatus{ + filterSnap: allStatuses[filterSnap], + } + } + for snapName, aliasStatuses := range allStatuses { + for alias, aliasStatus := range aliasStatuses { + infos = append(infos, &aliasInfo{ + Snap: snapName, + App: aliasStatus.App, + Alias: alias, + Status: aliasStatus.Status, + }) + } + } + sort.Sort(infos) + + for _, info := range infos { + var notes []string + app := info.App + if app == "" { + app = fmt.Sprintf("%s.???", info.Snap) + notes = append(notes, "undefined") + } + if info.Status != "" { + notes = append(notes, info.Status) + } + notesStr := strings.Join(notes, ",") + if notesStr == "" { + notesStr = "-" + } + fmt.Fprintf(w, "%s\t%s\t%s\n", app, info.Alias, notesStr) + } + } + return err +} diff --git a/cmd/snap/cmd_aliases_test.go b/cmd/snap/cmd_aliases_test.go new file mode 100644 index 00000000..584fad5a --- /dev/null +++ b/cmd/snap/cmd_aliases_test.go @@ -0,0 +1,169 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "io/ioutil" + "net/http" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestAliasesHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] aliases [] + +The aliases command lists all aliases available in the system and their status. + +$ snap aliases + +Lists only the aliases defined by the specified snap. + +An alias noted as undefined means it was explicitly enabled or disabled but is +not defined in the current revision of the snap; possibly temporarely (e.g +because of a revert), if not this can be cleared with snap alias --reset. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message +` + rest, err := Parser().ParseArgs([]string{"aliases", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestAliases(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/aliases") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": map[string]map[string]client.AliasStatus{ + "foo": { + "foo0": {App: "foo", Status: "auto"}, + "foo_reset": {App: "foo.reset", Status: ""}, + }, + "bar": { + "bar_dump": {App: "bar.dump", Status: "enabled"}, + "bar_dump.1": {App: "", Status: "disabled"}, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"aliases"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "App Alias Notes\n" + + "bar.dump bar_dump enabled\n" + + "bar.??? bar_dump.1 undefined,disabled\n" + + "foo foo0 auto\n" + + "foo.reset foo_reset -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestAliasesFilterSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/aliases") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": map[string]map[string]client.AliasStatus{ + "foo": { + "foo0": {App: "foo", Status: "auto"}, + "foo_reset": {App: "foo.reset", Status: ""}, + }, + "bar": { + "bar_dump": {App: "bar.dump", Status: "enabled"}, + "bar_dump.1": {App: "", Status: "disabled"}, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"aliases", "foo"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "App Alias Notes\n" + + "foo foo0 auto\n" + + "foo.reset foo_reset -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestAliasesNone(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/aliases") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": map[string]map[string]client.AliasStatus{}, + }) + }) + _, err := Parser().ParseArgs([]string{"aliases"}) + c.Assert(err, IsNil) + expectedStdout := "" + + "App Alias Notes\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestAliasesSorting(c *C) { + tests := []struct { + snap1 string + app1 string + alias1 string + snap2 string + app2 string + alias2 string + }{ + {"bar", "bar", "r", "baz", "baz", "z"}, + {"bar", "bar", "bar0", "bar", "bar.app", "bapp"}, + {"bar", "bar.app1", "bapp1", "bar", "bar.app2", "bapp2"}, + {"bar", "bar", "bar0", "bar", "", "bapp"}, + {"bar", "bar.app1", "appy", "bar", "", "appx"}, + {"bar", "", "bapp1", "bar", "", "bapp2"}, + {"bar", "bar.app1", "appx", "bar", "bar.app1", "appy"}, + } + + for _, test := range tests { + res := AliasInfoLess(test.snap1, test.alias1, test.app1, test.snap2, test.alias2, test.app2) + c.Check(res, Equals, true, Commentf("%v", test)) + + rres := AliasInfoLess(test.snap2, test.alias2, test.app2, test.snap1, test.alias1, test.app1) + c.Check(rres, Equals, false, Commentf("reversed %v", test)) + } + +} diff --git a/cmd/snap/cmd_auto_import.go b/cmd/snap/cmd_auto_import.go new file mode 100644 index 00000000..542ccb2e --- /dev/null +++ b/cmd/snap/cmd_auto_import.go @@ -0,0 +1,296 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package main + +import ( + "bufio" + "crypto" + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +const autoImportsName = "auto-import.assert" + +var mountInfoPath = "/proc/self/mountinfo" + +func autoImportCandidates() ([]string, error) { + var cands []string + + // see https://www.kernel.org/doc/Documentation/filesystems/proc.txt, + // sec. 3.5 + f, err := os.Open(mountInfoPath) + if err != nil { + return nil, err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + l := strings.Fields(scanner.Text()) + + // Per proc.txt:3.5, /proc//mountinfo looks like + // + // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) + // + // and (7) has zero or more elements, find the "-" separator. + i := 6 + for i < len(l) && l[i] != "-" { + i++ + } + if i+2 >= len(l) { + continue + } + + mountSrc := l[i+2] + + // skip everything that is not a device (cgroups, debugfs etc) + if !strings.HasPrefix(mountSrc, "/dev/") { + continue + } + // skip all loop devices (snaps) + if strings.HasPrefix(mountSrc, "/dev/loop") { + continue + } + + mountPoint := l[4] + cand := filepath.Join(mountPoint, autoImportsName) + if osutil.FileExists(cand) { + cands = append(cands, cand) + } + } + + return cands, scanner.Err() + +} + +func queueFile(src string) error { + // refuse huge files, this is for assertions + fi, err := os.Stat(src) + if err != nil { + return err + } + // 640kb ought be to enough for anyone + if fi.Size() > 640*1024 { + msg := fmt.Errorf("cannot queue %s, file size too big: %v", src, fi.Size()) + logger.Noticef("error: %v", msg) + return msg + } + + // ensure name is predictable, weak hash is ok + hash, _, err := osutil.FileDigest(src, crypto.SHA3_384) + if err != nil { + return err + } + + dst := filepath.Join(dirs.SnapAssertsSpoolDir, fmt.Sprintf("%s.assert", base64.URLEncoding.EncodeToString(hash))) + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + + return osutil.CopyFile(src, dst, osutil.CopyFlagOverwrite) +} + +func autoImportFromSpool() (added int, err error) { + files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir) + if os.IsNotExist(err) { + return 0, nil + } + if err != nil { + return 0, err + } + + for _, fi := range files { + cand := filepath.Join(dirs.SnapAssertsSpoolDir, fi.Name()) + if err := ackFile(cand); err != nil { + logger.Noticef("error: cannot import %s: %s", cand, err) + continue + } else { + logger.Noticef("imported %s", cand) + added++ + } + // FIXME: only remove stuff older than N days? + if err := os.Remove(cand); err != nil { + return 0, err + } + } + + return added, nil +} + +func autoImportFromAllMounts() (int, error) { + cands, err := autoImportCandidates() + if err != nil { + return 0, err + } + + added := 0 + for _, cand := range cands { + err := ackFile(cand) + // the server is not ready yet + if _, ok := err.(client.ConnectionError); ok { + logger.Noticef("queuing for later %s", cand) + if err := queueFile(cand); err != nil { + return 0, err + } + continue + } + if err != nil { + logger.Noticef("error: cannot import %s: %s", cand, err) + continue + } else { + logger.Noticef("imported %s", cand) + } + added++ + } + + return added, nil +} + +func tryMount(deviceName string) (string, error) { + tmpMountTarget, err := ioutil.TempDir("", "snapd-auto-import-mount-") + if err != nil { + err = fmt.Errorf("cannot create temporary mount point: %v", err) + logger.Noticef("error: %v", err) + return "", err + } + // udev does not provide much environment ;) + if os.Getenv("PATH") == "" { + os.Setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin") + } + // not using syscall.Mount() because we don't know the fs type in advance + cmd := exec.Command("mount", "-o", "ro", "--make-private", deviceName, tmpMountTarget) + if output, err := cmd.CombinedOutput(); err != nil { + os.Remove(tmpMountTarget) + err = fmt.Errorf("cannot mount %s: %s", deviceName, osutil.OutputErr(output, err)) + logger.Noticef("error: %v", err) + return "", err + } + + return tmpMountTarget, nil +} + +func doUmount(mp string) error { + if err := syscall.Unmount(mp, 0); err != nil { + return err + } + return os.Remove(mp) +} + +type cmdAutoImport struct { + Mount []string `long:"mount" arg-name:""` + + ForceClassic bool `long:"force-classic"` +} + +var shortAutoImportHelp = i18n.G("Inspects devices for actionable information") + +var longAutoImportHelp = i18n.G(` +The auto-import command searches available mounted devices looking for +assertions that are signed by trusted authorities, and potentially +performs system changes based on them. + +If one or more device paths are provided via --mount, these are temporariy +mounted to be inspected as well. Even in that case the command will still +consider all available mounted devices for inspection. + +Imported assertions must be made available in the auto-import.assert file +in the root of the filesystem. +`) + +func init() { + cmd := addCommand("auto-import", + shortAutoImportHelp, + longAutoImportHelp, + func() flags.Commander { + return &cmdAutoImport{} + }, map[string]string{ + "mount": i18n.G("Temporarily mount device before inspecting"), + "force-classic": i18n.G("Force import on classic systems"), + }, nil) + cmd.hidden = true +} + +func autoAddUsers() error { + cmd := cmdCreateUser{ + Known: true, Sudoer: true, + } + return cmd.Execute(nil) +} + +func (x *cmdAutoImport) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if release.OnClassic && !x.ForceClassic { + fmt.Fprintf(Stderr, "auto-import is disabled on classic\n") + return nil + } + + for _, path := range x.Mount { + // udev adds new /dev/loopX devices on the fly when a + // loop mount happens and there is no loop device left. + // + // We need to ignore these events because otherwise both + // our mount and the "mount -o loop" fight over the same + // device and we get nasty errors + if strings.HasPrefix(path, "/dev/loop") { + continue + } + + mp, err := tryMount(path) + if err != nil { + continue // Error was reported. Continue looking. + } + defer doUmount(mp) + } + + added1, err := autoImportFromSpool() + if err != nil { + return err + } + + added2, err := autoImportFromAllMounts() + if err != nil { + return err + } + + if added1+added2 > 0 { + return autoAddUsers() + } + + return nil +} diff --git a/cmd/snap/cmd_auto_import_test.go b/cmd/snap/cmd_auto_import_test.go new file mode 100644 index 00000000..7dc93f11 --- /dev/null +++ b/cmd/snap/cmd_auto_import_test.go @@ -0,0 +1,315 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +func makeMockMountInfo(c *C, content string) string { + fn := filepath.Join(c.MkDir(), "mountinfo") + err := ioutil.WriteFile(fn, []byte(content), 0644) + c.Assert(err, IsNil) + return fn +} + +func (s *SnapSuite) TestAutoImportAssertsHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + fakeAssertData := []byte("my-assertion") + + n := 0 + total := 2 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/assertions") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(postData, DeepEquals, fakeAssertData) + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) + n++ + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/create-user") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`) + + fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`) + n++ + default: + c.Fatalf("unexpected request: %v (expected %d got %d)", r, total, n) + } + + }) + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmt := ` +24 0 8:18 / %s rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + l, err := logger.NewConsoleLog(s.stderr, 0) + c.Assert(err, IsNil) + logger.SetLogger(l) + + rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, `created user "foo"`+"\n") + // matches because we may get a: + // "WARNING: cannot create syslog logger\n" + // in the output + c.Check(s.Stderr(), Matches, fmt.Sprintf("(?ms).*imported %s\n", fakeAssertsFn)) + c.Check(n, Equals, total) +} + +func (s *SnapSuite) TestAutoImportAssertsNotImportedFromLoop(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + fakeAssertData := []byte("bad-assertion") + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + // assertion is ignored, nothing is posted to this endpoint + panic("not reached") + }) + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmtWithLoop := ` +24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/loop1 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmtWithLoop, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestAutoImportCandidatesHappy(c *C) { + dirs := make([]string, 4) + args := make([]interface{}, len(dirs)) + files := make([]string, len(dirs)) + for i := range dirs { + dirs[i] = c.MkDir() + args[i] = dirs[i] + files[i] = filepath.Join(dirs[i], "auto-import.assert") + err := ioutil.WriteFile(files[i], nil, 0644) + c.Assert(err, IsNil) + } + + mockMountInfoFmtWithLoop := ` +too short +24 0 8:18 / %[1]s rw,relatime foo ext3 /dev/meep2 no,separator +24 0 8:18 / %[2]s rw,relatime - ext3 /dev/meep2 rw,errors=remount-ro,data=ordered +24 0 8:18 / %[3]s rw,relatime opt:1 - ext4 /dev/meep3 rw,errors=remount-ro,data=ordered +24 0 8:18 / %[4]s rw,relatime opt:1 opt:2 - ext2 /dev/meep1 rw,errors=remount-ro,data=ordered +` + + content := fmt.Sprintf(mockMountInfoFmtWithLoop, args...) + restore := snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + l, err := snap.AutoImportCandidates() + c.Check(err, IsNil) + c.Check(l, DeepEquals, files[1:len(files)]) +} + +func (s *SnapSuite) TestAutoImportAssertsHappyNotOnClassic(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + fakeAssertData := []byte("my-assertion") + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Errorf("auto-import on classic is disabled, but something tried to do a %q with %s", r.Method, r.URL.Path) + }) + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmt := ` +24 0 8:18 / %s rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "auto-import is disabled on classic\n") +} + +func (s *SnapSuite) TestAutoImportIntoSpool(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("") + + l, err := logger.NewConsoleLog(s.stderr, 0) + c.Assert(err, IsNil) + logger.SetLogger(l) + + fakeAssertData := []byte("good-assertion") + + // ensure we can not connect + snap.ClientConfig.BaseURL = "can-not-connect-to-this-url" + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err = ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmt := ` +24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/sc1 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "") + // matches because we may get a: + // "WARNING: cannot create syslog logger\n" + // in the output + c.Check(s.Stderr(), Matches, "(?ms).*queuing for later.*\n") + + files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir) + c.Assert(err, IsNil) + c.Check(files, HasLen, 1) + c.Check(files[0].Name(), Equals, "iOkaeet50rajLvL-0Qsf2ELrTdn3XIXRIBlDewcK02zwRi3_TJlUOTl9AaiDXmDn.assert") +} + +func (s *SnapSuite) TestAutoImportFromSpoolHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + fakeAssertData := []byte("my-assertion") + + n := 0 + total := 2 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/assertions") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(postData, DeepEquals, fakeAssertData) + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) + n++ + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/create-user") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`) + + fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`) + n++ + default: + c.Fatalf("unexpected request: %v (expected %d got %d)", r, total, n) + } + + }) + + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("") + + fakeAssertsFn := filepath.Join(dirs.SnapAssertsSpoolDir, "1234343") + err := os.MkdirAll(filepath.Dir(fakeAssertsFn), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + l, err := logger.NewConsoleLog(s.stderr, 0) + c.Assert(err, IsNil) + logger.SetLogger(l) + + rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, `created user "foo"`+"\n") + // matches because we may get a: + // "WARNING: cannot create syslog logger\n" + // in the output + c.Check(s.Stderr(), Matches, fmt.Sprintf("(?ms).*imported %s\n", fakeAssertsFn)) + c.Check(n, Equals, total) + + c.Check(osutil.FileExists(fakeAssertsFn), Equals, false) +} + +func (s *SnapSuite) TestAutoImportIntoSpoolUnhappyTooBig(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("") + + l, err := logger.NewConsoleLog(s.stderr, 0) + c.Assert(err, IsNil) + logger.SetLogger(l) + + // fake data is bigger than the default assertion limit + fakeAssertData := make([]byte, 641*1024) + + // ensure we can not connect + snap.ClientConfig.BaseURL = "can-not-connect-to-this-url" + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err = ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmt := ` +24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/sc1 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + _, err = snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, ErrorMatches, "cannot queue .*, file size too big: 656384") +} diff --git a/cmd/snap/cmd_booted.go b/cmd/snap/cmd_booted.go new file mode 100644 index 00000000..db51cc20 --- /dev/null +++ b/cmd/snap/cmd_booted.go @@ -0,0 +1,50 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" +) + +type cmdBooted struct{} + +func init() { + cmd := addCommand("booted", + "internal", + "internal", + func() flags.Commander { + return &cmdBooted{} + }, nil, nil) + cmd.hidden = true +} + +// WARNING: do not remove this command, older systems may still have +// a systemd snapd.firstboot.service job in /etc/systemd/system +// that we did not cleanup. so we need this dummy command or +// those units will start failing. +func (x *cmdBooted) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + fmt.Fprintf(Stderr, "booted command is deprecated") + return nil +} diff --git a/cmd/snap/cmd_buy.go b/cmd/snap/cmd_buy.go new file mode 100644 index 00000000..b9d847b0 --- /dev/null +++ b/cmd/snap/cmd_buy.go @@ -0,0 +1,135 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/store" + + "github.com/jessevdk/go-flags" +) + +var shortBuyHelp = i18n.G("Buys a snap") +var longBuyHelp = i18n.G(` +The buy command buys a snap from the store. +`) + +type cmdBuy struct { + Positional struct { + SnapName remoteSnapName + } `positional-args:"yes" required:"yes"` +} + +func init() { + addCommand("buy", shortBuyHelp, longBuyHelp, func() flags.Commander { + return &cmdBuy{} + }, map[string]string{}, []argDesc{{ + name: "", + desc: i18n.G("Snap name"), + }}) +} + +func (x *cmdBuy) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + return buySnap(string(x.Positional.SnapName)) +} + +func buySnap(snapName string) error { + cli := Client() + + user := cli.LoggedInUser() + if user == nil { + return fmt.Errorf(i18n.G("You need to be logged in to purchase software. Please run 'snap login' and try again.")) + } + + if strings.ContainsAny(snapName, ":*") { + return fmt.Errorf(i18n.G("cannot buy snap: invalid characters in name")) + } + + snap, resultInfo, err := cli.FindOne(snapName) + if err != nil { + return err + } + + opts := &store.BuyOptions{ + SnapID: snap.ID, + Currency: resultInfo.SuggestedCurrency, + } + + opts.Price, opts.Currency, err = getPrice(snap.Prices, opts.Currency) + if err != nil { + return fmt.Errorf(i18n.G("cannot buy snap: %v"), err) + } + + if snap.Status == "available" { + return fmt.Errorf(i18n.G("cannot buy snap: it has already been bought")) + } + + err = cli.ReadyToBuy() + if err != nil { + if e, ok := err.(*client.Error); ok { + switch e.Kind { + case client.ErrorKindNoPaymentMethods: + return fmt.Errorf(i18n.G(`You do not have a payment method associated with your account, visit https://my.ubuntu.com/payment/edit to add one. +Once completed, return here and run 'snap buy %s' again.`), snap.Name) + case client.ErrorKindTermsNotAccepted: + return fmt.Errorf(i18n.G(`Please visit https://my.ubuntu.com/payment/edit to agree to the latest terms and conditions. +Once completed, return here and run 'snap buy %s' again.`), snap.Name) + } + } + return err + } + + // TRANSLATORS: %q, %q and %s are the snap name, developer, and price. Please wrap the translation at 80 characters. + fmt.Fprintf(Stdout, i18n.G(`Please re-enter your Ubuntu One password to purchase %q from %q +for %s. Press ctrl-c to cancel.`), snap.Name, snap.Developer, formatPrice(opts.Price, opts.Currency)) + fmt.Fprint(Stdout, "\n") + + err = requestLogin(user.Email) + if err != nil { + return err + } + + _, err = cli.Buy(opts) + if err != nil { + if e, ok := err.(*client.Error); ok { + switch e.Kind { + case client.ErrorKindPaymentDeclined: + return fmt.Errorf(i18n.G(`Sorry, your payment method has been declined by the issuer. Please review your +payment details at https://my.ubuntu.com/payment/edit and try again.`)) + } + } + return err + } + + // TRANSLATORS: %q and %s are the same snap name. Please wrap the translation at 80 characters. + fmt.Fprintf(Stdout, i18n.G(`Thanks for purchasing %q. You may now install it on any of your devices +with 'snap install %s'.`), snapName, snapName) + fmt.Fprint(Stdout, "\n") + + return nil +} diff --git a/cmd/snap/cmd_buy_test.go b/cmd/snap/cmd_buy_test.go new file mode 100644 index 00000000..36fa6334 --- /dev/null +++ b/cmd/snap/cmd_buy_test.go @@ -0,0 +1,448 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "encoding/json" + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +type BuySnapSuite struct { + BaseSnapSuite +} + +var _ = check.Suite(&BuySnapSuite{}) + +type expectedURL struct { + Body string + Checker func(r *http.Request) + + callCount int +} + +type expectedMethod map[string]*expectedURL + +type expectedMethods map[string]*expectedMethod + +type buyTestMockSnapServer struct { + ExpectedMethods expectedMethods + + Checker *check.C +} + +func (s *buyTestMockSnapServer) serveHttp(w http.ResponseWriter, r *http.Request) { + method := s.ExpectedMethods[r.Method] + if method == nil || len(*method) == 0 { + s.Checker.Fatalf("unexpected HTTP method %s", r.Method) + } + + url := (*method)[r.URL.Path] + if url == nil { + s.Checker.Fatalf("unexpected URL %q", r.URL.Path) + } + + if url.Checker != nil { + url.Checker(r) + } + fmt.Fprintln(w, url.Body) + url.callCount++ +} + +func (s *buyTestMockSnapServer) checkCounts() { + for _, method := range s.ExpectedMethods { + for _, url := range *method { + s.Checker.Check(url.callCount, check.Equals, 1) + } + } +} + +func (s *BuySnapSuite) SetUpTest(c *check.C) { + s.BaseSnapSuite.SetUpTest(c) + s.Login(c) +} + +func (s *BuySnapSuite) TearDownTest(c *check.C) { + s.Logout(c) + s.BaseSnapSuite.TearDownTest(c) +} + +func (s *BuySnapSuite) TestBuyHelp(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"buy"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "the required argument `` was not provided") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *BuySnapSuite) TestBuyInvalidCharacters(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"buy", "a:b"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") + + _, err = snap.Parser().ParseArgs([]string{"buy", "c*d"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +const buyFreeSnapFailsFindJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *BuySnapSuite) TestBuyFreeSnapFails(c *check.C) { + mockServer := &buyTestMockSnapServer{ + ExpectedMethods: expectedMethods{ + "GET": &expectedMethod{ + "/v2/find": &expectedURL{ + Body: buyFreeSnapFailsFindJson, + }, + }, + }, + Checker: c, + } + defer mockServer.checkCounts() + s.RedirectClientToTestServer(mockServer.serveHttp) + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "cannot buy snap: snap is free") + c.Assert(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +const buySnapFindJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "priced", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10", + "prices": {"USD": 3.99, "GBP": 2.99} + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func buySnapFindURL(c *check.C) *expectedURL { + return &expectedURL{ + Body: buySnapFindJson, + Checker: func(r *http.Request) { + c.Check(r.URL.Query().Get("name"), check.Equals, "hello") + }, + } +} + +const buyReadyJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": true, + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func buyReady(c *check.C) *expectedURL { + return &expectedURL{ + Body: buyReadyJson, + } +} + +const buySnapJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": { + "state": "Complete" + }, + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +const loginJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": { + "id": 1, + "username": "username", + "email": "hello@mail.com", + "macaroon": "1234abcd", + "discharges": ["a", "b", "c"] + }, + "sources": [ + "store" + ] +} +` + +func (s *BuySnapSuite) TestBuySnapSuccess(c *check.C) { + mockServer := &buyTestMockSnapServer{ + ExpectedMethods: expectedMethods{ + "GET": &expectedMethod{ + "/v2/find": buySnapFindURL(c), + "/v2/buy/ready": buyReady(c), + }, + "POST": &expectedMethod{ + "/v2/login": &expectedURL{ + Body: loginJson, + }, + "/v2/buy": &expectedURL{ + Body: buySnapJson, + Checker: func(r *http.Request) { + var postData struct { + SnapID string `json:"snap-id"` + Price float64 `json:"price"` + Currency string `json:"currency"` + } + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&postData) + c.Assert(err, check.IsNil) + + c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6") + c.Check(postData.Price, check.Equals, 2.99) + c.Check(postData.Currency, check.Equals, "GBP") + }, + }, + }, + }, + Checker: c, + } + defer mockServer.checkCounts() + s.RedirectClientToTestServer(mockServer.serveHttp) + + // Confirm the purchase. + s.password = "the password" + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Check(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical" +for 2.99GBP. Press ctrl-c to cancel. +Password of "hello@mail.com": +Thanks for purchasing "hello". You may now install it on any of your devices +with 'snap install hello'. +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const buySnapPaymentDeclinedJson = ` +{ + "type": "error", + "result": { + "message": "payment declined", + "kind": "payment-declined" + }, + "status-code": 400 +} +` + +func (s *BuySnapSuite) TestBuySnapPaymentDeclined(c *check.C) { + mockServer := &buyTestMockSnapServer{ + ExpectedMethods: expectedMethods{ + "GET": &expectedMethod{ + "/v2/find": buySnapFindURL(c), + "/v2/buy/ready": buyReady(c), + }, + "POST": &expectedMethod{ + "/v2/login": &expectedURL{ + Body: loginJson, + }, + "/v2/buy": &expectedURL{ + Body: buySnapPaymentDeclinedJson, + Checker: func(r *http.Request) { + var postData struct { + SnapID string `json:"snap-id"` + Price float64 `json:"price"` + Currency string `json:"currency"` + } + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&postData) + c.Assert(err, check.IsNil) + + c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6") + c.Check(postData.Price, check.Equals, 2.99) + c.Check(postData.Currency, check.Equals, "GBP") + }, + }, + }, + }, + Checker: c, + } + defer mockServer.checkCounts() + s.RedirectClientToTestServer(mockServer.serveHttp) + + // Confirm the purchase. + s.password = "the password" + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `Sorry, your payment method has been declined by the issuer. Please review your +payment details at https://my.ubuntu.com/payment/edit and try again.`) + c.Check(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical" +for 2.99GBP. Press ctrl-c to cancel. +Password of "hello@mail.com": +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const readyToBuyNoPaymentMethodJson = ` +{ + "type": "error", + "result": { + "message": "no payment methods", + "kind": "no-payment-methods" + }, + "status-code": 400 +}` + +func (s *BuySnapSuite) TestBuySnapFailsNoPaymentMethod(c *check.C) { + mockServer := &buyTestMockSnapServer{ + ExpectedMethods: expectedMethods{ + "GET": &expectedMethod{ + "/v2/find": buySnapFindURL(c), + "/v2/buy/ready": &expectedURL{ + Body: readyToBuyNoPaymentMethodJson, + }, + }, + }, + Checker: c, + } + defer mockServer.checkCounts() + s.RedirectClientToTestServer(mockServer.serveHttp) + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `You do not have a payment method associated with your account, visit https://my.ubuntu.com/payment/edit to add one. +Once completed, return here and run 'snap buy hello' again.`) + c.Check(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +const readyToBuyNotAcceptedTermsJson = ` +{ + "type": "error", + "result": { + "message": "terms of service not accepted", + "kind": "terms-not-accepted" + }, + "status-code": 400 +}` + +func (s *BuySnapSuite) TestBuySnapFailsNotAcceptedTerms(c *check.C) { + mockServer := &buyTestMockSnapServer{ + ExpectedMethods: expectedMethods{ + "GET": &expectedMethod{ + "/v2/find": buySnapFindURL(c), + "/v2/buy/ready": &expectedURL{ + Body: readyToBuyNotAcceptedTermsJson, + }, + }, + }, + Checker: c, + } + defer mockServer.checkCounts() + s.RedirectClientToTestServer(mockServer.serveHttp) + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `Please visit https://my.ubuntu.com/payment/edit to agree to the latest terms and conditions. +Once completed, return here and run 'snap buy hello' again.`) + c.Check(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *BuySnapSuite) TestBuyFailsWithoutLogin(c *check.C) { + // We don't login here + s.Logout(c) + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Check(err, check.NotNil) + c.Check(err.Error(), check.Equals, "You need to be logged in to purchase software. Please run 'snap login' and try again.") + c.Check(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_changes.go b/cmd/snap/cmd_changes.go new file mode 100644 index 00000000..220c27e1 --- /dev/null +++ b/cmd/snap/cmd_changes.go @@ -0,0 +1,159 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + "regexp" + "sort" + "time" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortChangesHelp = i18n.G("List system changes") +var shortChangeHelp = i18n.G("List a change's tasks") +var longChangesHelp = i18n.G(` +The changes command displays a summary of the recent system changes performed.`) +var longChangeHelp = i18n.G(` +The change command displays a summary of tasks associated to an individual change.`) + +type cmdChanges struct { + Positional struct { + Snap string `positional-arg-name:""` + } `positional-args:"yes"` +} + +type cmdChange struct { + Positional struct { + ID changeID `positional-arg-name:"" required:"yes"` + } `positional-args:"yes"` +} + +func init() { + addCommand("changes", shortChangesHelp, longChangesHelp, func() flags.Commander { return &cmdChanges{} }, nil, nil) + addCommand("change", shortChangeHelp, longChangeHelp, func() flags.Commander { return &cmdChange{} }, nil, nil) +} + +type changesByTime []*client.Change + +func (s changesByTime) Len() int { return len(s) } +func (s changesByTime) Less(i, j int) bool { return s[i].SpawnTime.Before(s[j].SpawnTime) } +func (s changesByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +var allDigits = regexp.MustCompile(`^[0-9]+$`).MatchString + +func (c *cmdChanges) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if allDigits(c.Positional.Snap) { + // TRANSLATORS: the %s is the argument given by the user to "snap changes" + return fmt.Errorf(i18n.G(`"snap changes" command expects a snap name, try: "snap change %s"`), c.Positional.Snap) + } + + if c.Positional.Snap == "everything" { + fmt.Fprintln(Stdout, i18n.G("Yes, yes it does.")) + return nil + } + + opts := client.ChangesOptions{ + SnapName: c.Positional.Snap, + Selector: client.ChangesAll, + } + + cli := Client() + changes, err := cli.Changes(&opts) + if err != nil { + return err + } + + if len(changes) == 0 { + return fmt.Errorf(i18n.G("no changes found")) + } + + sort.Sort(changesByTime(changes)) + + w := tabWriter() + + fmt.Fprintf(w, i18n.G("ID\tStatus\tSpawn\tReady\tSummary\n")) + for _, chg := range changes { + spawnTime := chg.SpawnTime.UTC().Format(time.RFC3339) + readyTime := chg.ReadyTime.UTC().Format(time.RFC3339) + if chg.ReadyTime.IsZero() { + readyTime = "-" + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", chg.ID, chg.Status, spawnTime, readyTime, chg.Summary) + } + + w.Flush() + fmt.Fprintln(Stdout) + + return nil +} + +func (c *cmdChange) Execute([]string) error { + cli := Client() + chg, err := cli.Change(string(c.Positional.ID)) + if err != nil { + return err + } + + w := tabWriter() + + fmt.Fprintf(w, i18n.G("Status\tSpawn\tReady\tSummary\n")) + for _, t := range chg.Tasks { + spawnTime := t.SpawnTime.UTC().Format(time.RFC3339) + readyTime := t.ReadyTime.UTC().Format(time.RFC3339) + if t.ReadyTime.IsZero() { + readyTime = "-" + } + summary := t.Summary + if t.Status == "Doing" && t.Progress.Total > 1 { + summary = fmt.Sprintf("%s (%.2f%%)", summary, float64(t.Progress.Done)/float64(t.Progress.Total)*100.0) + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", t.Status, spawnTime, readyTime, summary) + } + + w.Flush() + + for _, t := range chg.Tasks { + if len(t.Log) == 0 { + continue + } + fmt.Fprintln(Stdout) + fmt.Fprintln(Stdout, line) + fmt.Fprintln(Stdout, t.Summary) + fmt.Fprintln(Stdout) + for _, line := range t.Log { + fmt.Fprintln(Stdout, line) + } + } + + fmt.Fprintln(Stdout) + + return nil +} + +const line = "......................................................................" diff --git a/cmd/snap/cmd_changes_test.go b/cmd/snap/cmd_changes_test.go new file mode 100644 index 00000000..f044ef7e --- /dev/null +++ b/cmd/snap/cmd_changes_test.go @@ -0,0 +1,97 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +var mockChangeJSON = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] +}}` + +func (s *SnapSuite) TestChangeSimple(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, mockChangeJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"change", "42"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?ms)Status +Spawn +Ready +Summary +Do +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary +`) + c.Check(s.Stderr(), check.Equals, "") +} + +var mockChangeInProgressJSON = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Doing", "progress": {"done": 50, "total": 100}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] +}}` + +func (s *SnapSuite) TestChangeProgress(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, mockChangeInProgressJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"change", "42"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?ms)Status +Spawn +Ready +Summary +Doing +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary \(50.00%\) +`) + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_connect.go b/cmd/snap/cmd_connect.go new file mode 100644 index 00000000..68b0c106 --- /dev/null +++ b/cmd/snap/cmd_connect.go @@ -0,0 +1,85 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdConnect struct { + Positionals struct { + PlugSpec SnapAndName `required:"yes"` + SlotSpec SnapAndName + } `positional-args:"true"` +} + +var shortConnectHelp = i18n.G("Connects a plug to a slot") +var longConnectHelp = i18n.G(` +The connect command connects a plug to a slot. +It may be called in the following ways: + +$ snap connect : : + +Connects the provided plug to the given slot. + +$ snap connect : + +Connects the specific plug to the only slot in the provided snap that matches +the connected interface. If more than one potential slot exists, the command +fails. + +$ snap connect : + +Connects the provided plug to the slot in the core snap with a name matching +the plug name. +`) + +func init() { + addCommand("connect", shortConnectHelp, longConnectHelp, func() flags.Commander { + return &cmdConnect{} + }, nil, []argDesc{ + {name: i18n.G(":")}, + {name: i18n.G(":")}, + }) +} + +func (x *cmdConnect) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + // snap connect [:] + if x.Positionals.PlugSpec.Snap != "" && x.Positionals.PlugSpec.Name == "" { + // Move the value of .Snap to .Name and keep .Snap empty + x.Positionals.PlugSpec.Name = x.Positionals.PlugSpec.Snap + x.Positionals.PlugSpec.Snap = "" + } + + cli := Client() + id, err := cli.Connect(x.Positionals.PlugSpec.Snap, x.Positionals.PlugSpec.Name, x.Positionals.SlotSpec.Snap, x.Positionals.SlotSpec.Name) + if err != nil { + return err + } + + _, err = wait(cli, id) + return err +} diff --git a/cmd/snap/cmd_connect_test.go b/cmd/snap/cmd_connect_test.go new file mode 100644 index 00000000..fe33ba64 --- /dev/null +++ b/cmd/snap/cmd_connect_test.go @@ -0,0 +1,194 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestConnectHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] connect [:] [:] + +The connect command connects a plug to a slot. +It may be called in the following ways: + +$ snap connect : : + +Connects the provided plug to the given slot. + +$ snap connect : + +Connects the specific plug to the only slot in the provided snap that matches +the connected interface. If more than one potential slot exists, the command +fails. + +$ snap connect : + +Connects the provided plug to the slot in the core snap with a name matching +the plug name. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message +` + rest, err := Parser().ParseArgs([]string{"connect", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestConnectExplicitEverything(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "connect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"connect", "producer:plug", "consumer:slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestConnectExplicitPlugImplicitSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "connect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"connect", "producer:plug", "consumer"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestConnectImplicitPlugExplicitSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "connect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"connect", "plug", "consumer:slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestConnectImplicitPlugImplicitSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "connect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"connect", "plug", "consumer"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} diff --git a/cmd/snap/cmd_create_key.go b/cmd/snap/cmd_create_key.go new file mode 100644 index 00000000..63ae5d27 --- /dev/null +++ b/cmd/snap/cmd_create_key.go @@ -0,0 +1,86 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "errors" + "fmt" + + "github.com/jessevdk/go-flags" + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" +) + +type cmdCreateKey struct { + Positional struct { + KeyName string + } `positional-args:"true"` +} + +func init() { + cmd := addCommand("create-key", + i18n.G("Create cryptographic key pair"), + i18n.G("Create a cryptographic key pair that can be used for signing assertions."), + func() flags.Commander { + return &cmdCreateKey{} + }, nil, []argDesc{{ + name: i18n.G(""), + desc: i18n.G("Name of key to create; defaults to 'default'"), + }}) + cmd.hidden = true +} + +func (x *cmdCreateKey) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + keyName := x.Positional.KeyName + if keyName == "" { + keyName = "default" + } + if !asserts.IsValidAccountKeyName(keyName) { + return fmt.Errorf(i18n.G("key name %q is not valid; only ASCII letters, digits, and hyphens are allowed"), keyName) + } + + fmt.Fprint(Stdout, i18n.G("Passphrase: ")) + passphrase, err := terminal.ReadPassword(0) + fmt.Fprint(Stdout, "\n") + if err != nil { + return err + } + fmt.Fprint(Stdout, i18n.G("Confirm passphrase: ")) + confirmPassphrase, err := terminal.ReadPassword(0) + fmt.Fprint(Stdout, "\n") + if err != nil { + return err + } + if string(passphrase) != string(confirmPassphrase) { + return errors.New("passphrases do not match") + } + if err != nil { + return err + } + + manager := asserts.NewGPGKeypairManager() + return manager.Generate(string(passphrase), keyName) +} diff --git a/cmd/snap/cmd_create_key_test.go b/cmd/snap/cmd_create_key_test.go new file mode 100644 index 00000000..42089097 --- /dev/null +++ b/cmd/snap/cmd_create_key_test.go @@ -0,0 +1,34 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestCreateKeyInvalidCharacters(c *C) { + _, err := snap.Parser().ParseArgs([]string{"create-key", "a b"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "key name \"a b\" is not valid; only ASCII letters, digits, and hyphens are allowed") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_create_user.go b/cmd/snap/cmd_create_user.go new file mode 100644 index 00000000..17a6318d --- /dev/null +++ b/cmd/snap/cmd_create_user.go @@ -0,0 +1,115 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortCreateUserHelp = i18n.G("Creates a local system user") +var longCreateUserHelp = i18n.G(` +The create-user command creates a local system user with the username and SSH +keys registered on the store account identified by the provided email address. + +An account can be setup at https://login.ubuntu.com. +`) + +type cmdCreateUser struct { + Positional struct { + Email string + } `positional-args:"yes"` + + JSON bool `long:"json"` + Sudoer bool `long:"sudoer"` + Known bool `long:"known"` + ForceManaged bool `long:"force-managed"` +} + +func init() { + cmd := addCommand("create-user", shortCreateUserHelp, longCreateUserHelp, func() flags.Commander { return &cmdCreateUser{} }, + map[string]string{ + "json": i18n.G("Output results in JSON format"), + "sudoer": i18n.G("Grant sudo access to the created user"), + "known": i18n.G("Use known assertions for user creation"), + "force-managed": i18n.G("Force adding the user, even if the device is already managed"), + }, []argDesc{{ + // TRANSLATORS: noun + name: i18n.G(""), + // TRANSLATORS: note users on login.ubuntu.com can have multiple email addresses + desc: i18n.G("An email of a user on login.ubuntu.com"), + }}) + cmd.hidden = true +} + +func (x *cmdCreateUser) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + cli := Client() + + options := client.CreateUserOptions{ + Email: x.Positional.Email, + Sudoer: x.Sudoer, + Known: x.Known, + ForceManaged: x.ForceManaged, + } + + var results []*client.CreateUserResult + var result *client.CreateUserResult + var err error + + if options.Email == "" && options.Known { + results, err = cli.CreateUsers([]*client.CreateUserOptions{&options}) + } else { + result, err = cli.CreateUser(&options) + if err == nil { + results = append(results, result) + } + } + + createErr := err + + // Print results regardless of error because some users may have been created. + if x.JSON { + var data []byte + if result != nil { + data, err = json.Marshal(result) + } else if len(results) > 0 { + data, err = json.Marshal(results) + } + if err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", data) + } else { + for _, result := range results { + fmt.Fprintf(Stdout, i18n.G("created user %q\n"), result.Username) + } + } + + return createErr +} diff --git a/cmd/snap/cmd_create_user_test.go b/cmd/snap/cmd_create_user_test.go new file mode 100644 index 00000000..50726570 --- /dev/null +++ b/cmd/snap/cmd_create_user_test.go @@ -0,0 +1,150 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "encoding/json" + "fmt" + "net/http" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +func makeCreateUserChecker(c *check.C, n *int, email string, sudoer, known bool) func(w http.ResponseWriter, r *http.Request) { + f := func(w http.ResponseWriter, r *http.Request) { + switch *n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/create-user") + var gotBody map[string]interface{} + dec := json.NewDecoder(r.Body) + err := dec.Decode(&gotBody) + c.Assert(err, check.IsNil) + + wantBody := make(map[string]interface{}) + if email != "" { + wantBody["email"] = "one@email.com" + } + if sudoer { + wantBody["sudoer"] = true + } + if known { + wantBody["known"] = true + } + c.Check(gotBody, check.DeepEquals, wantBody) + + if email == "" { + fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "karl", "ssh-keys": ["a","b"]}]}`) + } else { + fmt.Fprintln(w, `{"type": "sync", "result": {"username": "karl", "ssh-keys": ["a","b"]}}`) + } + default: + c.Fatalf("got too many requests (now on %d)", *n+1) + } + + *n++ + } + return f +} + +func (s *SnapSuite) TestCreateUserNoSudoer(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, false)) + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "one@email.com"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) + c.Assert(s.Stdout(), check.Equals, `created user "karl"`+"\n") + c.Assert(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestCreateUserSudoer(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", true, false)) + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "--sudoer", "one@email.com"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) + c.Assert(s.Stdout(), check.Equals, `created user "karl"`+"\n") + c.Assert(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestCreateUserJSON(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, false)) + + expectedResponse := &client.CreateUserResult{ + Username: "karl", + SSHKeys: []string{"a", "b"}, + } + actualResponse := &client.CreateUserResult{} + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "--json", "one@email.com"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) + json.Unmarshal(s.stdout.Bytes(), actualResponse) + c.Assert(actualResponse, check.DeepEquals, expectedResponse) + c.Assert(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestCreateUserNoEmailJSON(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "", false, true)) + + var expectedResponse = []*client.CreateUserResult{{ + Username: "karl", + SSHKeys: []string{"a", "b"}, + }} + var actualResponse []*client.CreateUserResult + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "--json", "--known"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) + json.Unmarshal(s.stdout.Bytes(), &actualResponse) + c.Assert(actualResponse, check.DeepEquals, expectedResponse) + c.Assert(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestCreateUserKnown(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, true)) + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "--known", "one@email.com"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestCreateUserKnownNoEmail(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "", false, true)) + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "--known"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) +} diff --git a/cmd/snap/cmd_delete_key.go b/cmd/snap/cmd_delete_key.go new file mode 100644 index 00000000..2f8757bc --- /dev/null +++ b/cmd/snap/cmd_delete_key.go @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" +) + +type cmdDeleteKey struct { + Positional struct { + KeyName keyName + } `positional-args:"true" required:"true"` +} + +func init() { + cmd := addCommand("delete-key", + i18n.G("Delete cryptographic key pair"), + i18n.G("Delete the local cryptographic key pair with the given name."), + func() flags.Commander { + return &cmdDeleteKey{} + }, nil, []argDesc{{ + name: i18n.G(""), + desc: i18n.G("Name of key to delete"), + }}) + cmd.hidden = true +} + +func (x *cmdDeleteKey) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + manager := asserts.NewGPGKeypairManager() + return manager.Delete(string(x.Positional.KeyName)) +} diff --git a/cmd/snap/cmd_delete_key_test.go b/cmd/snap/cmd_delete_key_test.go new file mode 100644 index 00000000..65b83dc3 --- /dev/null +++ b/cmd/snap/cmd_delete_key_test.go @@ -0,0 +1,64 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "encoding/json" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapKeysSuite) TestDeleteKeyRequiresName(c *C) { + _, err := snap.Parser().ParseArgs([]string{"delete-key"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "the required argument `` was not provided") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestDeleteKeyNonexistent(c *C) { + _, err := snap.Parser().ParseArgs([]string{"delete-key", "nonexistent"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "cannot find key named \"nonexistent\" in GPG keyring") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestDeleteKey(c *C) { + rest, err := snap.Parser().ParseArgs([]string{"delete-key", "another"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") + _, err = snap.Parser().ParseArgs([]string{"keys", "--json"}) + c.Assert(err, IsNil) + expectedResponse := []snap.Key{ + { + Name: "default", + Sha3_384: "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ", + }, + } + var obtainedResponse []snap.Key + json.Unmarshal(s.stdout.Bytes(), &obtainedResponse) + c.Check(obtainedResponse, DeepEquals, expectedResponse) + c.Check(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_disconnect.go b/cmd/snap/cmd_disconnect.go new file mode 100644 index 00000000..1cab22b4 --- /dev/null +++ b/cmd/snap/cmd_disconnect.go @@ -0,0 +1,84 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdDisconnect struct { + Positionals struct { + Offer SnapAndName `required:"true"` + Use SnapAndName + } `positional-args:"true"` +} + +var shortDisconnectHelp = i18n.G("Disconnects a plug from a slot") +var longDisconnectHelp = i18n.G(` +The disconnect command disconnects a plug from a slot. +It may be called in the following ways: + +$ snap disconnect : : + +Disconnects the specific plug from the specific slot. + +$ snap disconnect : + +Disconnects everything from the provided plug or slot. +The snap name may be omitted for the core snap. +`) + +func init() { + addCommand("disconnect", shortDisconnectHelp, longDisconnectHelp, func() flags.Commander { + return &cmdDisconnect{} + }, nil, []argDesc{ + {name: i18n.G(":")}, + {name: i18n.G(":")}, + }) +} + +func (x *cmdDisconnect) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + // snap disconnect : + // snap disconnect + if x.Positionals.Use.Snap == "" && x.Positionals.Use.Name == "" { + // Swap Offer and Use around + x.Positionals.Offer, x.Positionals.Use = x.Positionals.Use, x.Positionals.Offer + } + if x.Positionals.Use.Name == "" { + return fmt.Errorf("please provide the plug or slot name to disconnect from snap %q", x.Positionals.Use.Snap) + } + + cli := Client() + id, err := cli.Disconnect(x.Positionals.Offer.Snap, x.Positionals.Offer.Name, x.Positionals.Use.Snap, x.Positionals.Use.Name) + if err != nil { + return err + } + + _, err = wait(cli, id) + return err +} diff --git a/cmd/snap/cmd_disconnect_test.go b/cmd/snap/cmd_disconnect_test.go new file mode 100644 index 00000000..145bb375 --- /dev/null +++ b/cmd/snap/cmd_disconnect_test.go @@ -0,0 +1,172 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestDisconnectHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] disconnect [:] [:] + +The disconnect command disconnects a plug from a slot. +It may be called in the following ways: + +$ snap disconnect : : + +Disconnects the specific plug from the specific slot. + +$ snap disconnect : + +Disconnects everything from the provided plug or slot. +The snap name may be omitted for the core snap. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message +` + rest, err := Parser().ParseArgs([]string{"disconnect", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestDisconnectExplicitEverything(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "disconnect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"disconnect", "producer:plug", "consumer:slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDisconnectEverythingFromSpecificSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "disconnect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "", + "plug": "", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"disconnect", "consumer:slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDisconnectEverythingFromSpecificSnapPlugOrSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "disconnect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "", + "plug": "", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "plug-or-slot", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"disconnect", "consumer:plug-or-slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDisconnectEverythingFromSpecificSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Fatalf("expected nothing to reach the server") + }) + rest, err := Parser().ParseArgs([]string{"disconnect", "consumer"}) + c.Assert(err, ErrorMatches, `please provide the plug or slot name to disconnect from snap "consumer"`) + c.Assert(rest, DeepEquals, []string{"consumer"}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_download.go b/cmd/snap/cmd_download.go new file mode 100644 index 00000000..18929ecf --- /dev/null +++ b/cmd/snap/cmd_download.go @@ -0,0 +1,140 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/image" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/store" +) + +type cmdDownload struct { + channelMixin + Revision string `long:"revision"` + + Positional struct { + Snap remoteSnapName + } `positional-args:"true" required:"true"` +} + +var shortDownloadHelp = i18n.G("Downloads the given snap") +var longDownloadHelp = i18n.G(` +The download command downloads the given snap and its supporting assertions +to the current directory under .snap and .assert file extensions, respectively. +`) + +func init() { + addCommand("download", shortDownloadHelp, longDownloadHelp, func() flags.Commander { + return &cmdDownload{} + }, channelDescs.also(map[string]string{ + "revision": i18n.G("Download the given revision of a snap, to which you must have developer access"), + }), []argDesc{{ + name: "", + desc: i18n.G("Snap name"), + }}) +} + +func fetchSnapAssertions(sto *store.Store, snapPath string, snapInfo *snap.Info, dlOpts *image.DownloadOptions) error { + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: sysdb.Trusted(), + }) + if err != nil { + return err + } + + assertPath := strings.TrimSuffix(snapPath, filepath.Ext(snapPath)) + ".assert" + w, err := os.Create(assertPath) + if err != nil { + return fmt.Errorf(i18n.G("cannot create assertions file: %v"), err) + } + defer w.Close() + + encoder := asserts.NewEncoder(w) + save := func(a asserts.Assertion) error { + return encoder.Encode(a) + } + f := image.StoreAssertionFetcher(sto, dlOpts, db, save) + + return image.FetchAndCheckSnapAssertions(snapPath, snapInfo, f, db) +} + +func (x *cmdDownload) Execute(args []string) error { + if err := x.setChannelFromCommandline(); err != nil { + return err + } + + if len(args) > 0 { + return ErrExtraArgs + } + + var revision snap.Revision + if x.Revision == "" { + revision = snap.R(0) + } else { + var err error + revision, err = snap.ParseRevision(x.Revision) + if err != nil { + return err + } + } + + snapName := string(x.Positional.Snap) + + // FIXME: set auth context + var authContext auth.AuthContext + var user *auth.UserState + + sto := store.New(nil, authContext) + // we always allow devmode for downloads + devMode := true + + dlOpts := image.DownloadOptions{ + TargetDir: "", // cwd + DevMode: devMode, + Channel: x.Channel, + User: user, + } + + fmt.Fprintf(Stderr, i18n.G("Fetching snap %q\n"), snapName) + snapPath, snapInfo, err := image.DownloadSnap(sto, snapName, revision, &dlOpts) + if err != nil { + return err + } + + fmt.Fprintf(Stderr, i18n.G("Fetching assertions for %q\n"), snapName) + err = fetchSnapAssertions(sto, snapPath, snapInfo, &dlOpts) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/snap/cmd_experimental.go b/cmd/snap/cmd_experimental.go new file mode 100644 index 00000000..a4c1addf --- /dev/null +++ b/cmd/snap/cmd_experimental.go @@ -0,0 +1,34 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/i18n" +) + +type cmdExperimental struct{} + +var shortExperimentalHelp = i18n.G("Runs unsupported experimental commands") +var longExperimentalHelp = i18n.G(` +The experimental command contains a selection of additional sub-commands. + +Experimental commands can be removed without notice and may not work on +non-development systems. +`) diff --git a/cmd/snap/cmd_export_key.go b/cmd/snap/cmd_export_key.go new file mode 100644 index 00000000..d2297001 --- /dev/null +++ b/cmd/snap/cmd_export_key.go @@ -0,0 +1,95 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" +) + +type cmdExportKey struct { + Account string `long:"account"` + Positional struct { + KeyName keyName + } `positional-args:"true"` +} + +func init() { + cmd := addCommand("export-key", + i18n.G("Export cryptographic public key"), + i18n.G("Export a public key assertion body that may be imported by other systems."), + func() flags.Commander { + return &cmdExportKey{} + }, map[string]string{ + "account": i18n.G("Format public key material as a request for an account-key for this account-id"), + }, []argDesc{{ + name: i18n.G(""), + desc: i18n.G("Name of key to export"), + }}) + cmd.hidden = true +} + +func (x *cmdExportKey) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + keyName := string(x.Positional.KeyName) + if keyName == "" { + keyName = "default" + } + + manager := asserts.NewGPGKeypairManager() + if x.Account != "" { + privKey, err := manager.GetByName(keyName) + if err != nil { + return err + } + pubKey := privKey.PublicKey() + headers := map[string]interface{}{ + "account-id": x.Account, + "name": keyName, + "public-key-sha3-384": pubKey.ID(), + "since": time.Now().UTC().Format(time.RFC3339), + // XXX: To support revocation, we need to check for matching known assertions and set a suitable revision if we find one. + } + body, err := asserts.EncodePublicKey(pubKey) + if err != nil { + return err + } + assertion, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, headers, body, privKey) + if err != nil { + return err + } + fmt.Fprint(Stdout, string(asserts.Encode(assertion))) + } else { + encoded, err := manager.Export(keyName) + if err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", encoded) + } + return nil +} diff --git a/cmd/snap/cmd_export_key_test.go b/cmd/snap/cmd_export_key_test.go new file mode 100644 index 00000000..18082b3f --- /dev/null +++ b/cmd/snap/cmd_export_key_test.go @@ -0,0 +1,86 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapKeysSuite) TestExportKeyNonexistent(c *C) { + _, err := snap.Parser().ParseArgs([]string{"export-key", "nonexistent"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "cannot find key named \"nonexistent\" in GPG keyring") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestExportKeyDefault(c *C) { + rest, err := snap.Parser().ParseArgs([]string{"export-key"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + pubKey, err := asserts.DecodePublicKey(s.stdout.Bytes()) + c.Assert(err, IsNil) + c.Check(pubKey.ID(), Equals, "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestExportKeyNonDefault(c *C) { + rest, err := snap.Parser().ParseArgs([]string{"export-key", "another"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + pubKey, err := asserts.DecodePublicKey(s.stdout.Bytes()) + c.Assert(err, IsNil) + c.Check(pubKey.ID(), Equals, "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestExportKeyAccount(c *C) { + rootPrivKey, _ := assertstest.GenerateKey(1024) + storePrivKey, _ := assertstest.GenerateKey(752) + storeSigning := assertstest.NewStoreStack("canonical", rootPrivKey, storePrivKey) + manager := asserts.NewGPGKeypairManager() + assertstest.NewAccount(storeSigning, "developer1", nil, "") + rest, err := snap.Parser().ParseArgs([]string{"export-key", "another", "--account=developer1"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + assertion, err := asserts.Decode(s.stdout.Bytes()) + c.Assert(err, IsNil) + c.Check(assertion.Type(), Equals, asserts.AccountKeyRequestType) + c.Check(assertion.Revision(), Equals, 0) + c.Check(assertion.HeaderString("account-id"), Equals, "developer1") + c.Check(assertion.HeaderString("name"), Equals, "another") + c.Check(assertion.HeaderString("public-key-sha3-384"), Equals, "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L") + since, err := time.Parse(time.RFC3339, assertion.HeaderString("since")) + c.Assert(err, IsNil) + zone, offset := since.Zone() + c.Check(zone, Equals, "UTC") + c.Check(offset, Equals, 0) + c.Check(s.Stderr(), Equals, "") + privKey, err := manager.Get(assertion.HeaderString("public-key-sha3-384")) + c.Assert(err, IsNil) + err = asserts.SignatureCheck(assertion, privKey.PublicKey()) + c.Assert(err, IsNil) +} diff --git a/cmd/snap/cmd_find.go b/cmd/snap/cmd_find.go new file mode 100644 index 00000000..b8bf9417 --- /dev/null +++ b/cmd/snap/cmd_find.go @@ -0,0 +1,143 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "errors" + "fmt" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +var shortFindHelp = i18n.G("Finds packages to install") +var longFindHelp = i18n.G(` +The find command queries the store for available packages. +`) + +func getPrice(prices map[string]float64, currency string) (float64, string, error) { + // If there are no prices, then the snap is free + if len(prices) == 0 { + // TRANSLATORS: free as in gratis + return 0, "", errors.New(i18n.G("snap is free")) + } + + // Look up the price by currency code + val, ok := prices[currency] + + // Fall back to dollars + if !ok { + currency = "USD" + val, ok = prices["USD"] + } + + // If there aren't even dollars, grab the first currency, + // ordered alphabetically by currency code + if !ok { + currency = "ZZZ" + for c, v := range prices { + if c < currency { + currency, val = c, v + } + } + } + + return val, currency, nil +} + +type SectionName string + +func (s SectionName) Complete(match string) []flags.Completion { + cli := Client() + sections, err := cli.Sections() + if err != nil { + return nil + } + ret := make([]flags.Completion, 0, len(sections)) + for _, s := range sections { + if strings.HasPrefix(s, match) { + ret = append(ret, flags.Completion{Item: s}) + } + } + return ret +} + +type cmdFind struct { + Private bool `long:"private"` + Section SectionName `long:"section"` + Positional struct { + Query string + } `positional-args:"yes"` +} + +func init() { + addCommand("find", shortFindHelp, longFindHelp, func() flags.Commander { + return &cmdFind{} + }, map[string]string{ + "private": i18n.G("Search private snaps"), + "section": i18n.G("Restrict the search to a given section"), + }, []argDesc{{name: i18n.G("")}}) +} + +func (x *cmdFind) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + // magic! `snap find` returns the featured snaps + if x.Positional.Query == "" && x.Section == "" { + x.Section = "featured" + } + + return findSnaps(&client.FindOptions{ + Private: x.Private, + Section: string(x.Section), + Query: x.Positional.Query, + }) +} + +func findSnaps(opts *client.FindOptions) error { + cli := Client() + snaps, resInfo, err := cli.Find(opts) + if err != nil { + return err + } + + if len(snaps) == 0 { + // TRANSLATORS: the %q is the (quoted) query the user entered + fmt.Fprintf(Stderr, i18n.G("The search %q returned 0 snaps\n"), opts.Query) + return nil + } + + w := tabWriter() + defer w.Flush() + + fmt.Fprintln(w, i18n.G("Name\tVersion\tDeveloper\tNotes\tSummary")) + + for _, snap := range snaps { + // TODO: get snap.Publisher, so we can only show snap.Developer if it's different + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Developer, NotesFromRemote(snap, resInfo), snap.Summary) + } + + return nil +} diff --git a/cmd/snap/cmd_find_test.go b/cmd/snap/cmd_find_test.go new file mode 100644 index 00000000..561d5ff4 --- /dev/null +++ b/cmd/snap/cmd_find_test.go @@ -0,0 +1,364 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + "github.com/jessevdk/go-flags" + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +const findJSON = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + }, + { + "channel": "stable", + "confinement": "strict", + "description": "This is a simple hello world example.", + "developer": "canonical", + "download-size": 20480, + "icon": "", + "id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "private": false, + "resource": "/v2/snaps/hello-world", + "revision": "26", + "status": "available", + "summary": "Hello world example", + "type": "app", + "version": "6.1" + }, + { + "channel": "stable", + "confinement": "strict", + "description": "1.0GB", + "developer": "noise", + "download-size": 512004096, + "icon": "", + "id": "asXOGCreK66DIAdyXmucwspTMgqA4rne", + "name": "hello-huge", + "private": false, + "resource": "/v2/snaps/hello-huge", + "revision": "1", + "status": "available", + "summary": "a really big snap", + "type": "app", + "version": "1.0" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestFindSnapName(c *check.C) { + n := 0 + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + q := r.URL.Query() + if q.Get("q") == "" { + v, ok := q["section"] + c.Check(ok, check.Equals, true) + c.Check(v, check.DeepEquals, []string{""}) + } + fmt.Fprintln(w, findJSON) + default: + c.Fatalf("expected to get 2 requests, now on %d", n+1) + } + n++ + }) + + rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + + c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary +hello +2.10 +canonical +- +GNU Hello, the "hello world" snap +hello-world +6.1 +canonical +- +Hello world example +hello-huge +1.0 +noise +- +a really big snap +`) + c.Check(s.Stderr(), check.Equals, "") + + s.ResetStdStreams() +} + +const findHelloJSON = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + }, + { + "channel": "stable", + "confinement": "strict", + "description": "1.0GB", + "developer": "noise", + "download-size": 512004096, + "icon": "", + "id": "asXOGCreK66DIAdyXmucwspTMgqA4rne", + "name": "hello-huge", + "private": false, + "resource": "/v2/snaps/hello-huge", + "revision": "1", + "status": "available", + "summary": "a really big snap", + "type": "app", + "version": "1.0" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestFindHello(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + q := r.URL.Query() + c.Check(q.Get("q"), check.Equals, "hello") + fmt.Fprintln(w, findHelloJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary +hello +2.10 +canonical +- +GNU Hello, the "hello world" snap +hello-huge +1.0 +noise +- +a really big snap +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const findPricedJSON = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "prices": {"GBP": 1.99, "USD": 2.99}, + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "priced", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestFindPriced(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + fmt.Fprintln(w, findPricedJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary +hello +2.10 +canonical +1.99GBP +GNU Hello, the "hello world" snap +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const findPricedAndBoughtJSON = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "prices": {"GBP": 1.99, "USD": 2.99}, + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestFindPricedAndBought(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + fmt.Fprintln(w, findPricedAndBoughtJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary +hello +2.10 +canonical +bought +GNU Hello, the "hello world" snap +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestFindNothingMeansFeaturedSection(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + c.Check(r.URL.Query().Get("section"), check.Equals, "featured") + fmt.Fprintln(w, findJSON) + default: + c.Fatalf("expected to get 1 request, now on %d", n+1) + } + n++ + }) + + _, err := snap.Parser().ParseArgs([]string{"find"}) + c.Assert(err, check.IsNil) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestSectionCompletion(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0, 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/sections") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []string{"foo", "bar", "baz"}, + }) + default: + c.Fatalf("expected to get 2 requests, now on #%d", n+1) + } + n++ + }) + + c.Check(snap.SectionName("").Complete(""), check.DeepEquals, []flags.Completion{ + {Item: "foo"}, + {Item: "bar"}, + {Item: "baz"}, + }) + + c.Check(snap.SectionName("").Complete("f"), check.DeepEquals, []flags.Completion{ + {Item: "foo"}, + }) +} diff --git a/cmd/snap/cmd_first_boot.go b/cmd/snap/cmd_first_boot.go new file mode 100644 index 00000000..8163143d --- /dev/null +++ b/cmd/snap/cmd_first_boot.go @@ -0,0 +1,49 @@ +// -*- 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" +) + +type cmdInternalFirstBoot struct{} + +func init() { + cmd := addCommand("firstboot", + "internal", + "internal", func() flags.Commander { + return &cmdInternalFirstBoot{} + }, nil, nil) + cmd.hidden = true +} + +// WARNING: do not remove this command, older systems may still have +// a systemd snapd.firstboot.service job in /etc/systemd/system +// that we did not cleanup. so we need this dummy command or +// those units will start failing. +func (x *cmdInternalFirstBoot) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + fmt.Fprintf(Stderr, "firstboot command is deprecated") + return nil +} diff --git a/cmd/snap/cmd_get.go b/cmd/snap/cmd_get.go new file mode 100644 index 00000000..b09ce607 --- /dev/null +++ b/cmd/snap/cmd_get.go @@ -0,0 +1,124 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +var shortGetHelp = i18n.G("Prints configuration options") +var longGetHelp = i18n.G(` +The get command prints configuration options for the provided snap. + + $ snap get snap-name username + frank + +If multiple option names are provided, a document is returned: + + $ snap get snap-name username password + { + "username": "frank", + "password": "..." + } + +Nested values may be retrieved via a dotted path: + + $ snap get snap-name author.name + frank +`) + +type cmdGet struct { + Positional struct { + Snap installedSnapName + Keys []string + } `positional-args:"yes" required:"yes"` + + Typed bool `short:"t"` + Document bool `short:"d"` +} + +func init() { + addCommand("get", shortGetHelp, longGetHelp, func() flags.Commander { return &cmdGet{} }, + map[string]string{ + "d": i18n.G("Always return document, even with single key"), + "t": i18n.G("Strict typing with nulls and quoted strings"), + }, []argDesc{ + { + name: "", + desc: i18n.G("The snap whose conf is being requested"), + }, + { + name: i18n.G(""), + desc: i18n.G("Key of interest within the configuration"), + }, + }) +} + +func (x *cmdGet) Execute(args []string) error { + if len(args) > 0 { + // TRANSLATORS: the %s is the list of extra arguments + return fmt.Errorf(i18n.G("too many arguments: %s"), strings.Join(args, " ")) + } + + if x.Document && x.Typed { + return fmt.Errorf("cannot use -d and -t together") + } + + snapName := string(x.Positional.Snap) + confKeys := x.Positional.Keys + + cli := Client() + conf, err := cli.Conf(snapName, confKeys) + if err != nil { + return err + } + + var confToPrint interface{} = conf + if !x.Document && len(confKeys) == 1 { + confToPrint = conf[confKeys[0]] + } + + if x.Typed && confToPrint == nil { + fmt.Fprintln(Stdout, "null") + return nil + } + + if s, ok := confToPrint.(string); ok && !x.Typed { + fmt.Fprintln(Stdout, s) + return nil + } + + var bytes []byte + if confToPrint != nil { + bytes, err = json.MarshalIndent(confToPrint, "", "\t") + if err != nil { + return err + } + } + + fmt.Fprintln(Stdout, string(bytes)) + return nil +} diff --git a/cmd/snap/cmd_get_test.go b/cmd/snap/cmd_get_test.go new file mode 100644 index 00000000..d716712a --- /dev/null +++ b/cmd/snap/cmd_get_test.go @@ -0,0 +1,105 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "strings" + + . "gopkg.in/check.v1" + + snapset "github.com/snapcore/snapd/cmd/snap" +) + +var getTests = []struct { + args, stdout, error string +}{{ + args: "get snap-name --foo", + error: ".*unknown flag.*foo.*", +}, { + args: "get snapname test-key1", + stdout: "test-value1\n", +}, { + args: "get snapname test-key2", + stdout: "2\n", +}, { + args: "get snapname missing-key", + stdout: "\n", +}, { + args: "get -t snapname test-key1", + stdout: "\"test-value1\"\n", +}, { + args: "get -t snapname test-key2", + stdout: "2\n", +}, { + args: "get -t snapname missing-key", + stdout: "null\n", +}, { + args: "get -d snapname test-key1", + stdout: "{\n\t\"test-key1\": \"test-value1\"\n}\n", +}, { + args: "get snapname test-key1 test-key2", + stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n", +}} + +func (s *SnapSuite) TestSnapGetTests(c *C) { + s.mockGetConfigServer(c) + + for _, test := range getTests { + s.stdout.Truncate(0) + s.stderr.Truncate(0) + + c.Logf("Test: %s", test.args) + + _, err := snapset.Parser().ParseArgs(strings.Fields(test.args)) + if test.error != "" { + c.Check(err, ErrorMatches, test.error) + } else { + c.Check(err, IsNil) + c.Check(s.Stdout(), Equals, test.stdout) + } + } +} + +func (s *SnapSuite) mockGetConfigServer(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v2/snaps/snapname/conf" { + c.Errorf("unexpected path %q", r.URL.Path) + return + } + + c.Check(r.Method, Equals, "GET") + + query := r.URL.Query() + switch query.Get("keys") { + case "test-key1": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key1":"test-value1"}}`) + case "test-key2": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key2":2}}`) + case "test-key1,test-key2": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key1":"test-value1","test-key2":2}}`) + case "missing-key": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`) + default: + c.Errorf("unexpected keys %q", query.Get("keys")) + } + }) +} diff --git a/cmd/snap/cmd_help.go b/cmd/snap/cmd_help.go new file mode 100644 index 00000000..97336092 --- /dev/null +++ b/cmd/snap/cmd_help.go @@ -0,0 +1,62 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "os" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortHelpHelp = i18n.G("Help") +var longHelpHelp = i18n.G(` +The help command shows helpful information. Unlike this. ;-) +`) + +type cmdHelp struct { + Manpage bool `long:"man"` + parser *flags.Parser +} + +func init() { + addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} }, + map[string]string{"man": i18n.G("Generate the manpage")}, nil) +} + +func (cmd *cmdHelp) setParser(parser *flags.Parser) { + cmd.parser = parser +} + +func (cmd cmdHelp) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if cmd.Manpage { + cmd.parser.WriteManPage(Stdout) + os.Exit(0) + } + + return &flags.Error{ + Type: flags.ErrHelp, + } +} diff --git a/cmd/snap/cmd_help_test.go b/cmd/snap/cmd_help_test.go new file mode 100644 index 00000000..2d24d7c0 --- /dev/null +++ b/cmd/snap/cmd_help_test.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "os" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestHelpPrintsHelp(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + + for _, cmdLine := range [][]string{ + {"snap", "help"}, + {"snap", "--help"}, + {"snap", "-h"}, + } { + os.Args = cmdLine + + err := snap.RunMain() + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?smU)Usage: + +snap \[OPTIONS\] + +Install, configure, refresh and remove snap packages. Snaps are +'universal' packages that work across many different Linux systems, +enabling secure distribution of the latest apps and utilities for +cloud, servers, desktops and the internet of things. + +This is the CLI for snapd, a background service that takes care of +snaps on the system. Start with 'snap list' to see installed snaps. + + +Application Options: + +--version +Print the version and exit + +Help Options: + +-h, --help +Show this help message + +Available commands: + +abort.* +`) + c.Check(s.Stderr(), check.Equals, "") + } +} + +func (s *SnapSuite) TestSubCommandHelpPrintsHelp(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + + os.Args = []string{"snap", "install", "--help"} + + err := snap.RunMain() + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?smU)Usage: + +snap \[OPTIONS\] install \[install-OPTIONS\] ... +.* +`) + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_info.go b/cmd/snap/cmd_info.go new file mode 100644 index 00000000..f73a945d --- /dev/null +++ b/cmd/snap/cmd_info.go @@ -0,0 +1,266 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "bytes" + "fmt" + "io" + "path/filepath" + "strings" + "text/tabwriter" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" +) + +type infoCmd struct { + Verbose bool `long:"verbose"` + Positional struct { + Snaps []anySnapName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +var shortInfoHelp = i18n.G("show detailed information about a snap") +var longInfoHelp = i18n.G(` +The info command shows detailed information about a snap, be it by name or by path.`) + +func init() { + addCommand("info", + shortInfoHelp, + longInfoHelp, + func() flags.Commander { + return &infoCmd{} + }, map[string]string{ + "verbose": i18n.G("Include a verbose list of a snap's notes (otherwise, summarise notes)"), + }, nil) +} + +func norm(path string) string { + path = filepath.Clean(path) + if osutil.IsDirectory(path) { + path = path + "/" + } + + return path +} + +func maybePrintType(w io.Writer, t string) { + // XXX: using literals here until we reshuffle snap & client properly + // (and os->core rename happens, etc) + switch t { + case "", "app", "application": + return + case "os": + t = "core" + } + + fmt.Fprintf(w, "type:\t%s\n", t) +} + +func tryDirect(w io.Writer, path string, verbose bool) bool { + path = norm(path) + + snapf, err := snap.Open(path) + if err != nil { + return false + } + + info, err := snap.ReadInfoFromSnapFile(snapf, nil) + if err != nil { + return false + } + fmt.Fprintf(w, "path:\t%q\n", path) + fmt.Fprintf(w, "name:\t%s\n", info.Name()) + fmt.Fprintf(w, "summary:\t%q\n", info.Summary()) + + var notes *Notes + if verbose { + fmt.Fprintln(w, "notes:\t") + fmt.Fprintf(w, " confinement:\t%s\n", info.Confinement) + if info.Broken == "" { + fmt.Fprintln(w, " broken:\tfalse") + } else { + fmt.Fprintf(w, " broken:\ttrue (%s)\n", info.Broken) + } + + } else { + notes = NotesFromInfo(info) + } + fmt.Fprintf(w, "version:\t%s %s\n", info.Version, notes) + maybePrintType(w, string(info.Type)) + + return true +} + +func coalesce(snaps ...*client.Snap) *client.Snap { + for _, s := range snaps { + if s != nil { + return s + } + } + return nil +} + +// formatDescr formats a given string (typically a snap description) +// in a user friendly way. +// +// The rules are (intentionally) very simple: +// - word wrap at "max" chars +// - keep \n intact and break here +// - ignore \r +func formatDescr(descr string, max int) string { + out := bytes.NewBuffer(nil) + for _, line := range strings.Split(descr, "\n") { + if len(line) > max { + for _, chunk := range strutil.WordWrap(line, max) { + fmt.Fprintf(out, " %s\n", chunk) + } + } else { + fmt.Fprintf(out, " %s\n", line) + } + } + + return strings.TrimSuffix(out.String(), "\n") +} + +func maybePrintCommands(w io.Writer, snapName string, allApps []client.AppInfo, n int) { + if len(allApps) == 0 { + return + } + + commands := make([]string, 0, len(allApps)) + for _, app := range allApps { + if app.Daemon != "" { + continue + } + + // TODO: helper for this? + cmdStr := app.Name + if cmdStr != snapName { + cmdStr = fmt.Sprintf("%s.%s", snapName, cmdStr) + } + + if len(app.Aliases) != 0 { + cmdStr = fmt.Sprintf("%s (%s)", cmdStr, strings.Join(app.Aliases, ",")) + } + + commands = append(commands, cmdStr) + } + if len(commands) == 0 { + return + } + + fmt.Fprintf(w, "commands:\n") + for _, cmd := range commands { + fmt.Fprintf(w, " - %s\n", cmd) + } +} + +func (x *infoCmd) Execute([]string) error { + cli := Client() + + w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) + + noneOK := true + for i, snapName := range x.Positional.Snaps { + snapName := string(snapName) + if i > 0 { + fmt.Fprintln(w, "---") + } + + if tryDirect(w, snapName, x.Verbose) { + noneOK = false + continue + } + remote, _, _ := cli.FindOne(snapName) + local, _, _ := cli.Snap(snapName) + + both := coalesce(local, remote) + + if both == nil { + fmt.Fprintf(w, "argument:\t%q\nwarning:\t%s\n", snapName, i18n.G("not a valid snap")) + continue + } + noneOK = false + + fmt.Fprintf(w, "name:\t%s\n", both.Name) + fmt.Fprintf(w, "summary:\t%q\n", both.Summary) + // TODO: have publisher; use publisher here, + // and additionally print developer if publisher != developer + fmt.Fprintf(w, "publisher:\t%s\n", both.Developer) + // FIXME: find out for real + termWidth := 77 + fmt.Fprintf(w, "description: |\n%s\n", formatDescr(both.Description, termWidth)) + maybePrintType(w, both.Type) + maybePrintCommands(w, snapName, both.Apps, termWidth) + if x.Verbose { + fmt.Fprintln(w, "notes:\t") + fmt.Fprintf(w, " private:\t%t\n", both.Private) + fmt.Fprintf(w, " confinement:\t%s\n", both.Confinement) + } + + if local != nil { + var notes *Notes + if x.Verbose { + jailMode := local.Confinement == client.DevModeConfinement && !local.DevMode + fmt.Fprintf(w, " devmode:\t%t\n", local.DevMode) + fmt.Fprintf(w, " jailmode:\t%t\n", jailMode) + fmt.Fprintf(w, " trymode:\t%t\n", local.TryMode) + fmt.Fprintf(w, " enabled:\t%t\n", local.Status == client.StatusActive) + if local.Broken == "" { + fmt.Fprintf(w, " broken:\t%t\n", false) + } else { + fmt.Fprintf(w, " broken:\t%t (%s)\n", true, local.Broken) + } + } else { + notes = NotesFromLocal(local) + } + + fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel) + fmt.Fprintf(w, "installed:\t%s\t(%s)\t%s\t%s\n", local.Version, local.Revision, strutil.SizeToStr(local.InstalledSize), notes) + fmt.Fprintf(w, "refreshed:\t%s\n", local.InstallDate) + } + + if remote != nil && remote.Channels != nil { + // \t\t\t so we get "installed" lined up with "channels" + fmt.Fprintf(w, "channels:\t\t\t\n") + for _, ch := range []string{"stable", "candidate", "beta", "edge"} { + m := remote.Channels[ch] + if m == nil { + continue + } + fmt.Fprintf(w, " %s:\t%s\t(%s)\t%s\t%s\n", ch, m.Version, m.Revision, strutil.SizeToStr(m.Size), NotesFromChannelSnapInfo(m)) + } + } + } + w.Flush() + + if noneOK { + return fmt.Errorf(i18n.G("no valid snaps given")) + } + + return nil +} diff --git a/cmd/snap/cmd_interfaces.go b/cmd/snap/cmd_interfaces.go new file mode 100644 index 00000000..f4d5f0d7 --- /dev/null +++ b/cmd/snap/cmd_interfaces.go @@ -0,0 +1,138 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdInterfaces struct { + Interface string `short:"i"` + Positionals struct { + Query SnapAndName `skip-help:"true"` + } `positional-args:"true"` +} + +var shortInterfacesHelp = i18n.G("Lists interfaces in the system") +var longInterfacesHelp = i18n.G(` +The interfaces command lists interfaces available in the system. + +By default all slots and plugs, used and offered by all snaps, are displayed. + +$ snap interfaces : + +Lists only the specified slot or plug. + +$ snap interfaces + +Lists the slots offered and plugs used by the specified snap. + +$ snap interfaces -i= [] + +Filters the complete output so only plugs and/or slots matching the provided details are listed. +`) + +func init() { + addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander { + return &cmdInterfaces{} + }, map[string]string{ + "i": i18n.G("Constrain listing to specific interfaces"), + }, []argDesc{{ + name: i18n.G(":"), + desc: i18n.G("Constrain listing to a specific snap or snap:name"), + }}) +} + +func (x *cmdInterfaces) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + ifaces, err := Client().Interfaces() + if err == nil { + if len(ifaces.Plugs) == 0 && len(ifaces.Slots) == 0 { + return fmt.Errorf(i18n.G("no interfaces found")) + } + w := tabWriter() + fmt.Fprintln(w, i18n.G("Slot\tPlug")) + defer w.Flush() + for _, slot := range ifaces.Slots { + if wanted := x.Positionals.Query.Snap; wanted != "" { + ok := wanted == slot.Snap + for i := 0; i < len(slot.Connections) && !ok; i++ { + ok = wanted == slot.Connections[i].Snap + } + if !ok { + continue + } + } + if x.Positionals.Query.Name != "" && x.Positionals.Query.Name != slot.Name { + continue + } + if x.Interface != "" && slot.Interface != x.Interface { + continue + } + // The OS snap is special and enable abbreviated + // display syntax on the slot-side of the connection. + if slot.Snap == "core" || slot.Snap == "ubuntu-core" { + fmt.Fprintf(w, ":%s\t", slot.Name) + } else { + fmt.Fprintf(w, "%s:%s\t", slot.Snap, slot.Name) + } + for i := 0; i < len(slot.Connections); i++ { + if i > 0 { + fmt.Fprint(w, ",") + } + if slot.Connections[i].Name != slot.Name { + fmt.Fprintf(w, "%s:%s", slot.Connections[i].Snap, slot.Connections[i].Name) + } else { + fmt.Fprintf(w, "%s", slot.Connections[i].Snap) + } + } + // Display visual indicator for disconnected slots + if len(slot.Connections) == 0 { + fmt.Fprint(w, "-") + } + fmt.Fprintf(w, "\n") + } + // Plugs are treated differently. Since the loop above already printed each connected + // plug, the loop below focuses on printing just the disconnected plugs. + for _, plug := range ifaces.Plugs { + if x.Positionals.Query.Snap != "" && x.Positionals.Query.Snap != plug.Snap { + continue + } + if x.Positionals.Query.Name != "" && x.Positionals.Query.Name != plug.Name { + continue + } + if x.Interface != "" && plug.Interface != x.Interface { + continue + } + // Display visual indicator for disconnected plugs. + if len(plug.Connections) == 0 { + fmt.Fprintf(w, "-\t%s:%s\n", plug.Snap, plug.Name) + } + } + } + return err +} diff --git a/cmd/snap/cmd_interfaces_test.go b/cmd/snap/cmd_interfaces_test.go new file mode 100644 index 00000000..91254a52 --- /dev/null +++ b/cmd/snap/cmd_interfaces_test.go @@ -0,0 +1,527 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "io/ioutil" + "net/http" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestInterfacesHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] interfaces [interfaces-OPTIONS] [:] + +The interfaces command lists interfaces available in the system. + +By default all slots and plugs, used and offered by all snaps, are displayed. + +$ snap interfaces : + +Lists only the specified slot or plug. + +$ snap interfaces + +Lists the slots offered and plugs used by the specified snap. + +$ snap interfaces -i= [] + +Filters the complete output so only plugs and/or slots matching the provided +details are listed. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message + +[interfaces command options] + -i= Constrain listing to specific interfaces + +[interfaces command arguments] + :: Constrain listing to a specific snap or snap:name +` + rest, err := Parser().ParseArgs([]string{"interfaces", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestInterfacesZeroSlotsOnePlug(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Interfaces{ + Plugs: []client.Plug{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "- keyboard-lights:capslock-led\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfacesZeroPlugsOneSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Interfaces{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "canonical-pi2:pin-13 -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfacesOneSlotOnePlug(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Interfaces{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.PlugRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }, + Plugs: []client.Plug{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + Interface: "bool-file", + Label: "Capslock indicator LED", + Connections: []client.SlotRef{ + { + Snap: "canonical-pi2", + Name: "pin-13", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "canonical-pi2:pin-13 keyboard-lights:capslock-led\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") + + s.SetUpTest(c) + // should be the same + rest, err = Parser().ParseArgs([]string{"interfaces", "canonical-pi2"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") + + s.SetUpTest(c) + // and the same again + rest, err = Parser().ParseArgs([]string{"interfaces", "keyboard-lights"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfacesTwoPlugs(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Interfaces{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.PlugRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + { + Snap: "keyboard-lights", + Name: "scrollock-led", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "canonical-pi2:pin-13 keyboard-lights:capslock-led,keyboard-lights:scrollock-led\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfacesPlugsWithCommonName(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Interfaces{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.PlugRef{ + { + Snap: "paste-daemon", + Name: "network-listening", + }, + { + Snap: "time-daemon", + Name: "network-listening", + }, + }, + }, + }, + Plugs: []client.Plug{ + { + Snap: "paste-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.SlotRef{ + { + Snap: "canonical-pi2", + Name: "network-listening", + }, + }, + }, + { + Snap: "time-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.SlotRef{ + { + Snap: "canonical-pi2", + Name: "network-listening", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "canonical-pi2:network-listening paste-daemon,time-daemon\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfacesOsSnapSlots(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Interfaces{ + Slots: []client.Slot{ + { + Snap: "core", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.PlugRef{ + { + Snap: "paste-daemon", + Name: "network-listening", + }, + { + Snap: "time-daemon", + Name: "network-listening", + }, + }, + }, + }, + Plugs: []client.Plug{ + { + Snap: "paste-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.SlotRef{ + { + Snap: "core", + Name: "network-listening", + }, + }, + }, + { + Snap: "time-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.SlotRef{ + { + Snap: "core", + Name: "network-listening", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + ":network-listening paste-daemon,time-daemon\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfacesTwoSlotsAndFiltering(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Interfaces{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "debug-console", + Interface: "serial-port", + Label: "Serial port on the expansion header", + Connections: []client.PlugRef{ + { + Snap: "core", + Name: "debug-console", + }, + }, + }, + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.PlugRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces", "-i=serial-port"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "canonical-pi2:debug-console core\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfacesOfSpecificSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Interfaces{ + Slots: []client.Slot{ + { + Snap: "cheese", + Name: "photo-trigger", + Interface: "bool-file", + Label: "Photo trigger", + }, + { + Snap: "wake-up-alarm", + Name: "toggle", + Interface: "bool-file", + Label: "Alarm toggle", + }, + { + Snap: "wake-up-alarm", + Name: "snooze", + Interface: "bool-file", + Label: "Alarm snooze", + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces", "wake-up-alarm"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "wake-up-alarm:toggle -\n" + + "wake-up-alarm:snooze -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfacesOfSpecificSnapAndSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Interfaces{ + Slots: []client.Slot{ + { + Snap: "cheese", + Name: "photo-trigger", + Interface: "bool-file", + Label: "Photo trigger", + }, + { + Snap: "wake-up-alarm", + Name: "toggle", + Interface: "bool-file", + Label: "Alarm toggle", + }, + { + Snap: "wake-up-alarm", + Name: "snooze", + Interface: "bool-file", + Label: "Alarm snooze", + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces", "wake-up-alarm:snooze"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "wake-up-alarm:snooze -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfacesNothingAtAll(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Interfaces{}, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, ErrorMatches, "no interfaces found") + // XXX: not sure why this is returned, I guess that's what happens when a + // command Execute returns an error. + c.Assert(rest, DeepEquals, []string{"interfaces"}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_keys.go b/cmd/snap/cmd_keys.go new file mode 100644 index 00000000..e4206cc2 --- /dev/null +++ b/cmd/snap/cmd_keys.go @@ -0,0 +1,90 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdKeys struct { + JSON bool `long:"json"` +} + +func init() { + cmd := addCommand("keys", + i18n.G("List cryptographic keys"), + i18n.G("List cryptographic keys that can be used for signing assertions."), + func() flags.Commander { + return &cmdKeys{} + }, map[string]string{"json": i18n.G("Output results in JSON format")}, nil) + cmd.hidden = true +} + +// Key represents a key that can be used for signing assertions. +type Key struct { + Name string `json:"name"` + Sha3_384 string `json:"sha3-384"` +} + +func (x *cmdKeys) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + w := tabWriter() + if !x.JSON { + fmt.Fprintln(w, i18n.G("Name\tSHA3-384")) + defer w.Flush() + } + keys := []Key{} + + manager := asserts.NewGPGKeypairManager() + display := func(privk asserts.PrivateKey, fpr string, uid string) error { + key := Key{ + Name: uid, + Sha3_384: privk.PublicKey().ID(), + } + if x.JSON { + keys = append(keys, key) + } else { + fmt.Fprintf(w, "%s\t%s\n", key.Name, key.Sha3_384) + } + return nil + } + err := manager.Walk(display) + if err != nil { + return err + } + if x.JSON { + obj, err := json.Marshal(keys) + if err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", obj) + } + + return nil +} diff --git a/cmd/snap/cmd_keys_test.go b/cmd/snap/cmd_keys_test.go new file mode 100644 index 00000000..eec332e5 --- /dev/null +++ b/cmd/snap/cmd_keys_test.go @@ -0,0 +1,127 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +type SnapKeysSuite struct { + BaseSnapSuite + + GnupgCmd string +} + +// FIXME: Ideally we would just use gpg2 and remove the gnupg2_test.go file. +// However currently there is LP: #1621839 which prevents us from +// switching to gpg2 fully. Once this is resolved we should switch. +var _ = Suite(&SnapKeysSuite{GnupgCmd: "/usr/bin/gpg"}) + +var fakePinentryData = []byte(`#!/bin/sh +set -e +echo "OK Pleased to meet you" +while true; do + read line + case $line in + BYE) + exit 0 + ;; + *) + echo "OK I agree to everything" + ;; +esac +done +`) + +func (s *SnapKeysSuite) SetUpTest(c *C) { + s.BaseSnapSuite.SetUpTest(c) + + tempdir := c.MkDir() + for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} { + data, err := ioutil.ReadFile(filepath.Join("test-data", fileName)) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644) + c.Assert(err, IsNil) + } + fakePinentryFn := filepath.Join(tempdir, "pinentry-fake") + err := ioutil.WriteFile(fakePinentryFn, fakePinentryData, 0755) + c.Assert(err, IsNil) + gpgAgentConfFn := filepath.Join(tempdir, "gpg-agent.conf") + err = ioutil.WriteFile(gpgAgentConfFn, []byte(fmt.Sprintf(`pinentry-program %s`, fakePinentryFn)), 0644) + c.Assert(err, IsNil) + + os.Setenv("SNAP_GNUPG_HOME", tempdir) + os.Setenv("SNAP_GNUPG_CMD", s.GnupgCmd) +} + +func (s *SnapKeysSuite) TearDownTest(c *C) { + os.Unsetenv("SNAP_GNUPG_HOME") + os.Unsetenv("SNAP_GNUPG_CMD") + s.BaseSnapSuite.TearDownTest(c) +} + +func (s *SnapKeysSuite) TestKeys(c *C) { + rest, err := snap.Parser().ParseArgs([]string{"keys"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, `Name +SHA3-384 +default +g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ +another +DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L +`) + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestKeysJSON(c *C) { + rest, err := snap.Parser().ParseArgs([]string{"keys", "--json"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedResponse := []snap.Key{ + { + Name: "default", + Sha3_384: "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ", + }, + { + Name: "another", + Sha3_384: "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L", + }, + } + var obtainedResponse []snap.Key + json.Unmarshal(s.stdout.Bytes(), &obtainedResponse) + c.Check(obtainedResponse, DeepEquals, expectedResponse) + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestKeysJSONEmpty(c *C) { + err := os.RemoveAll(os.Getenv("SNAP_GNUPG_HOME")) + c.Assert(err, IsNil) + rest, err := snap.Parser().ParseArgs([]string{"keys", "--json"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "[]\n") + c.Check(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_known.go b/cmd/snap/cmd_known.go new file mode 100644 index 00000000..107bf9cb --- /dev/null +++ b/cmd/snap/cmd_known.go @@ -0,0 +1,129 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/store" + + "github.com/jessevdk/go-flags" +) + +type cmdKnown struct { + KnownOptions struct { + // XXX: how to get a list of assert types for completion? + AssertTypeName string `required:"true"` + HeaderFilters []string `required:"0"` + } `positional-args:"true" required:"true"` + + Remote bool `long:"remote"` +} + +var shortKnownHelp = i18n.G("Shows known assertions of the provided type") +var longKnownHelp = i18n.G(` +The known command shows known assertions of the provided type. +If header=value pairs are provided after the assertion type, the assertions +shown must also have the specified headers matching the provided values. +`) + +func init() { + addCommand("known", shortKnownHelp, longKnownHelp, func() flags.Commander { + return &cmdKnown{} + }, nil, []argDesc{ + { + name: i18n.G(""), + desc: i18n.G("Assertion type name"), + }, { + name: i18n.G("
"), + desc: i18n.G("Constrain listing to those matching header=value"), + }, + }) +} + +var nl = []byte{'\n'} + +var storeNew = store.New + +func downloadAssertion(typeName string, headers map[string]string) ([]asserts.Assertion, error) { + var user *auth.UserState + + // FIXME: set auth context + var authContext auth.AuthContext + + at := asserts.Type(typeName) + if at == nil { + return nil, fmt.Errorf("cannot find assertion type %q", typeName) + } + primaryKeys := make([]string, len(at.PrimaryKey)) + for i, k := range at.PrimaryKey { + pk, ok := headers[k] + if !ok { + return nil, fmt.Errorf("missing primary header %q to query remote assertion", k) + } + primaryKeys[i] = pk + } + + sto := storeNew(nil, authContext) + as, err := sto.Assertion(at, primaryKeys, user) + if err != nil { + return nil, err + } + + return []asserts.Assertion{as}, nil +} + +func (x *cmdKnown) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + // TODO: share this kind of parsing once it's clearer how often is used in snap + headers := map[string]string{} + for _, headerFilter := range x.KnownOptions.HeaderFilters { + parts := strings.SplitN(headerFilter, "=", 2) + if len(parts) != 2 { + return fmt.Errorf(i18n.G("invalid header filter: %q (want key=value)"), headerFilter) + } + headers[parts[0]] = parts[1] + } + + var assertions []asserts.Assertion + var err error + if x.Remote { + assertions, err = downloadAssertion(x.KnownOptions.AssertTypeName, headers) + } else { + assertions, err = Client().Known(x.KnownOptions.AssertTypeName, headers) + } + if err != nil { + return err + } + + enc := asserts.NewEncoder(Stdout) + for _, a := range assertions { + enc.Encode(a) + } + + return nil +} diff --git a/cmd/snap/cmd_known_test.go b/cmd/snap/cmd_known_test.go new file mode 100644 index 00000000..6ebee7da --- /dev/null +++ b/cmd/snap/cmd_known_test.go @@ -0,0 +1,90 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/store" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +// acquire example data via: +// curl -H "accept: application/x.ubuntu.assertion" https://assertions.ubuntu.com/v1/assertions/model/16/canonical/pi2 +const mockModelAssertion = `type: model +authority-id: canonical +series: 16 +brand-id: canonical +model: pi99 +architecture: armhf +gadget: pi99 +kernel: pi99-kernel +timestamp: 2016-08-31T00:00:00.0Z +sign-key-sha3-384: 9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn + +AcLorsomethingthatlooksvaguelylikeasignature== +` + +func (s *SnapSuite) TestKnownRemote(c *check.C) { + var server *httptest.Server + + restorer := snap.MockStoreNew(func(cfg *store.Config, auth auth.AuthContext) *store.Store { + if cfg == nil { + cfg = store.DefaultConfig() + } + serverURL, err := url.Parse(server.URL + "/assertions/") + c.Assert(err, check.IsNil) + cfg.AssertionsURI = serverURL + return store.New(cfg, auth) + }) + defer restorer() + + n := 0 + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/assertions/model/16/canonical/pi99") + fmt.Fprintln(w, mockModelAssertion) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + })) + + rest, err := snap.Parser().ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, mockModelAssertion) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestKnownRemoteMissingPrimaryKey(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical"}) + c.Assert(err, check.ErrorMatches, `missing primary header "model" to query remote assertion`) +} diff --git a/cmd/snap/cmd_list.go b/cmd/snap/cmd_list.go new file mode 100644 index 00000000..01c7fd19 --- /dev/null +++ b/cmd/snap/cmd_list.go @@ -0,0 +1,99 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "errors" + "fmt" + "sort" + "text/tabwriter" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortListHelp = i18n.G("List installed snaps") +var longListHelp = i18n.G(` +The list command displays a summary of snaps installed in the current system.`) + +type cmdList struct { + Positional struct { + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` + + All bool `long:"all"` +} + +func init() { + addCommand("list", shortListHelp, longListHelp, func() flags.Commander { return &cmdList{} }, + map[string]string{"all": i18n.G("Show all revisions")}, nil) +} + +type snapsByName []*client.Snap + +func (s snapsByName) Len() int { return len(s) } +func (s snapsByName) Less(i, j int) bool { return s[i].Name < s[j].Name } +func (s snapsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func (x *cmdList) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + names := make([]string, len(x.Positional.Snaps)) + for i, name := range x.Positional.Snaps { + names[i] = string(name) + } + + return listSnaps(names, x.All) +} + +func listSnaps(names []string, all bool) error { + cli := Client() + snaps, err := cli.List(names, &client.ListOptions{All: all}) + if err != nil { + if err == client.ErrNoSnapsInstalled { + fmt.Fprintln(Stderr, i18n.G("No snaps are installed yet. Try \"snap install hello-world\".")) + return nil + } + return err + } else if len(snaps) == 0 { + return errors.New(i18n.G("no matching snaps installed")) + } + sort.Sort(snapsByName(snaps)) + + w := tabWriter() + defer w.Flush() + + fmt.Fprintln(w, i18n.G("Name\tVersion\tRev\tDeveloper\tNotes")) + + for _, snap := range snaps { + // TODO: make JailMode a flag in the snap itself + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Revision, snap.Developer, NotesFromLocal(snap)) + } + + return nil +} + +func tabWriter() *tabwriter.Writer { + return tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0) +} diff --git a/cmd/snap/cmd_list_test.go b/cmd/snap/cmd_list_test.go new file mode 100644 index 00000000..8d5e84f5 --- /dev/null +++ b/cmd/snap/cmd_list_test.go @@ -0,0 +1,213 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestListHelp(c *check.C) { + msg := `Usage: + snap.test [OPTIONS] list [list-OPTIONS] [...] + +The list command displays a summary of snaps installed in the current system. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message + +[list command options] + --all Show all revisions +` + rest, err := snap.Parser().ParseArgs([]string{"list", "--help"}) + c.Assert(err.Error(), check.Equals, msg) + c.Assert(rest, check.DeepEquals, []string{}) +} + +func (s *SnapSuite) TestList(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(r.URL.RawQuery, check.Equals, "") + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes +foo +4.2 +17 +bar +- +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestListAll(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(r.URL.RawQuery, check.Equals, "select=all") + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list", "--all"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes +foo +4.2 +17 +bar +- +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestListEmpty(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "No snaps are installed yet. Try \"snap install hello-world\".\n") +} + +func (s *SnapSuite) TestListEmptyWithQuery(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list", "quux"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "No snaps are installed yet. Try \"snap install hello-world\".\n") +} + +func (s *SnapSuite) TestListWithNoMatchingQuery(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + _, err := snap.Parser().ParseArgs([]string{"list", "quux"}) + c.Assert(err, check.ErrorMatches, "no matching snaps installed") +} + +func (s *SnapSuite) TestListWithQuery(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(r.URL.Query(), check.HasLen, 0) + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes +foo +4.2 +17 +bar +- +`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestListWithNotes(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintln(w, `{"type": "sync", "result": [ +{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17, "trymode": true} +,{"name": "dm1", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "devmode"} +,{"name": "dm2", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "strict"} +,{"name": "cf1", "status": "active", "version": "6", "revision":2, "confinement": "devmode", "jailmode": true} +]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?ms)^Name +Version +Rev +Developer +Notes$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^foo +4.2 +17 +bar +try$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^dm1 +.* +devmode$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^dm2 +.* +devmode$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^cf1 +.* +jailmode$`) + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_login.go b/cmd/snap/cmd_login.go new file mode 100644 index 00000000..1bb4f43c --- /dev/null +++ b/cmd/snap/cmd_login.go @@ -0,0 +1,128 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package main + +import ( + "bufio" + "fmt" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type cmdLogin struct { + Positional struct { + Email string + } `positional-args:"yes"` +} + +var shortLoginHelp = i18n.G("Authenticates on snapd and the store") + +var longLoginHelp = i18n.G(` +The login command authenticates on snapd and the snap store and saves credentials +into the ~/.snap/auth.json file. Further communication with snapd will then be made +using those credentials. + +Login only works for local users in the sudo, admin or wheel groups. + +An account can be setup at https://login.ubuntu.com +`) + +func init() { + addCommand("login", + shortLoginHelp, + longLoginHelp, + func() flags.Commander { + return &cmdLogin{} + }, nil, []argDesc{{ + // TRANSLATORS: noun + name: i18n.G(""), + desc: i18n.G("The login.ubuntu.com email to login as"), + }}) +} + +func requestLoginWith2faRetry(email, password string) error { + var otp []byte + var err error + + var msgs = [3]string{ + i18n.G("Two-factor code: "), + i18n.G("Bad code. Try again: "), + i18n.G("Wrong again. Once more: "), + } + + cli := Client() + reader := bufio.NewReader(nil) + + for i := 0; ; i++ { + // first try is without otp + _, err = cli.Login(email, password, string(otp)) + if i >= len(msgs) || !client.IsTwoFactorError(err) { + return err + } + + reader.Reset(Stdin) + fmt.Fprint(Stdout, msgs[i]) + // the browser shows it as well (and Sergio wants to see it ;) + otp, _, err = reader.ReadLine() + if err != nil { + return err + } + } +} + +func requestLogin(email string) error { + fmt.Fprint(Stdout, fmt.Sprintf(i18n.G("Password of %q: "), email)) + password, err := ReadPassword(0) + fmt.Fprint(Stdout, "\n") + if err != nil { + return err + } + + // strings.TrimSpace needed because we get \r from the pty in the tests + return requestLoginWith2faRetry(email, strings.TrimSpace(string(password))) +} + +func (x *cmdLogin) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + email := x.Positional.Email + if email == "" { + fmt.Fprint(Stdout, i18n.G("Email address: ")) + in, _, err := bufio.NewReader(Stdin).ReadLine() + if err != nil { + return err + } + email = string(in) + } + + err := requestLogin(email) + if err != nil { + return err + } + fmt.Fprintln(Stdout, i18n.G("Login successful")) + + return nil +} diff --git a/cmd/snap/cmd_login_test.go b/cmd/snap/cmd_login_test.go new file mode 100644 index 00000000..c9b43894 --- /dev/null +++ b/cmd/snap/cmd_login_test.go @@ -0,0 +1,87 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +var mockLoginRsp = `{"type": "sync", "result": {"id":42, "username": "foo", "email": "foo@example.com", "macaroon": "yummy", "discarages":"plenty"}}` + +func makeLoginTestServer(c *C, n *int) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + switch *n { + case 0: + c.Check(r.URL.Path, Equals, "/v2/login") + c.Check(r.Method, Equals, "POST") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(string(postData), Equals, `{"email":"foo@example.com","password":"some-password"}`+"\n") + fmt.Fprintln(w, mockLoginRsp) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + *n++ + } +} + +func (s *SnapSuite) TestLoginSimple(c *C) { + n := 0 + s.RedirectClientToTestServer(makeLoginTestServer(c, &n)) + + // send the password + s.password = "some-password\n" + rest, err := snap.Parser().ParseArgs([]string{"login", "foo@example.com"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, `Password of "foo@example.com": +Login successful +`) + c.Check(s.Stderr(), Equals, "") + c.Check(n, Equals, 1) +} + +func (s *SnapSuite) TestLoginAskEmail(c *C) { + n := 0 + s.RedirectClientToTestServer(makeLoginTestServer(c, &n)) + + // send the email + fmt.Fprint(s.stdin, "foo@example.com\n") + // send the password + s.password = "some-password" + + rest, err := snap.Parser().ParseArgs([]string{"login"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + // test slightly ugly, on a real system STDOUT will be: + // Email address: foo@example.com\n + // because the input to stdin is echoed + c.Check(s.Stdout(), Equals, `Email address: Password of "foo@example.com": +Login successful +`) + c.Check(s.Stderr(), Equals, "") + c.Check(n, Equals, 1) +} diff --git a/cmd/snap/cmd_logout.go b/cmd/snap/cmd_logout.go new file mode 100644 index 00000000..993012cb --- /dev/null +++ b/cmd/snap/cmd_logout.go @@ -0,0 +1,49 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package main + +import ( + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +type cmdLogout struct{} + +var shortLogoutHelp = i18n.G("Log out of the store") + +var longLogoutHelp = i18n.G("This command logs the current user out of the store") + +func init() { + addCommand("logout", + shortLogoutHelp, + longLogoutHelp, + func() flags.Commander { + return &cmdLogout{} + }, nil, nil) +} + +func (cmd *cmdLogout) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + return Client().Logout() +} diff --git a/cmd/snap/cmd_managed.go b/cmd/snap/cmd_managed.go new file mode 100644 index 00000000..27cbb944 --- /dev/null +++ b/cmd/snap/cmd_managed.go @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortIsManagedHelp = i18n.G("Prints whether system is managed") +var longIsManagedHelp = i18n.G(` +The managed command will print true or false informing whether +snapd has registered users. +`) + +type cmdIsManaged struct{} + +func init() { + cmd := addCommand("managed", shortIsManagedHelp, longIsManagedHelp, func() flags.Commander { return &cmdIsManaged{} }, nil, nil) + cmd.hidden = true +} + +func (cmd cmdIsManaged) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + sysinfo, err := Client().SysInfo() + if err != nil { + return err + } + + fmt.Fprintf(Stdout, "%v\n", sysinfo.Managed) + return nil +} diff --git a/cmd/snap/cmd_managed_test.go b/cmd/snap/cmd_managed_test.go new file mode 100644 index 00000000..95014e8d --- /dev/null +++ b/cmd/snap/cmd_managed_test.go @@ -0,0 +1,46 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestManaged(c *C) { + for _, managed := range []bool{true, false} { + s.stdout.Truncate(0) + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/system-info") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {"managed":%v}}`, managed) + }) + + _, err := snap.Parser().ParseArgs([]string{"managed"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, fmt.Sprintf("%v\n", managed)) + } +} diff --git a/cmd/snap/cmd_prepare_image.go b/cmd/snap/cmd_prepare_image.go new file mode 100644 index 00000000..6d42a001 --- /dev/null +++ b/cmd/snap/cmd_prepare_image.go @@ -0,0 +1,73 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package main + +import ( + "path/filepath" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/image" +) + +type cmdPrepareImage struct { + Positional struct { + ModelAssertionFn string + Rootdir string + } `positional-args:"yes" required:"yes"` + + ExtraSnaps []string `long:"extra-snaps"` + Channel string `long:"channel"` +} + +func init() { + cmd := addCommand("prepare-image", + i18n.G("Prepare a snappy image"), + i18n.G("Prepare a snappy image"), + func() flags.Commander { + return &cmdPrepareImage{} + }, map[string]string{ + "extra-snaps": "Extra snaps to be installed", + "channel": "The channel to use", + }, []argDesc{ + { + name: i18n.G(""), + desc: i18n.G("The model assertion name"), + }, { + name: i18n.G(""), + desc: i18n.G("The output directory"), + }, + }) + cmd.hidden = true +} + +func (x *cmdPrepareImage) Execute(args []string) error { + opts := &image.Options{ + ModelFile: x.Positional.ModelAssertionFn, + + RootDir: filepath.Join(x.Positional.Rootdir, "image"), + GadgetUnpackDir: filepath.Join(x.Positional.Rootdir, "gadget"), + Channel: x.Channel, + Snaps: x.ExtraSnaps, + } + + return image.Prepare(opts) +} diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go new file mode 100644 index 00000000..15a4c8eb --- /dev/null +++ b/cmd/snap/cmd_run.go @@ -0,0 +1,200 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + "syscall" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapenv" +) + +var ( + syscallExec = syscall.Exec + userCurrent = user.Current +) + +type cmdRun struct { + Command string `long:"command" hidden:"yes"` + Hook string `long:"hook" hidden:"yes"` + Revision string `short:"r" default:"unset" hidden:"yes"` + Shell bool `long:"shell" ` +} + +func init() { + addCommand("run", + i18n.G("Run the given snap command"), + i18n.G("Run the given snap command with the right confinement and environment"), + func() flags.Commander { + return &cmdRun{} + }, map[string]string{ + "command": i18n.G("Alternative command to run"), + "hook": i18n.G("Hook to run"), + "r": i18n.G("Use a specific snap revision when running hook"), + "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), + }, nil) +} + +func (x *cmdRun) Execute(args []string) error { + if len(args) == 0 { + return fmt.Errorf(i18n.G("need the application to run as argument")) + } + snapApp := args[0] + args = args[1:] + + // Catch some invalid parameter combinations, provide helpful errors + if x.Hook != "" && x.Command != "" { + return fmt.Errorf(i18n.G("cannot use --hook and --command together")) + } + if x.Revision != "unset" && x.Revision != "" && x.Hook == "" { + return fmt.Errorf(i18n.G("-r can only be used with --hook")) + } + if x.Hook != "" && len(args) > 0 { + // TRANSLATORS: %q is the hook name; %s a space-separated list of extra arguments + return fmt.Errorf(i18n.G("too many arguments for hook %q: %s"), x.Hook, strings.Join(args, " ")) + } + + // Now actually handle the dispatching + if x.Hook != "" { + return snapRunHook(snapApp, x.Revision, x.Hook) + } + + // pass shell as a special command to snap-exec + if x.Shell { + x.Command = "shell" + } + + return snapRunApp(snapApp, x.Command, args) +} + +func getSnapInfo(snapName string, revision snap.Revision) (*snap.Info, error) { + if revision.Unset() { + curFn := filepath.Join(dirs.SnapMountDir, snapName, "current") + realFn, err := os.Readlink(curFn) + if err != nil { + return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err) + } + rev := filepath.Base(realFn) + revision, err = snap.ParseRevision(rev) + if err != nil { + return nil, fmt.Errorf("cannot read revision %s: %s", rev, err) + } + } + + info, err := snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: revision, + }) + if err != nil { + return nil, err + } + + return info, nil +} + +func createUserDataDirs(info *snap.Info) error { + usr, err := userCurrent() + if err != nil { + return fmt.Errorf(i18n.G("cannot get the current user: %v"), err) + } + + // see snapenv.User + userData := info.UserDataDir(usr.HomeDir) + commonUserData := info.UserCommonDataDir(usr.HomeDir) + for _, d := range []string{userData, commonUserData} { + if err := os.MkdirAll(d, 0755); err != nil { + // TRANSLATORS: %q is the directory whose creation failed, %v the error message + return fmt.Errorf(i18n.G("cannot create %q: %v"), d, err) + } + } + return nil +} + +func snapRunApp(snapApp, command string, args []string) error { + snapName, appName := snap.SplitSnapApp(snapApp) + info, err := getSnapInfo(snapName, snap.R(0)) + if err != nil { + return err + } + + app := info.Apps[appName] + if app == nil { + return fmt.Errorf(i18n.G("cannot find app %q in %q"), appName, snapName) + } + + return runSnapConfine(info, app.SecurityTag(), snapApp, command, "", args) +} + +func snapRunHook(snapName, snapRevision, hookName string) error { + revision, err := snap.ParseRevision(snapRevision) + if err != nil { + return err + } + + info, err := getSnapInfo(snapName, revision) + if err != nil { + return err + } + + hook := info.Hooks[hookName] + if hook == nil { + return fmt.Errorf(i18n.G("cannot find hook %q in %q"), hookName, snapName) + } + + return runSnapConfine(info, hook.SecurityTag(), snapName, "", hook.Name, nil) +} + +func runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string, args []string) error { + if err := createUserDataDirs(info); err != nil { + logger.Noticef("WARNING: cannot create user data directory: %s", err) + } + + cmd := []string{ + filepath.Join(dirs.LibExecDir, "snap-confine"), + } + if info.NeedsClassic() { + cmd = append(cmd, "--classic") + } + cmd = append(cmd, securityTag) + cmd = append(cmd, filepath.Join(dirs.LibExecDir, "snap-exec")) + + if command != "" { + cmd = append(cmd, "--command="+command) + } + + if hook != "" { + cmd = append(cmd, "--hook="+hook) + } + + // snap-exec is POSIXly-- options must come before positionals. + cmd = append(cmd, snapApp) + cmd = append(cmd, args...) + + return syscallExec(cmd[0], cmd, snapenv.ExecEnv(info)) +} diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go new file mode 100644 index 00000000..abe012c5 --- /dev/null +++ b/cmd/snap/cmd_run_test.go @@ -0,0 +1,402 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "os" + "os/user" + "path/filepath" + + "gopkg.in/check.v1" + + snaprun "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +var mockYaml = []byte(`name: snapname +version: 1.0 +apps: + app: + command: run-app +hooks: + configure: +`) +var mockContents = "SNAP" + +func (s *SnapSuite) TestInvalidParameters(c *check.C) { + invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "snap-name"} + _, err := snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*cannot use --hook and --command together.*") + + invalidParameters = []string{"run", "-r=1", "--command=command-name", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") + + invalidParameters = []string{"run", "-r=1", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") + + invalidParameters = []string{"run", "--hook=configure", "foo", "bar", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*") +} + +func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.LibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.LibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunClassicAppIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + si := snaptest.MockSnap(c, string(mockYaml)+"confinement: classic\n", string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.LibExecDir, "snap-confine"), "--classic", + "snap.snapname.app", + filepath.Join(dirs.LibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunAppWithCommandIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + err = snaprun.SnapRunApp("snapname.app", "my-command", []string{"arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.LibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.LibExecDir, "snap-exec"), + "--command=my-command", "snapname.app", "arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunCreateDataDirs(c *check.C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, check.IsNil) + info.SideInfo.Revision = snap.R(42) + + fakeHome := c.MkDir() + restorer := snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + err = snaprun.CreateUserDataDirs(info) + c.Assert(err, check.IsNil) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/42")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/common")), check.Equals, true) +} + +func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Run a hook from the active revision + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.LibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.LibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Specifically pass "unset" which would use the active version. + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=unset", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.LibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.LibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + // Create both revisions 41 and 42 + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(41), + }) + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Run a hook on revision 41 + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.LibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.LibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41") +} + +func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + // Only create revision 42 + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + return nil + }) + defer restorer() + + // Attempt to run a hook on revision 41, which doesn't exist + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"}) + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "cannot find .*") +} + +func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "snapname"}) + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"") +} + +func (s *SnapSuite) TestSnapRunHookMissingHookIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + // Only create revision 42 + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + called := false + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + called = true + return nil + }) + defer restorer() + + err = snaprun.SnapRunHook("snapname", "unset", "missing-hook") + c.Assert(err, check.ErrorMatches, `cannot find hook "missing-hook" in "snapname"`) + c.Check(called, check.Equals, false) +} + +func (s *SnapSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--unknown", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, "unknown flag `unknown'") +} + +func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--command=shell"}) + c.Assert(err, check.ErrorMatches, "need the application to run as argument") +} + +func (s *SnapSuite) TestSnapRunErorrForUnavailableApp(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "not-there"}) + c.Assert(err, check.ErrorMatches, "cannot find current revision for snap not-there: readlink /snap/not-there/current: no such file or directory") +} + +func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execEnv = envv + return nil + }) + defer restorer() + + // set a SNAP{,_*} variable in the environment + os.Setenv("SNAP_NAME", "something-else") + os.Setenv("SNAP_ARCH", "PDP-7") + defer os.Unsetenv("SNAP_NAME") + defer os.Unsetenv("SNAP_ARCH") + // but unrelated stuff is ok + os.Setenv("SNAP_THE_WORLD", "YES") + defer os.Unsetenv("SNAP_THE_WORLD") + + // and ensure those SNAP_ vars get overridden + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") + c.Check(execEnv, check.Not(testutil.Contains), "SNAP_NAME=something-else") + c.Check(execEnv, check.Not(testutil.Contains), "SNAP_ARCH=PDP-7") + c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES") +} diff --git a/cmd/snap/cmd_set.go b/cmd/snap/cmd_set.go new file mode 100644 index 00000000..5a087f38 --- /dev/null +++ b/cmd/snap/cmd_set.go @@ -0,0 +1,94 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +var shortSetHelp = i18n.G("Changes configuration options") +var longSetHelp = i18n.G(` +The set command changes the provided configuration options as requested. + + $ snap set snap-name username=frank password=$PASSWORD + +All configuration changes are persisted at once, and only after the +snap's configuration hook returns successfully. + +Nested values may be modified via a dotted path: + + $ snap set author.name=frank +`) + +type cmdSet struct { + Positional struct { + Snap installedSnapName + ConfValues []string `required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func init() { + addCommand("set", shortSetHelp, longSetHelp, func() flags.Commander { return &cmdSet{} }, nil, []argDesc{ + { + name: "", + desc: i18n.G("The snap to configure (e.g. hello-world)"), + }, { + name: i18n.G(""), + desc: i18n.G("Configuration value (key=value)"), + }, + }) +} + +func (x *cmdSet) Execute(args []string) error { + patchValues := make(map[string]interface{}) + for _, patchValue := range x.Positional.ConfValues { + parts := strings.SplitN(patchValue, "=", 2) + if len(parts) != 2 { + return fmt.Errorf(i18n.G("invalid configuration: %q (want key=value)"), patchValue) + } + var value interface{} + err := json.Unmarshal([]byte(parts[1]), &value) + if err == nil { + patchValues[parts[0]] = value + } else { + // Not valid JSON-- just save the string as-is. + patchValues[parts[0]] = parts[1] + } + } + + return configure(string(x.Positional.Snap), patchValues) +} + +func configure(snapName string, patchValues map[string]interface{}) error { + cli := Client() + id, err := cli.SetConf(snapName, patchValues) + if err != nil { + return err + } + + _, err = wait(cli, id) + return err +} diff --git a/cmd/snap/cmd_set_test.go b/cmd/snap/cmd_set_test.go new file mode 100644 index 00000000..676c1b42 --- /dev/null +++ b/cmd/snap/cmd_set_test.go @@ -0,0 +1,114 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snapset "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" +) + +var validApplyYaml = []byte(`name: snapname +version: 1.0 +hooks: + configure: +`) +var validApplyContents = "" + +func (s *SnapSuite) TestInvalidSetParameters(c *check.C) { + invalidParameters := []string{"set", "snap-name", "key", "value"} + _, err := snapset.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*invalid configuration:.*(want key=value).*") +} + +func (s *SnapSuite) TestSnapSetIntegrationString(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockSetConfigServer(c, "value") + + // Set a config value for the active snap + _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", "key=value"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) TestSnapSetIntegrationNumber(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockSetConfigServer(c, 1.2) + + // Set a config value for the active snap + _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", "key=1.2"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) TestSnapSetIntegrationJson(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockSetConfigServer(c, map[string]interface{}{"subkey": "value"}) + + // Set a config value for the active snap + _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", `key={"subkey":"value"}`}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) mockSetConfigServer(c *check.C, expectedValue interface{}) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/snaps/snapname/conf": + c.Check(r.Method, check.Equals, "PUT") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "key": expectedValue, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, check.Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) +} diff --git a/cmd/snap/cmd_shell.go b/cmd/snap/cmd_shell.go new file mode 100644 index 00000000..88e9b4f1 --- /dev/null +++ b/cmd/snap/cmd_shell.go @@ -0,0 +1,98 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "syscall" + + //"github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" +) + +type cmdShell struct { + Positional struct { + ShellType string + } `positional-args:"yes"` +} + +// FIXME: reenable for GA +/* +func init() { + addCommand("shell", + i18n.G("Run snappy shell interface"), + i18n.G("Run snappy shell interface"), + func() flags.Commander { + return &cmdShell{} + }, nil, []argDesc{{ + name: i18n.G(""), + desc: i18n.G("The type of shell you want"), + }}) +} +*/ + +// reexec will reexec itself with sudo +func reexecWithSudo() error { + args := []string{"/usr/bin/sudo"} + args = append(args, os.Args...) + env := os.Environ() + if err := syscall.Exec(args[0], args, env); err != nil { + return fmt.Errorf("failed to exec classic shell: %s", err) + } + panic("this should never be reached") +} + +func (x *cmdShell) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + shellType := x.Positional.ShellType + + // FIXME: make this generic so that all snaps can provide a + // shell + if shellType == "classic" { + if !osutil.FileExists("/snap/classic/current") { + return fmt.Errorf(i18n.G(`Classic dimension disabled on this system. +Use "sudo snap install --devmode classic && sudo classic.create" to enable it.`)) + } + + // we need to re-exec if we do not run as root + if os.Getuid() != 0 { + if err := reexecWithSudo(); err != nil { + return err + } + } + + fmt.Fprintln(Stdout, i18n.G(`Entering classic dimension`)) + fmt.Fprintln(Stdout, i18n.G(` + +The home directory is shared between snappy and the classic dimension. +Run "exit" to leave the classic shell. +`)) + args := []string{"/snap/bin/classic.shell"} + return syscall.Exec(args[0], args, os.Environ()) + } + + return fmt.Errorf(i18n.G("unsupported shell %v"), shellType) +} diff --git a/cmd/snap/cmd_sign.go b/cmd/snap/cmd_sign.go new file mode 100644 index 00000000..4bf710bf --- /dev/null +++ b/cmd/snap/cmd_sign.go @@ -0,0 +1,79 @@ +// -*- 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 . + * + */ + +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/signtool" + "github.com/snapcore/snapd/i18n" +) + +var shortSignHelp = i18n.G("Sign an assertion") +var longSignHelp = i18n.G(`Sign an assertion using the specified key, using the input for headers from a JSON mapping provided through stdin, the body of the assertion can be specified through a "body" pseudo-header. +`) + +type cmdSign struct { + KeyName keyName `short:"k" default:"default"` +} + +func init() { + cmd := addCommand("sign", shortSignHelp, longSignHelp, func() flags.Commander { + return &cmdSign{} + }, map[string]string{"k": i18n.G("Name of the key to use, otherwise use the default key")}, nil) + cmd.hidden = true +} + +func (x *cmdSign) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + statement, err := ioutil.ReadAll(Stdin) + if err != nil { + return fmt.Errorf(i18n.G("cannot read assertion input: %v"), err) + } + + keypairMgr := asserts.NewGPGKeypairManager() + privKey, err := keypairMgr.GetByName(string(x.KeyName)) + if err != nil { + return err + } + + signOpts := signtool.Options{ + KeyID: privKey.PublicKey().ID(), + Statement: statement, + } + + encodedAssert, err := signtool.Sign(&signOpts, keypairMgr) + if err != nil { + return err + } + + _, err = Stdout.Write(encodedAssert) + if err != nil { + return err + } + return nil +} diff --git a/cmd/snap/cmd_sign_build.go b/cmd/snap/cmd_sign_build.go new file mode 100644 index 00000000..815501bc --- /dev/null +++ b/cmd/snap/cmd_sign_build.go @@ -0,0 +1,115 @@ +// -*- 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 . + * + */ + +package main + +import ( + "fmt" + "time" + + _ "golang.org/x/crypto/sha3" // expected for digests + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" +) + +type cmdSignBuild struct { + Positional struct { + Filename string + } `positional-args:"yes" required:"yes"` + + // XXX complete DeveloperID and SnapID + DeveloperID string `long:"developer-id" required:"yes"` + SnapID string `long:"snap-id" required:"yes"` + KeyName keyName `short:"k" default:"default" ` + Grade string `long:"grade" choice:"devel" choice:"stable" default:"stable"` +} + +var shortSignBuildHelp = i18n.G("Create snap build assertion") +var longSignBuildHelp = i18n.G("Create snap-build assertion for the provided snap file.") + +func init() { + cmd := addCommand("sign-build", + shortSignBuildHelp, + longSignBuildHelp, + func() flags.Commander { + return &cmdSignBuild{} + }, map[string]string{ + "developer-id": i18n.G("Identifier of the signer"), + "snap-id": i18n.G("Identifier of the snap package associated with the build"), + "k": i18n.G("Name of the GnuPG key to use (defaults to 'default' as key name)"), + "grade": i18n.G("Grade states the build quality of the snap (defaults to 'stable')"), + }, []argDesc{{ + name: i18n.G(""), + desc: i18n.G("Filename of the snap you want to assert a build for"), + }}) + cmd.hidden = true +} + +func (x *cmdSignBuild) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapDigest, snapSize, err := asserts.SnapFileSHA3_384(x.Positional.Filename) + if err != nil { + return err + } + + gkm := asserts.NewGPGKeypairManager() + privKey, err := gkm.GetByName(string(x.KeyName)) + if err != nil { + // TRANSLATORS: %q is the key name, %v the error message + return fmt.Errorf(i18n.G("cannot use %q key: %v"), x.KeyName, err) + } + + pubKey := privKey.PublicKey() + timestamp := time.Now().Format(time.RFC3339) + + headers := map[string]interface{}{ + "developer-id": x.DeveloperID, + "authority-id": x.DeveloperID, + "snap-sha3-384": snapDigest, + "snap-id": x.SnapID, + "snap-size": fmt.Sprintf("%d", snapSize), + "grade": x.Grade, + "timestamp": timestamp, + } + + adb, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: gkm, + }) + if err != nil { + return fmt.Errorf(i18n.G("cannot open the assertions database: %v"), err) + } + + a, err := adb.Sign(asserts.SnapBuildType, headers, nil, pubKey.ID()) + if err != nil { + return fmt.Errorf(i18n.G("cannot sign assertion: %v"), err) + } + + _, err = Stdout.Write(asserts.Encode(a)) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/snap/cmd_sign_build_test.go b/cmd/snap/cmd_sign_build_test.go new file mode 100644 index 00000000..09ea2744 --- /dev/null +++ b/cmd/snap/cmd_sign_build_test.go @@ -0,0 +1,134 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + snap "github.com/snapcore/snapd/cmd/snap" +) + +type SnapSignBuildSuite struct { + BaseSnapSuite +} + +var _ = Suite(&SnapSignBuildSuite{}) + +func (s *SnapSignBuildSuite) TestSignBuildMandatoryFlags(c *C) { + _, err := snap.Parser().ParseArgs([]string{"sign-build", "foo_1_amd64.snap"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "the required flags `--developer-id' and `--snap-id' were not specified") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSignBuildSuite) TestSignBuildMissingSnap(c *C) { + _, err := snap.Parser().ParseArgs([]string{"sign-build", "foo_1_amd64.snap", "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "cannot compute snap \"foo_1_amd64.snap\" digest: open foo_1_amd64.snap: no such file or directory") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSignBuildSuite) TestSignBuildMissingKey(c *C) { + snapFilename := "foo_1_amd64.snap" + _err := ioutil.WriteFile(snapFilename, []byte("sample"), 0644) + c.Assert(_err, IsNil) + defer os.Remove(snapFilename) + + tempdir := c.MkDir() + os.Setenv("SNAP_GNUPG_HOME", tempdir) + defer os.Unsetenv("SNAP_GNUPG_HOME") + + _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "cannot use \"default\" key: cannot find key named \"default\" in GPG keyring") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSignBuildSuite) TestSignBuildWorks(c *C) { + snapFilename := "foo_1_amd64.snap" + snapContent := []byte("sample") + _err := ioutil.WriteFile(snapFilename, snapContent, 0644) + c.Assert(_err, IsNil) + defer os.Remove(snapFilename) + + tempdir := c.MkDir() + for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} { + data, err := ioutil.ReadFile(filepath.Join("test-data", fileName)) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644) + c.Assert(err, IsNil) + } + os.Setenv("SNAP_GNUPG_HOME", tempdir) + defer os.Unsetenv("SNAP_GNUPG_HOME") + + _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) + c.Assert(err, IsNil) + + assertion, err := asserts.Decode([]byte(s.Stdout())) + c.Assert(err, IsNil) + c.Check(assertion.Type(), Equals, asserts.SnapBuildType) + c.Check(assertion.Revision(), Equals, 0) + c.Check(assertion.HeaderString("authority-id"), Equals, "dev-id1") + c.Check(assertion.HeaderString("developer-id"), Equals, "dev-id1") + c.Check(assertion.HeaderString("grade"), Equals, "stable") + c.Check(assertion.HeaderString("snap-id"), Equals, "snap-id-1") + c.Check(assertion.HeaderString("snap-size"), Equals, fmt.Sprintf("%d", len(snapContent))) + c.Check(assertion.HeaderString("snap-sha3-384"), Equals, "jyP7dUgb8HiRNd1SdYPp_il-YNrl6P6PgNAe-j6_7WytjKslENhMD3Of5XBU5bQK") + + // check for valid signature ?! + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSignBuildSuite) TestSignBuildWorksDevelGrade(c *C) { + snapFilename := "foo_1_amd64.snap" + snapContent := []byte("sample") + _err := ioutil.WriteFile(snapFilename, snapContent, 0644) + c.Assert(_err, IsNil) + defer os.Remove(snapFilename) + + tempdir := c.MkDir() + for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} { + data, err := ioutil.ReadFile(filepath.Join("test-data", fileName)) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644) + c.Assert(err, IsNil) + } + os.Setenv("SNAP_GNUPG_HOME", tempdir) + defer os.Unsetenv("SNAP_GNUPG_HOME") + + _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1", "--grade", "devel"}) + c.Assert(err, IsNil) + assertion, err := asserts.Decode([]byte(s.Stdout())) + c.Assert(err, IsNil) + c.Check(assertion.Type(), Equals, asserts.SnapBuildType) + c.Check(assertion.HeaderString("grade"), Equals, "devel") + + // check for valid signature ?! + c.Check(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_sign_test.go b/cmd/snap/cmd_sign_test.go new file mode 100644 index 00000000..341551e7 --- /dev/null +++ b/cmd/snap/cmd_sign_test.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +var statement = []byte(fmt.Sprintf(`{"type": "snap-build", +"authority-id": "devel1", +"series": "16", +"snap-id": "snapidsnapidsnapidsnapidsnapidsn", +"snap-sha3-384": "QlqR0uAWEAWF5Nwnzj5kqmmwFslYPu1IL16MKtLKhwhv0kpBv5wKZ_axf_nf_2cL", +"snap-size": "1", +"grade": "devel", +"timestamp": %q +}`, time.Now().Format(time.RFC3339))) + +func (s *SnapKeysSuite) TestHappyDefaultKey(c *C) { + s.stdin.Write(statement) + + rest, err := snap.Parser().ParseArgs([]string{"sign"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + a, err := asserts.Decode(s.stdout.Bytes()) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapBuildType) +} + +func (s *SnapKeysSuite) TestHappyNonDefaultKey(c *C) { + s.stdin.Write(statement) + + rest, err := snap.Parser().ParseArgs([]string{"sign", "-k", "another"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + a, err := asserts.Decode(s.stdout.Bytes()) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapBuildType) +} diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go new file mode 100644 index 00000000..3dcbb1cd --- /dev/null +++ b/cmd/snap/cmd_snap_op.go @@ -0,0 +1,837 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "errors" + "fmt" + "os" + "os/signal" + "path/filepath" + "sort" + "strings" + "syscall" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/progress" +) + +func lastLogStr(logs []string) string { + if len(logs) == 0 { + return "" + } + return logs[len(logs)-1] +} + +var ( + maxGoneTime = 5 * time.Second + pollTime = 100 * time.Millisecond +) + +func wait(cli *client.Client, id string) (*client.Change, error) { + pb := progress.NewTextProgress() + defer func() { + pb.Finished() + }() + + tMax := time.Time{} + + var lastID string + lastLog := map[string]string{} + for { + chg, err := cli.Change(id) + if err != nil { + // a client.Error means we were able to communicate with + // the server (got an answer) + if e, ok := err.(*client.Error); ok { + return nil, e + } + + // an non-client error here means the server most + // likely went away + // XXX: it actually can be a bunch of other things; fix client to expose it better + now := time.Now() + if tMax.IsZero() { + tMax = now.Add(maxGoneTime) + } + if now.After(tMax) { + return nil, err + } + pb.Spin(i18n.G("Waiting for server to restart")) + time.Sleep(pollTime) + continue + } + if !tMax.IsZero() { + pb.Finished() + tMax = time.Time{} + } + + for _, t := range chg.Tasks { + switch { + case t.Status != "Doing": + continue + case t.Progress.Total == 1: + pb.Spin(t.Summary) + nowLog := lastLogStr(t.Log) + if lastLog[t.ID] != nowLog { + pb.Notify(nowLog) + lastLog[t.ID] = nowLog + } + case t.ID == lastID: + pb.Set(float64(t.Progress.Done)) + default: + pb.Start(t.Progress.Label, float64(t.Progress.Total)) + lastID = t.ID + } + break + } + + if chg.Ready { + if chg.Status == "Done" { + return chg, nil + } + + if chg.Err != "" { + return chg, errors.New(chg.Err) + } + + return nil, fmt.Errorf(i18n.G("change finished in status %q with no error message"), chg.Status) + } + + // note this very purposely is not a ticker; we want + // to sleep 100ms between calls, not call once every + // 100ms. + time.Sleep(pollTime) + } +} + +var ( + shortInstallHelp = i18n.G("Installs a snap to the system") + shortRemoveHelp = i18n.G("Removes a snap from the system") + shortRefreshHelp = i18n.G("Refreshes a snap in the system") + shortTryHelp = i18n.G("Tests a snap in the system") + shortEnableHelp = i18n.G("Enables a snap in the system") + shortDisableHelp = i18n.G("Disables a snap in the system") +) + +var longInstallHelp = i18n.G(` +The install command installs the named snap in the system. +`) + +var longRemoveHelp = i18n.G(` +The remove command removes the named snap from the system. + +By default all the snap revisions are removed, including their data and the common +data directory. When a --revision option is passed only the specified revision is +removed. +`) + +var longRefreshHelp = i18n.G(` +The refresh command refreshes (updates) the named snap. +`) + +var longTryHelp = i18n.G(` +The try command installs an unpacked snap into the system for testing purposes. +The unpacked snap content continues to be used even after installation, so +non-metadata changes there go live instantly. Metadata changes such as those +performed in snap.yaml will require reinstallation to go live. + +If snap-dir argument is omitted, the try command will attempt to infer it if +either snapcraft.yaml file and prime directory or meta/snap.yaml file can be +found relative to current working directory. +`) + +var longEnableHelp = i18n.G(` +The enable command enables a snap that was previously disabled. +`) + +var longDisableHelp = i18n.G(` +The disable command disables a snap. The binaries and services of the +snap will no longer be available. But all the data is still available +and the snap can easily be enabled again. +`) + +type cmdRemove struct { + Revision string `long:"revision"` + Positional struct { + Snaps []installedSnapName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func (x *cmdRemove) removeOne(opts *client.SnapOptions) error { + name := x.Positional.Snaps[0] + + cli := Client() + changeID, err := cli.Remove(string(name), opts) + if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindSnapNotInstalled { + fmt.Fprintf(Stderr, e.Message+"\n") + return nil + } + if err != nil { + return err + } + + if _, err := wait(cli, changeID); err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name) + return nil +} + +func (x *cmdRemove) removeMany(opts *client.SnapOptions) error { + names := make([]string, len(x.Positional.Snaps)) + for i, s := range x.Positional.Snaps { + names[i] = string(s) + } + + cli := Client() + changeID, err := cli.RemoveMany(names, opts) + if err != nil { + return err + } + + chg, err := wait(cli, changeID) + if err != nil { + return err + } + + var removed []string + if err := chg.Get("snap-names", &removed); err != nil && err != client.ErrNoData { + return err + } + + seen := make(map[string]bool) + for _, name := range removed { + fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name) + seen[name] = true + } + for _, name := range names { + if !seen[name] { + // FIXME: this is the only reason why a name can be + // skipped, but it does feel awkward + fmt.Fprintf(Stdout, i18n.G("%s not installed\n"), name) + } + } + + return nil + +} + +func (x *cmdRemove) Execute([]string) error { + opts := &client.SnapOptions{Revision: x.Revision} + if len(x.Positional.Snaps) == 1 { + return x.removeOne(opts) + } + + if x.Revision != "" { + return errors.New(i18n.G("a single snap name is needed to specify the revision")) + } + return x.removeMany(nil) +} + +type channelMixin struct { + Channel string `long:"channel"` + + // shortcuts + EdgeChannel bool `long:"edge"` + BetaChannel bool `long:"beta"` + CandidateChannel bool `long:"candidate"` + StableChannel bool `long:"stable" ` +} + +type mixinDescs map[string]string + +func (mxd mixinDescs) also(m map[string]string) mixinDescs { + n := make(map[string]string, len(mxd)+len(m)) + for k, v := range mxd { + n[k] = v + } + for k, v := range m { + n[k] = v + } + return n +} + +var channelDescs = mixinDescs{ + "channel": i18n.G("Use this channel instead of stable"), + "beta": i18n.G("Install from the beta channel"), + "edge": i18n.G("Install from the edge channel"), + "candidate": i18n.G("Install from the candidate channel"), + "stable": i18n.G("Install from the stable channel"), +} + +func (mx *channelMixin) setChannelFromCommandline() error { + for _, ch := range []struct { + enabled bool + chName string + }{ + {mx.StableChannel, "stable"}, + {mx.CandidateChannel, "candidate"}, + {mx.BetaChannel, "beta"}, + {mx.EdgeChannel, "edge"}, + } { + if !ch.enabled { + continue + } + if mx.Channel != "" { + return fmt.Errorf("Please specify a single channel") + } + mx.Channel = ch.chName + } + + return nil +} + +// show what has been done +func showDone(names []string, op string) error { + cli := Client() + snaps, err := cli.List(names, nil) + if err != nil { + return err + } + + for _, snap := range snaps { + channelStr := "" + if snap.Channel != "" && snap.Channel != "stable" { + channelStr = fmt.Sprintf(" (%s)", snap.Channel) + } + switch op { + case "install": + if snap.Developer != "" { + fmt.Fprintf(Stdout, i18n.G("%s%s %s from '%s' installed\n"), snap.Name, channelStr, snap.Version, snap.Developer) + } else { + fmt.Fprintf(Stdout, i18n.G("%s%s %s installed\n"), snap.Name, channelStr, snap.Version) + } + case "refresh": + if snap.Developer != "" { + fmt.Fprintf(Stdout, i18n.G("%s%s %s from '%s' refreshed\n"), snap.Name, channelStr, snap.Version, snap.Developer) + } else { + fmt.Fprintf(Stdout, i18n.G("%s%s %s refreshed\n"), snap.Name, channelStr, snap.Version) + } + default: + fmt.Fprintf(Stdout, "internal error, unknown op %q", op) + } + } + return nil +} + +func (mx *channelMixin) asksForChannel() bool { + return mx.Channel != "" +} + +type modeMixin struct { + DevMode bool `long:"devmode"` + JailMode bool `long:"jailmode"` + Classic bool `long:"classic"` +} + +var modeDescs = mixinDescs{ + "classic": i18n.G("Put snap in classic mode and disable security confinement"), + "devmode": i18n.G("Put snap in development mode and disable security confinement"), + "jailmode": i18n.G("Put snap in enforced confinement mode"), +} + +var errModeConflict = errors.New(i18n.G("cannot use devmode and jailmode flags together")) + +func (mx modeMixin) validateMode() error { + if mx.DevMode && mx.JailMode { + return errModeConflict + } + return nil +} + +func (mx modeMixin) asksForMode() bool { + return mx.DevMode || mx.JailMode +} + +type cmdInstall struct { + channelMixin + modeMixin + Revision string `long:"revision"` + + Dangerous bool `long:"dangerous"` + // alias for --dangerous, deprecated but we need to support it + // because we released 2.14.2 with --force-dangerous + ForceDangerous bool `long:"force-dangerous" hidden:"yes"` + + Positional struct { + Snaps []remoteSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func setupAbortHandler(changeId string) { + // Intercept sigint + c := make(chan os.Signal, 2) + signal.Notify(c, syscall.SIGINT) + go func() { + <-c + cli := Client() + _, err := cli.Abort(changeId) + if err != nil { + fmt.Fprintf(Stderr, err.Error()+"\n") + } + }() +} + +func (x *cmdInstall) installOne(name string, opts *client.SnapOptions) error { + var err error + var installFromFile bool + var changeID string + + cli := Client() + if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") { + installFromFile = true + changeID, err = cli.InstallPath(name, opts) + } else { + changeID, err = cli.Install(name, opts) + } + if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindSnapAlreadyInstalled { + fmt.Fprintf(Stderr, e.Message+"\n") + return nil + } + if err != nil { + return err + } + + setupAbortHandler(changeID) + + chg, err := wait(cli, changeID) + if err != nil { + return err + } + + // extract the snapName from the change, important for sideloaded + var snapName string + + if installFromFile { + if err := chg.Get("snap-name", &snapName); err != nil { + return fmt.Errorf("cannot extract the snap-name from local file %q: %s", name, err) + } + name = snapName + } + + return showDone([]string{name}, "install") +} + +func (x *cmdInstall) installMany(names []string, opts *client.SnapOptions) error { + // sanity check + for _, name := range names { + if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") { + return fmt.Errorf("only one snap file can be installed at a time") + } + } + + cli := Client() + changeID, err := cli.InstallMany(names, opts) + if err != nil { + return err + } + + setupAbortHandler(changeID) + + chg, err := wait(cli, changeID) + if err != nil { + return err + } + + var installed []string + if err := chg.Get("snap-names", &installed); err != nil && err != client.ErrNoData { + return err + } + + if len(installed) > 0 { + if err := showDone(installed, "install"); err != nil { + return err + } + } + + // show skipped + seen := make(map[string]bool) + for _, name := range installed { + seen[name] = true + } + for _, name := range names { + if !seen[name] { + // FIXME: this is the only reason why a name can be + // skipped, but it does feel awkward + fmt.Fprintf(Stdout, i18n.G("%s already installed\n"), name) + } + } + + return nil +} + +func (x *cmdInstall) Execute([]string) error { + if err := x.setChannelFromCommandline(); err != nil { + return err + } + if err := x.validateMode(); err != nil { + return err + } + + dangerous := x.Dangerous || x.ForceDangerous + opts := &client.SnapOptions{ + Channel: x.Channel, + DevMode: x.DevMode, + JailMode: x.JailMode, + Classic: x.Classic, + Revision: x.Revision, + Dangerous: dangerous, + } + + names := make([]string, len(x.Positional.Snaps)) + for i, name := range x.Positional.Snaps { + names[i] = string(name) + } + + if len(names) == 1 { + return x.installOne(names[0], opts) + } + + if x.asksForMode() || x.asksForChannel() { + return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags")) + } + + return x.installMany(names, nil) +} + +type cmdRefresh struct { + channelMixin + modeMixin + + Revision string `long:"revision"` + List bool `long:"list"` + IgnoreValidation bool `long:"ignore-validation"` + Positional struct { + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` +} + +func refreshMany(snaps []string, opts *client.SnapOptions) error { + cli := Client() + changeID, err := cli.RefreshMany(snaps, opts) + if err != nil { + return err + } + + chg, err := wait(cli, changeID) + if err != nil { + return err + } + + var refreshed []string + if err := chg.Get("snap-names", &refreshed); err != nil && err != client.ErrNoData { + return err + } + + if len(refreshed) > 0 { + return showDone(refreshed, "refresh") + } + + fmt.Fprintln(Stderr, i18n.G("All snaps up to date.")) + + return nil +} + +func refreshOne(name string, opts *client.SnapOptions) error { + cli := Client() + changeID, err := cli.Refresh(name, opts) + if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindNoUpdateAvailable { + fmt.Fprintf(Stderr, e.Message+"\n") + return nil + } + if err != nil { + return err + } + + if _, err := wait(cli, changeID); err != nil { + return err + } + + return showDone([]string{name}, "refresh") +} + +func listRefresh() error { + cli := Client() + snaps, _, err := cli.Find(&client.FindOptions{ + Refresh: true, + }) + if err != nil { + return err + } + if len(snaps) == 0 { + fmt.Fprintln(Stderr, i18n.G("All snaps up to date.")) + return nil + } + + sort.Sort(snapsByName(snaps)) + + w := tabWriter() + defer w.Flush() + + fmt.Fprintln(w, i18n.G("Name\tVersion\tRev\tDeveloper\tNotes")) + for _, snap := range snaps { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Revision, snap.Developer, NotesFromRemote(snap, nil)) + } + + return nil +} + +func (x *cmdRefresh) Execute([]string) error { + if err := x.setChannelFromCommandline(); err != nil { + return err + } + if err := x.validateMode(); err != nil { + return err + } + + if x.List { + if x.asksForMode() || x.asksForChannel() { + return errors.New(i18n.G("--list does not take mode nor channel flags")) + } + + return listRefresh() + } + names := make([]string, len(x.Positional.Snaps)) + for i, name := range x.Positional.Snaps { + names[i] = string(name) + } + if len(x.Positional.Snaps) == 1 { + opts := &client.SnapOptions{ + Channel: x.Channel, + DevMode: x.DevMode, + Classic: x.Classic, + JailMode: x.JailMode, + IgnoreValidation: x.IgnoreValidation, + Revision: x.Revision, + } + return refreshOne(names[0], opts) + } + + if x.asksForMode() || x.asksForChannel() { + return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags")) + } + + if x.IgnoreValidation { + return errors.New(i18n.G("a single snap name must be specified when ignoring validation")) + } + + return refreshMany(names, nil) +} + +type cmdTry struct { + modeMixin + Positional struct { + SnapDir string `positional-arg-name:""` + } `positional-args:"yes"` +} + +func (x *cmdTry) Execute([]string) error { + if err := x.validateMode(); err != nil { + return err + } + cli := Client() + name := x.Positional.SnapDir + opts := &client.SnapOptions{ + DevMode: x.DevMode, + JailMode: x.JailMode, + } + + if name == "" { + if osutil.FileExists("snapcraft.yaml") && osutil.IsDirectory("prime") { + name = "prime" + } else { + if osutil.FileExists("meta/snap.yaml") { + name = "./" + } + } + if name == "" { + return fmt.Errorf(i18n.G("error: the `` argument was not provided and couldn't be inferred")) + } + } + + path, err := filepath.Abs(name) + if err != nil { + // TRANSLATORS: %q gets what the user entered, %v gets the resulting error message + return fmt.Errorf(i18n.G("cannot get full path for %q: %v"), name, err) + } + + changeID, err := cli.Try(path, opts) + if err != nil { + return err + } + + chg, err := wait(cli, changeID) + if err != nil { + return err + } + + // extract the snap name + var snapName string + if err := chg.Get("snap-name", &snapName); err != nil { + // TRANSLATORS: %q gets the snap name, %v gets the resulting error message + return fmt.Errorf(i18n.G("cannot extract the snap-name from local file %q: %v"), name, err) + } + name = snapName + + // show output as speced + snaps, err := cli.List([]string{name}, nil) + if err != nil { + return err + } + if len(snaps) != 1 { + // TRANSLATORS: %q gets the snap name, %v the list of things found when trying to list it + return fmt.Errorf(i18n.G("cannot get data for %q: %v"), name, snaps) + } + snap := snaps[0] + // TRANSLATORS: 1. snap name, 2. snap version (keep those together please). the 3rd %s is a path (where it's mounted from). + fmt.Fprintf(Stdout, i18n.G("%s %s mounted from %s\n"), name, snap.Version, path) + return nil +} + +type cmdEnable struct { + Positional struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *cmdEnable) Execute([]string) error { + cli := Client() + name := string(x.Positional.Snap) + opts := &client.SnapOptions{} + changeID, err := cli.Enable(name, opts) + if err != nil { + return err + } + + _, err = wait(cli, changeID) + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("%s enabled\n"), name) + return nil +} + +type cmdDisable struct { + Positional struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *cmdDisable) Execute([]string) error { + cli := Client() + name := string(x.Positional.Snap) + opts := &client.SnapOptions{} + changeID, err := cli.Disable(name, opts) + if err != nil { + return err + } + + _, err = wait(cli, changeID) + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("%s disabled\n"), name) + return nil +} + +type cmdRevert struct { + modeMixin + Revision string `long:"revision"` + Positional struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` +} + +var shortRevertHelp = i18n.G("Reverts the given snap to the previous state") +var longRevertHelp = i18n.G(` +The revert command reverts the given snap to its state before +the latest refresh. This will reactivate the previous snap revision, +and will use the original data that was associated with that revision, +discarding any data changes that were done by the latest revision. As +an exception, data which the snap explicitly chooses to share across +revisions is not touched by the revert process. +`) + +func (x *cmdRevert) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if err := x.validateMode(); err != nil { + return err + } + + cli := Client() + name := string(x.Positional.Snap) + opts := &client.SnapOptions{DevMode: x.DevMode, JailMode: x.JailMode, Revision: x.Revision} + changeID, err := cli.Revert(name, opts) + if err != nil { + return err + } + + if _, err := wait(cli, changeID); err != nil { + return err + } + + // show output as speced + snaps, err := cli.List([]string{name}, nil) + if err != nil { + return err + } + if len(snaps) != 1 { + // TRANSLATORS: %q gets the snap name, %v the list of things found when trying to list it + return fmt.Errorf(i18n.G("cannot get data for %q: %v"), name, snaps) + } + snap := snaps[0] + fmt.Fprintf(Stdout, i18n.G("%s reverted to %s\n"), name, snap.Version) + return nil +} + +func init() { + addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} }, + map[string]string{"revision": i18n.G("Remove only the given revision")}, nil) + addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} }, + channelDescs.also(modeDescs).also(map[string]string{ + "revision": i18n.G("Install the given revision of a snap, to which you must have developer access"), + "dangerous": i18n.G("Install the given snap file even if there are no pre-acknowledged signatures for it, meaning it was not verified and could be dangerous (--devmode implies this)"), + "force-dangerous": i18n.G("Alias for --dangerous (DEPRECATED)"), + }), nil) + addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} }, + channelDescs.also(modeDescs).also(map[string]string{ + "revision": i18n.G("Refresh to the given revision"), + "list": i18n.G("Show available snaps for refresh"), + "ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"), + }), nil) + addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, modeDescs, nil) + addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, nil, nil) + addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} }, nil, nil) + addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, modeDescs.also(map[string]string{ + "revision": "Revert to the given revision", + }), nil) +} diff --git a/cmd/snap/cmd_snap_op_test.go b/cmd/snap/cmd_snap_op_test.go new file mode 100644 index 00000000..e90d9e35 --- /dev/null +++ b/cmd/snap/cmd_snap_op_test.go @@ -0,0 +1,730 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "regexp" + "strconv" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +type snapOpTestServer struct { + c *check.C + + checker func(r *http.Request) + n int + total int + channel string +} + +var _ = check.Suite(&SnapOpSuite{}) + +func (t *snapOpTestServer) handle(w http.ResponseWriter, r *http.Request) { + switch t.n { + case 0: + t.checker(r) + t.c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(http.StatusAccepted) + fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) + case 1: + t.c.Check(r.Method, check.Equals, "GET") + t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + case 2: + t.c.Check(r.Method, check.Equals, "GET") + t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-name": "foo"}}}`) + case 3: + t.c.Check(r.Method, check.Equals, "GET") + t.c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "1.0", "developer": "bar", "revision":42, "channel":"%s"}]}\n`, t.channel) + default: + t.c.Fatalf("expected to get %d requests, now on %d", t.total, t.n+1) + } + + t.n++ +} + +type SnapOpSuite struct { + BaseSnapSuite + + restoreAll func() + srv snapOpTestServer +} + +func (s *SnapOpSuite) SetUpTest(c *check.C) { + s.BaseSnapSuite.SetUpTest(c) + + restoreClientRetry := client.MockDoRetry(time.Millisecond, 10*time.Millisecond) + restorePollTime := snap.MockPollTime(time.Millisecond) + s.restoreAll = func() { + restoreClientRetry() + restorePollTime() + } + + s.srv = snapOpTestServer{ + c: c, + total: 4, + } +} + +func (s *SnapOpSuite) TearDownTest(c *check.C) { + s.restoreAll() + s.BaseSnapSuite.TearDownTest(c) +} + +func (s *SnapOpSuite) TestWait(c *check.C) { + restore := snap.MockMaxGoneTime(time.Millisecond) + defer restore() + + // lazy way of getting a URL that won't work nor break stuff + server := httptest.NewServer(nil) + snap.ClientConfig.BaseURL = server.URL + server.Close() + + d := c.MkDir() + oldStdout := os.Stdout + stdout, err := ioutil.TempFile(d, "stdout") + c.Assert(err, check.IsNil) + defer func() { + os.Stdout = oldStdout + stdout.Close() + os.Remove(stdout.Name()) + }() + os.Stdout = stdout + + cli := snap.Client() + chg, err := snap.Wait(cli, "x") + c.Assert(chg, check.IsNil) + c.Assert(err, check.NotNil) + buf, err := ioutil.ReadFile(stdout.Name()) + c.Assert(err, check.IsNil) + c.Check(string(buf), check.Matches, "(?ms).*Waiting for server to restart.*") +} + +func (s *SnapOpSuite) TestWaitRecovers(c *check.C) { + restore := snap.MockMaxGoneTime(time.Millisecond) + defer restore() + + nah := true + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + if nah { + nah = false + return + } + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) + }) + + d := c.MkDir() + oldStdout := os.Stdout + stdout, err := ioutil.TempFile(d, "stdout") + c.Assert(err, check.IsNil) + defer func() { + os.Stdout = oldStdout + stdout.Close() + os.Remove(stdout.Name()) + }() + os.Stdout = stdout + + cli := snap.Client() + chg, err := snap.Wait(cli, "x") + // we got the change + c.Assert(chg, check.NotNil) + c.Assert(err, check.IsNil) + buf, err := ioutil.ReadFile(stdout.Name()) + c.Assert(err, check.IsNil) + + // but only after recovering + c.Check(string(buf), check.Matches, "(?ms).*Waiting for server to restart.*") +} + +func (s *SnapOpSuite) TestInstall(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "channel": "chan", + }) + s.srv.channel = "chan" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "chan", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(chan\) 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallDevMode(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "devmode": true, + "channel": "chan", + }) + s.srv.channel = "chan" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "chan", "--devmode", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(chan\) 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallClassic(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "classic": true, + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"install", "--classic", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallPath(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + c.Assert(string(postData), check.Matches, "(?s).*\r\nsnap-data\r\n.*") + c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") + c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"devmode\"\r\n\r\nfalse\r\n.*") + } + + snapBody := []byte("snap-data") + s.RedirectClientToTestServer(s.srv.handle) + snapPath := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snapPath, snapBody, 0644) + c.Assert(err, check.IsNil) + + rest, err := snap.Parser().ParseArgs([]string{"install", snapPath}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallPathDevMode(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + c.Assert(string(postData), check.Matches, "(?s).*\r\nsnap-data\r\n.*") + c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") + c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"devmode\"\r\n\r\ntrue\r\n.*") + } + + snapBody := []byte("snap-data") + s.RedirectClientToTestServer(s.srv.handle) + snapPath := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snapPath, snapBody, 0644) + c.Assert(err, check.IsNil) + + rest, err := snap.Parser().ParseArgs([]string{"install", "--devmode", snapPath}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallPathClassic(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + c.Assert(string(postData), check.Matches, "(?s).*\r\nsnap-data\r\n.*") + c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") + c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"classic\"\r\n\r\ntrue\r\n.*") + } + + snapBody := []byte("snap-data") + s.RedirectClientToTestServer(s.srv.handle) + snapPath := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snapPath, snapBody, 0644) + c.Assert(err, check.IsNil) + + rest, err := snap.Parser().ParseArgs([]string{"install", "--classic", snapPath}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallPathDangerous(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + c.Assert(string(postData), check.Matches, "(?s).*\r\nsnap-data\r\n.*") + c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") + c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\ntrue\r\n.*") + } + + snapBody := []byte("snap-data") + s.RedirectClientToTestServer(s.srv.handle) + snapPath := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snapPath, snapBody, 0644) + c.Assert(err, check.IsNil) + + rest, err := snap.Parser().ParseArgs([]string{"install", "--dangerous", snapPath}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestRevert(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "revert", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"revert", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo reverted to 1.0`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} +func (s *SnapSuite) TestRefreshList(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + c.Check(r.URL.Query().Get("select"), check.Equals, "refresh") + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2update1", "developer": "bar", "revision":17,"summary":"some summary"}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"refresh", "--list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes +foo +4.2update1 +17 +bar +-.* +`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestRefreshListErr(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--list", "--beta"}) + c.Check(err, check.ErrorMatches, "--list does not take .* flags") +} + +func (s *SnapOpSuite) TestRefreshOne(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + }) + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "foo"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' refreshed`) + +} + +func (s *SnapOpSuite) TestRefreshOneSwitchChannel(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "channel": "beta", + }) + s.srv.channel = "beta" + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "foo"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from 'bar' refreshed`) +} + +func (s *SnapOpSuite) TestRefreshOneClassic(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "classic": true, + }) + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "--classic", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) TestRefreshOneDevmode(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "devmode": true, + }) + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "--devmode", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) TestRefreshOneJailmode(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "jailmode": true, + }) + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "--jailmode", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) TestRefreshOneIgnoreValidation(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "ignore-validation": true, + }) + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "--ignore-validation", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) TestRefreshOneModeErr(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--jailmode", "--devmode", "one"}) + c.Assert(err, check.ErrorMatches, `cannot use devmode and jailmode flags together`) +} + +func (s *SnapOpSuite) TestRefreshOneChanErr(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "--channel=foo", "one"}) + c.Assert(err, check.ErrorMatches, `Please specify a single channel`) +} + +func (s *SnapOpSuite) TestRefreshAllChannel(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) +} + +func (s *SnapOpSuite) TestRefreshManyChannel(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "one", "two"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) +} + +func (s *SnapOpSuite) TestRefreshManyIgnoreValidation(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--ignore-validation", "one", "two"}) + c.Assert(err, check.ErrorMatches, `a single snap name must be specified when ignoring validation`) +} + +func (s *SnapOpSuite) TestRefreshAllModeFlags(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--devmode"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) +} + +func (s *SnapOpSuite) runTryTest(c *check.C, devmode bool) { + // pass relative path to cmd + tryDir := "some-dir" + + s.srv.checker = func(r *http.Request) { + // ensure the client always sends the absolute path + fullTryDir, err := filepath.Abs(tryDir) + c.Assert(err, check.IsNil) + + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ntry\r\n.*") + c.Assert(string(postData), check.Matches, fmt.Sprintf("(?s).*Content-Disposition: form-data; name=\"snap-path\"\r\n\r\n%s\r\n.*", regexp.QuoteMeta(fullTryDir))) + c.Assert(string(postData), check.Matches, fmt.Sprintf("(?s).*Content-Disposition: form-data; name=\"devmode\"\r\n\r\n%s\r\n.*", strconv.FormatBool(devmode))) + } + + s.RedirectClientToTestServer(s.srv.handle) + + cmd := []string{"try", tryDir} + if devmode { + cmd = append(cmd, "--devmode") + } + + rest, err := snap.Parser().ParseArgs(cmd) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, fmt.Sprintf(`(?sm).*foo 1.0 mounted from .*%s`, tryDir)) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestTryNoDevMode(c *check.C) { + s.runTryTest(c, false) +} +func (s *SnapOpSuite) TestTryDevMode(c *check.C) { + s.runTryTest(c, true) +} + +func (s *SnapSuite) TestInstallChannelDuplicationError(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"install", "--edge", "--beta", "some-snap"}) + c.Assert(err, check.ErrorMatches, "Please specify a single channel") +} + +func (s *SnapSuite) TestRefreshChannelDuplicationError(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"refresh", "--edge", "--beta", "some-snap"}) + c.Assert(err, check.ErrorMatches, "Please specify a single channel") +} + +func (s *SnapOpSuite) TestInstallFromChannel(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "channel": "edge", + }) + s.srv.channel = "edge" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"install", "--edge", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(edge\) 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestEnable(c *check.C) { + s.srv.total = 3 + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "enable", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"enable", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo enabled`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestDisable(c *check.C) { + s.srv.total = 3 + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "disable", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"disable", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo disabled`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestRemove(c *check.C) { + s.srv.total = 3 + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "remove", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"remove", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo removed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestRemoveManyRevision(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"remove", "--revision=17", "one", "two"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify the revision`) +} + +func (s *SnapOpSuite) TestRemoveMany(c *check.C) { + total := 3 + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "remove", + "snaps": []interface{}{"one", "two"}, + }) + + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(http.StatusAccepted) + fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + case 2: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": ["one","two"]}}}`) + default: + c.Fatalf("expected to get %d requests, now on %d", total, n+1) + } + + n++ + }) + + rest, err := snap.Parser().ParseArgs([]string{"remove", "one", "two"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*one removed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*two removed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, total) +} + +func (s *SnapOpSuite) TestInstallManyChannel(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"install", "--beta", "one", "two"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) +} + +func (s *SnapOpSuite) TestInstallManyMixFileAndStore(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"install", "store-snap", "./local.snap"}) + c.Assert(err, check.ErrorMatches, `only one snap file can be installed at a time`) +} + +func (s *SnapOpSuite) TestInstallMany(c *check.C) { + total := 4 + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "snaps": []interface{}{"one", "two"}, + }) + + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(http.StatusAccepted) + fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + case 2: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": ["one","two"]}}}`) + case 3: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "one", "status": "active", "version": "1.0", "developer": "bar", "revision":42, "channel":"stable"},{"name": "two", "status": "active", "version": "2.0", "developer": "baz", "revision":42, "channel":"edge"}]}\n`) + + default: + c.Fatalf("expected to get %d requests, now on %d", total, n+1) + } + + n++ + }) + + rest, err := snap.Parser().ParseArgs([]string{"install", "one", "two"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + // note that (stable) is omitted + c.Check(s.Stdout(), check.Matches, `(?sm).*one 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*two \(edge\) 2.0 from 'baz' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, total) +} diff --git a/cmd/snap/cmd_unalias.go b/cmd/snap/cmd_unalias.go new file mode 100644 index 00000000..50ce7ff7 --- /dev/null +++ b/cmd/snap/cmd_unalias.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdUnalias struct { + Positionals struct { + Snap installedSnapName `required:"yes"` + Aliases []string `required:"yes"` + } `positional-args:"true"` +} + +var shortUnaliasHelp = i18n.G("Disables the given aliases") +var longUnaliasHelp = i18n.G(` +The unalias command disables explicitly the given application aliases defined by the snap. +`) + +func init() { + addCommand("unalias", shortUnaliasHelp, longUnaliasHelp, func() flags.Commander { + return &cmdUnalias{} + }, nil, []argDesc{ + {name: ""}, + {name: i18n.G("")}, + }) +} + +func (x *cmdUnalias) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapName := string(x.Positionals.Snap) + aliases := x.Positionals.Aliases + + cli := Client() + id, err := cli.Unalias(snapName, aliases) + if err != nil { + return err + } + + _, err = wait(cli, id) + return err +} diff --git a/cmd/snap/cmd_unalias_test.go b/cmd/snap/cmd_unalias_test.go new file mode 100644 index 00000000..72f38870 --- /dev/null +++ b/cmd/snap/cmd_unalias_test.go @@ -0,0 +1,70 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestUnaliasHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] unalias [] [...] + +The unalias command disables explicitly the given application aliases defined +by the snap. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message +` + rest, err := Parser().ParseArgs([]string{"unalias", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestUnalias(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/aliases": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "unalias", + "snap": "alias-snap", + "aliases": []interface{}{"alias1", "alias2"}, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"unalias", "alias-snap", "alias1", "alias2"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} diff --git a/cmd/snap/cmd_version.go b/cmd/snap/cmd_version.go new file mode 100644 index 00000000..6a706485 --- /dev/null +++ b/cmd/snap/cmd_version.go @@ -0,0 +1,75 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortVersionHelp = i18n.G("Shows version details") +var longVersionHelp = i18n.G(` +The version command displays the versions of the running client, server, +and operating system. +`) + +type cmdVersion struct{} + +func init() { + addCommand("version", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdVersion{} }, nil, nil) +} + +func (cmd cmdVersion) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + printVersions() + return nil +} + +func printVersions() error { + sv, err := Client().ServerVersion() + if err != nil { + sv = &client.ServerVersion{ + Version: i18n.G("unavailable"), + Series: "-", + OSID: "-", + OSVersionID: "-", + } + } + + w := tabWriter() + + fmt.Fprintf(w, "snap\t%s\n", cmd.Version) + fmt.Fprintf(w, "snapd\t%s\n", sv.Version) + fmt.Fprintf(w, "series\t%s\n", sv.Series) + if sv.OnClassic { + fmt.Fprintf(w, "%s\t%s\n", sv.OSID, sv.OSVersionID) + } + w.Flush() + + return err +} diff --git a/cmd/snap/cmd_version_test.go b/cmd/snap/cmd_version_test.go new file mode 100644 index 00000000..59cdee2d --- /dev/null +++ b/cmd/snap/cmd_version_test.go @@ -0,0 +1,59 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestVersionCommandOnClassic(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic":true,"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`) + }) + restore := mockArgs("snap", "version") + defer restore() + restore = mockVersion("4.56") + defer restore() + + _, err := snap.Parser().ParseArgs([]string{"version"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\nubuntu 12.34\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestVersionCommandOnAllSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`) + }) + restore := mockArgs("snap", "--version") + defer restore() + restore = mockVersion("4.56") + defer restore() + + _, err := snap.Parser().ParseArgs([]string{"version"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\n") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_watch.go b/cmd/snap/cmd_watch.go new file mode 100644 index 00000000..290dce04 --- /dev/null +++ b/cmd/snap/cmd_watch.go @@ -0,0 +1,57 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +type cmdWatch struct { + Positional struct { + ChangeID changeID `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +var shortWatchHelp = i18n.G("Watch a change in progress") +var longWatchHelp = i18n.G(` +The watch command waits for the given change-id to finish and shows progress +(if available). +`) + +func init() { + addCommand("watch", shortWatchHelp, longWatchHelp, func() flags.Commander { + return &cmdWatch{} + }, nil, []argDesc{{ + name: i18n.G(""), + desc: i18n.G("Change ID"), + }}) +} + +func (x *cmdWatch) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + cli := Client() + _, err := wait(cli, string(x.Positional.ChangeID)) + + return err +} diff --git a/cmd/snap/cmd_watch_test.go b/cmd/snap/cmd_watch_test.go new file mode 100644 index 00000000..52727f74 --- /dev/null +++ b/cmd/snap/cmd_watch_test.go @@ -0,0 +1,85 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "time" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/testutil" +) + +var fmtWatchChangeJSON = `{"type": "sync", "result": { + "id": "42", + "kind": "some-kind", + "summary": "some summary...", + "status": "Doing", + "ready": false, + "tasks": [{"id": "84", "kind": "bar", "summary": "some summary", "status": "Doing", "progress": {"label": "my-snap", "done": %d, "total": %d}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] +}}` + +func (s *SnapSuite) TestCmdWatch(c *C) { + restore := snap.MockMaxGoneTime(time.Millisecond) + defer restore() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/42") + fmt.Fprintf(w, fmtWatchChangeJSON, 0, 100*1024) + case 1: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/42") + fmt.Fprintf(w, fmtWatchChangeJSON, 50*1024, 100*1024) + case 2: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"id": "42", "ready": true, "status": "Done"}}`) + } + n++ + }) + + oldStdout := os.Stdout + stdout, err := ioutil.TempFile("", "stdout") + c.Assert(err, IsNil) + defer func() { + os.Stdout = oldStdout + stdout.Close() + os.Remove(stdout.Name()) + }() + os.Stdout = stdout + + _, err = snap.Parser().ParseArgs([]string{"watch", "42"}) + os.Stdout = oldStdout + c.Assert(err, IsNil) + c.Check(n, Equals, 3) + + buf, err := ioutil.ReadFile(stdout.Name()) + c.Assert(err, IsNil) + c.Check(string(buf), testutil.Contains, "\rmy-snap 50.00 KB / 100.00 KB") +} diff --git a/cmd/snap/complete.go b/cmd/snap/complete.go new file mode 100644 index 00000000..b7d16f32 --- /dev/null +++ b/cmd/snap/complete.go @@ -0,0 +1,100 @@ +package main + +import ( + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/client" +) + +type installedSnapName string + +func (s installedSnapName) Complete(match string) []flags.Completion { + cli := Client() + snaps, err := cli.List(nil, nil) + if err != nil { + return nil + } + + ret := make([]flags.Completion, 0, len(snaps)) + for _, snap := range snaps { + if strings.HasPrefix(snap.Name, match) { + ret = append(ret, flags.Completion{Item: snap.Name}) + } + } + + return ret +} + +type remoteSnapName string + +func (s remoteSnapName) Complete(match string) []flags.Completion { + if len(match) < 3 { + return nil + } + cli := Client() + snaps, _, err := cli.Find(&client.FindOptions{ + Prefix: true, + Query: match, + }) + if err != nil { + return nil + } + ret := make([]flags.Completion, len(snaps)) + for i, snap := range snaps { + ret[i] = flags.Completion{Item: snap.Name} + } + return ret +} + +type anySnapName string + +func (s anySnapName) Complete(match string) []flags.Completion { + res := installedSnapName(s).Complete(match) + seen := make(map[string]bool) + for _, x := range res { + seen[x.Item] = true + } + + for _, x := range remoteSnapName(s).Complete(match) { + if !seen[x.Item] { + res = append(res, x) + } + } + + return res +} + +type changeID string + +func (s changeID) Complete(match string) []flags.Completion { + cli := Client() + changes, err := cli.Changes(&client.ChangesOptions{Selector: client.ChangesAll}) + if err != nil { + return nil + } + + ret := make([]flags.Completion, 0, len(changes)) + for _, change := range changes { + if strings.HasPrefix(change.ID, match) { + ret = append(ret, flags.Completion{Item: change.ID}) + } + } + + return ret +} + +type keyName string + +func (s keyName) Complete(match string) []flags.Completion { + var res []flags.Completion + asserts.NewGPGKeypairManager().Walk(func(_ asserts.PrivateKey, _ string, uid string) error { + if strings.HasPrefix(uid, match) { + res = append(res, flags.Completion{Item: uid}) + } + return nil + }) + return res +} diff --git a/cmd/snap/export_test.go b/cmd/snap/export_test.go new file mode 100644 index 00000000..b229d0ff --- /dev/null +++ b/cmd/snap/export_test.go @@ -0,0 +1,104 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "os/user" + "time" + + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/store" +) + +var RunMain = run + +var ( + CreateUserDataDirs = createUserDataDirs + SnapRunApp = snapRunApp + SnapRunHook = snapRunHook + Wait = wait + ResolveApp = resolveApp +) + +func MockPollTime(d time.Duration) (restore func()) { + d0 := pollTime + pollTime = d + return func() { + pollTime = d0 + } +} + +func MockMaxGoneTime(d time.Duration) (restore func()) { + d0 := maxGoneTime + maxGoneTime = d + return func() { + maxGoneTime = d0 + } +} + +func MockSyscallExec(f func(string, []string, []string) error) (restore func()) { + syscallExecOrig := syscallExec + syscallExec = f + return func() { + syscallExec = syscallExecOrig + } +} + +func MockUserCurrent(f func() (*user.User, error)) (restore func()) { + userCurrentOrig := userCurrent + userCurrent = f + return func() { + userCurrent = userCurrentOrig + } +} + +func MockStoreNew(f func(*store.Config, auth.AuthContext) *store.Store) (restore func()) { + storeNewOrig := storeNew + storeNew = f + return func() { + storeNew = storeNewOrig + } +} + +func MockMountInfoPath(newMountInfoPath string) (restore func()) { + mountInfoPathOrig := mountInfoPath + mountInfoPath = newMountInfoPath + return func() { + mountInfoPath = mountInfoPathOrig + } +} + +var AutoImportCandidates = autoImportCandidates + +func AliasInfoLess(snapName1, alias1, app1, snapName2, alias2, app2 string) bool { + x := aliasInfos{ + &aliasInfo{ + Snap: snapName1, + Alias: alias1, + App: app1, + }, + &aliasInfo{ + Snap: snapName2, + Alias: alias2, + App: app2, + }, + } + return x.Less(0, 1) +} diff --git a/cmd/snap/gnupg2_test.go b/cmd/snap/gnupg2_test.go new file mode 100644 index 00000000..aa142906 --- /dev/null +++ b/cmd/snap/gnupg2_test.go @@ -0,0 +1,27 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" +) + +// FIXME: drop once gpg2 is the default +var _ = Suite(&SnapKeysSuite{GnupgCmd: "/usr/bin/gpg2"}) diff --git a/cmd/snap/interfaces_common.go b/cmd/snap/interfaces_common.go new file mode 100644 index 00000000..9f292d74 --- /dev/null +++ b/cmd/snap/interfaces_common.go @@ -0,0 +1,85 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package main + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/i18n" +) + +// AttributePair contains a pair of key-value strings +type AttributePair struct { + // The key + Key string + // The value + Value string +} + +// UnmarshalFlag parses a string into an AttributePair +func (ap *AttributePair) UnmarshalFlag(value string) error { + parts := strings.SplitN(value, "=", 2) + if len(parts) < 2 || parts[0] == "" { + ap.Key = "" + ap.Value = "" + return fmt.Errorf(i18n.G("invalid attribute: %q (want key=value)"), value) + } + ap.Key = parts[0] + ap.Value = parts[1] + return nil +} + +// AttributePairSliceToMap converts a slice of AttributePair into a map +func AttributePairSliceToMap(attrs []AttributePair) map[string]string { + result := make(map[string]string) + for _, attr := range attrs { + result[attr.Key] = attr.Value + } + return result +} + +// SnapAndName holds a snap name and a plug or slot name. +type SnapAndName struct { + Snap string + Name string +} + +// UnmarshalFlag unmarshals snap and plug or slot name. +func (sn *SnapAndName) UnmarshalFlag(value string) error { + parts := strings.Split(value, ":") + sn.Snap = "" + sn.Name = "" + switch len(parts) { + case 1: + sn.Snap = parts[0] + case 2: + sn.Snap = parts[0] + sn.Name = parts[1] + // Reject "snap:" (that should be spelled as "snap") + if sn.Name == "" { + sn.Snap = "" + } + } + if sn.Snap == "" && sn.Name == "" { + return fmt.Errorf(i18n.G("invalid value: %q (want snap:name or snap)"), value) + } + return nil +} diff --git a/cmd/snap/interfaces_common_test.go b/cmd/snap/interfaces_common_test.go new file mode 100644 index 00000000..3b46ab9b --- /dev/null +++ b/cmd/snap/interfaces_common_test.go @@ -0,0 +1,106 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +type AttributePairSuite struct{} + +var _ = Suite(&AttributePairSuite{}) + +func (s *AttributePairSuite) TestUnmarshalFlagAttributePair(c *C) { + var ap AttributePair + // Typical + err := ap.UnmarshalFlag("key=value") + c.Assert(err, IsNil) + c.Check(ap.Key, Equals, "key") + c.Check(ap.Value, Equals, "value") + // Empty key + err = ap.UnmarshalFlag("=value") + c.Assert(err, ErrorMatches, `invalid attribute: "=value" \(want key=value\)`) + c.Check(ap.Key, Equals, "") + c.Check(ap.Value, Equals, "") + // Empty value + err = ap.UnmarshalFlag("key=") + c.Assert(err, IsNil) + c.Check(ap.Key, Equals, "key") + c.Check(ap.Value, Equals, "") + // Both key and value empty + err = ap.UnmarshalFlag("=") + c.Assert(err, ErrorMatches, `invalid attribute: "=" \(want key=value\)`) + c.Check(ap.Key, Equals, "") + c.Check(ap.Value, Equals, "") + // Value containing = + err = ap.UnmarshalFlag("key=value=more") + c.Assert(err, IsNil) + c.Check(ap.Key, Equals, "key") + c.Check(ap.Value, Equals, "value=more") + // Malformed format + err = ap.UnmarshalFlag("malformed") + c.Assert(err, ErrorMatches, `invalid attribute: "malformed" \(want key=value\)`) + c.Check(ap.Key, Equals, "") + c.Check(ap.Value, Equals, "") +} + +func (s *AttributePairSuite) TestAttributePairSliceToMap(c *C) { + attrs := []AttributePair{ + {"key1", "value1"}, + {"key2", "value2"}, + } + m := AttributePairSliceToMap(attrs) + c.Check(m, DeepEquals, map[string]string{ + "key1": "value1", + "key2": "value2", + }) +} + +type SnapAndNameSuite struct{} + +var _ = Suite(&SnapAndNameSuite{}) + +func (s *SnapAndNameSuite) TestUnmarshalFlag(c *C) { + var sn SnapAndName + // Typical + err := sn.UnmarshalFlag("snap:name") + c.Assert(err, IsNil) + c.Check(sn.Snap, Equals, "snap") + c.Check(sn.Name, Equals, "name") + // Abbreviated + err = sn.UnmarshalFlag("snap") + c.Assert(err, IsNil) + c.Check(sn.Snap, Equals, "snap") + c.Check(sn.Name, Equals, "") + // Invalid + for _, input := range []string{ + "snap:", // Empty name, should be spelled as "snap" + ":", // Both snap and name empty, makes no sense + "snap:name:more", // Name containing :, probably a typo + "", // Empty input + } { + err = sn.UnmarshalFlag(input) + c.Assert(err, ErrorMatches, `invalid value: ".*" \(want snap:name or snap\)`) + c.Check(sn.Snap, Equals, "") + c.Check(sn.Name, Equals, "") + } +} diff --git a/cmd/snap/main.go b/cmd/snap/main.go new file mode 100644 index 00000000..f27d3546 --- /dev/null +++ b/cmd/snap/main.go @@ -0,0 +1,312 @@ +// -*- 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 . + * + */ + +package main + +import ( + "fmt" + "io" + "os" + "os/user" + "path/filepath" + "strings" + "unicode" + + "github.com/jessevdk/go-flags" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/store" +) + +func init() { + // set User-Agent for when 'snap' talks to the store directly (snap download etc...) + store.SetUserAgentFromVersion(cmd.Version, "snap") +} + +// Standard streams, redirected for testing. +var ( + Stdin io.Reader = os.Stdin + Stdout io.Writer = os.Stdout + Stderr io.Writer = os.Stderr + ReadPassword = terminal.ReadPassword +) + +type options struct { + Version func() `long:"version"` +} + +type argDesc struct { + name string + desc string +} + +var optionsData options + +// ErrExtraArgs is returned if extra arguments to a command are found +var ErrExtraArgs = fmt.Errorf(i18n.G("too many arguments for command")) + +// cmdInfo holds information needed to call parser.AddCommand(...). +type cmdInfo struct { + name, shortHelp, longHelp string + builder func() flags.Commander + hidden bool + optDescs map[string]string + argDescs []argDesc +} + +// commands holds information about all non-experimental commands. +var commands []*cmdInfo + +// experimentalCommands holds information about all experimental commands. +var experimentalCommands []*cmdInfo + +// addCommand replaces parser.addCommand() in a way that is compatible with +// re-constructing a pristine parser. +func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo { + info := &cmdInfo{ + name: name, + shortHelp: shortHelp, + longHelp: longHelp, + builder: builder, + optDescs: optDescs, + argDescs: argDescs, + } + commands = append(commands, info) + return info +} + +// addExperimentalCommand replaces parser.addCommand() in a way that is +// compatible with re-constructing a pristine parser. It is meant for +// adding experimental commands. +func addExperimentalCommand(name, shortHelp, longHelp string, builder func() flags.Commander) *cmdInfo { + info := &cmdInfo{ + name: name, + shortHelp: shortHelp, + longHelp: longHelp, + builder: builder, + } + experimentalCommands = append(experimentalCommands, info) + return info +} + +type parserSetter interface { + setParser(*flags.Parser) +} + +func lintDesc(cmdName, optName, desc, origDesc string) { + if len(optName) == 0 { + logger.Panicf("option on %q has no name", cmdName) + } + if len(origDesc) != 0 { + logger.Panicf("description of %s's %q of %q set from tag (=> no i18n)", cmdName, optName, origDesc) + } + if len(desc) > 0 { + if !unicode.IsUpper(([]rune)(desc)[0]) { + logger.Panicf("description of %s's %q not uppercase: %q", cmdName, optName, desc) + } + } +} + +func lintArg(cmdName, optName, desc, origDesc string) { + lintDesc(cmdName, optName, desc, origDesc) + if optName[0] != '<' || optName[len(optName)-1] != '>' { + logger.Panicf("argument %q's %q should have <>s", cmdName, optName) + } +} + +// Parser creates and populates a fresh parser. +// Since commands have local state a fresh parser is required to isolate tests +// from each other. +func Parser() *flags.Parser { + optionsData.Version = func() { + printVersions() + panic(&exitStatus{0}) + } + parser := flags.NewParser(&optionsData, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) + parser.ShortDescription = i18n.G("Tool to interact with snaps") + parser.LongDescription = i18n.G(` +Install, configure, refresh and remove snap packages. Snaps are +'universal' packages that work across many different Linux systems, +enabling secure distribution of the latest apps and utilities for +cloud, servers, desktops and the internet of things. + +This is the CLI for snapd, a background service that takes care of +snaps on the system. Start with 'snap list' to see installed snaps. +`) + parser.FindOptionByLongName("version").Description = i18n.G("Print the version and exit") + + // Add all regular commands + for _, c := range commands { + obj := c.builder() + if x, ok := obj.(parserSetter); ok { + x.setParser(parser) + } + + cmd, err := parser.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj) + if err != nil { + logger.Panicf("cannot add command %q: %v", c.name, err) + } + cmd.Hidden = c.hidden + + opts := cmd.Options() + if c.optDescs != nil && len(opts) != len(c.optDescs) { + logger.Panicf("wrong number of option descriptions for %s: expected %d, got %d", c.name, len(opts), len(c.optDescs)) + } + for _, opt := range opts { + name := opt.LongName + if name == "" { + name = string(opt.ShortName) + } + desc, ok := c.optDescs[name] + if !(c.optDescs == nil || ok) { + logger.Panicf("%s missing description for %s", c.name, name) + } + lintDesc(c.name, name, desc, opt.Description) + if desc != "" { + opt.Description = desc + } + } + + args := cmd.Args() + if c.argDescs != nil && len(args) != len(c.argDescs) { + logger.Panicf("wrong number of argument descriptions for %s: expected %d, got %d", c.name, len(args), len(c.argDescs)) + } + for i, arg := range args { + name, desc := arg.Name, "" + if c.argDescs != nil { + name = c.argDescs[i].name + desc = c.argDescs[i].desc + } + lintArg(c.name, name, desc, arg.Description) + arg.Name = name + arg.Description = desc + } + } + return parser +} + +// ClientConfig is the configuration of the Client used by all commands. +var ClientConfig client.Config + +// Client returns a new client using ClientConfig as configuration. +func Client() *client.Client { + return client.New(&ClientConfig) +} + +func init() { + err := logger.SimpleSetup() + if err != nil { + fmt.Fprintf(Stderr, i18n.G("WARNING: failed to activate logging: %v\n"), err) + } +} + +func resolveApp(snapApp string) (string, error) { + target, err := os.Readlink(filepath.Join(dirs.SnapBinariesDir, snapApp)) + if err != nil { + return "", err + } + if filepath.Base(target) == target { // alias pointing to an app command in /snap/bin + return target, nil + } + return snapApp, nil +} + +func main() { + cmd.ExecInCoreSnap() + + // magic \o/ + snapApp := filepath.Base(os.Args[0]) + if osutil.IsSymlink(filepath.Join(dirs.SnapBinariesDir, snapApp)) { + var err error + snapApp, err = resolveApp(snapApp) + if err != nil { + fmt.Fprintf(Stderr, i18n.G("cannot resolve snap app %q: %v"), snapApp, err) + os.Exit(46) + } + cmd := &cmdRun{} + args := []string{snapApp} + args = append(args, os.Args[1:]...) + // this will call syscall.Exec() so it does not return + // *unless* there is an error, i.e. we setup a wrong + // symlink (or syscall.Exec() fails for strange reasons) + err = cmd.Execute(args) + fmt.Fprintf(Stderr, i18n.G("internal error, please report: running %q failed: %v\n"), snapApp, err) + os.Exit(46) + } + + defer func() { + if v := recover(); v != nil { + if e, ok := v.(*exitStatus); ok { + os.Exit(e.code) + } + panic(v) + } + }() + + // no magic /o\ + if err := run(); err != nil { + fmt.Fprintf(Stderr, i18n.G("error: %v\n"), err) + os.Exit(1) + } +} + +type exitStatus struct { + code int +} + +func (e *exitStatus) Error() string { + return fmt.Sprintf("internal error: exitStatus{%d} being handled as normal error", e.code) +} + +func run() error { + parser := Parser() + _, err := parser.Parse() + if err != nil { + if e, ok := err.(*flags.Error); ok { + if e.Type == flags.ErrHelp || e.Type == flags.ErrCommandRequired { + if parser.Command.Active != nil && parser.Command.Active.Name == "help" { + parser.Command.Active = nil + } + parser.WriteHelp(Stdout) + return nil + } + if e.Type == flags.ErrUnknownCommand { + return fmt.Errorf(i18n.G(`unknown command %q, see "snap --help"`), os.Args[1]) + } + } + if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindLoginRequired { + u, _ := user.Current() + if u != nil && u.Username == "root" { + return fmt.Errorf(i18n.G(`%s (see "snap login --help")`), e.Message) + } + + // TRANSLATORS: %s will be a message along the lines of "login required" + return fmt.Errorf(i18n.G(`%s (try with sudo)`), e.Message) + } + } + + return err +} diff --git a/cmd/snap/main_test.go b/cmd/snap/main_test.go new file mode 100644 index 00000000..888c36b4 --- /dev/null +++ b/cmd/snap/main_test.go @@ -0,0 +1,263 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/testutil" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type BaseSnapSuite struct { + testutil.BaseTest + stdin *bytes.Buffer + stdout *bytes.Buffer + stderr *bytes.Buffer + password string + + AuthFile string +} + +func (s *BaseSnapSuite) readPassword(fd int) ([]byte, error) { + return []byte(s.password), nil +} + +func (s *BaseSnapSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.stdin = bytes.NewBuffer(nil) + s.stdout = bytes.NewBuffer(nil) + s.stderr = bytes.NewBuffer(nil) + s.password = "" + + snap.Stdin = s.stdin + snap.Stdout = s.stdout + snap.Stderr = s.stderr + snap.ReadPassword = s.readPassword + s.AuthFile = filepath.Join(c.MkDir(), "json") + os.Setenv(TestAuthFileEnvKey, s.AuthFile) +} + +func (s *BaseSnapSuite) TearDownTest(c *C) { + snap.Stdin = os.Stdin + snap.Stdout = os.Stdout + snap.Stderr = os.Stderr + snap.ReadPassword = terminal.ReadPassword + + c.Assert(s.AuthFile == "", Equals, false) + err := os.Unsetenv(TestAuthFileEnvKey) + c.Assert(err, IsNil) + s.BaseTest.TearDownTest(c) +} + +func (s *BaseSnapSuite) Stdout() string { + return s.stdout.String() +} + +func (s *BaseSnapSuite) Stderr() string { + return s.stderr.String() +} + +func (s *BaseSnapSuite) ResetStdStreams() { + s.stdin.Reset() + s.stdout.Reset() + s.stderr.Reset() +} + +func (s *BaseSnapSuite) RedirectClientToTestServer(handler func(http.ResponseWriter, *http.Request)) { + server := httptest.NewServer(http.HandlerFunc(handler)) + s.BaseTest.AddCleanup(func() { server.Close() }) + snap.ClientConfig.BaseURL = server.URL + s.BaseTest.AddCleanup(func() { snap.ClientConfig.BaseURL = "" }) +} + +func (s *BaseSnapSuite) Login(c *C) { + err := osutil.AtomicWriteFile(s.AuthFile, []byte(TestAuthFileContents), 0600, 0) + c.Assert(err, IsNil) +} + +func (s *BaseSnapSuite) Logout(c *C) { + if osutil.FileExists(s.AuthFile) { + c.Assert(os.Remove(s.AuthFile), IsNil) + } +} + +type SnapSuite struct { + BaseSnapSuite +} + +var _ = Suite(&SnapSuite{}) + +// DecodedRequestBody returns the JSON-decoded body of the request. +func DecodedRequestBody(c *C, r *http.Request) map[string]interface{} { + var body map[string]interface{} + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&body) + c.Assert(err, IsNil) + return body +} + +// EncodeResponseBody writes JSON-serialized body to the response writer. +func EncodeResponseBody(c *C, w http.ResponseWriter, body interface{}) { + encoder := json.NewEncoder(w) + err := encoder.Encode(body) + c.Assert(err, IsNil) +} + +func mockArgs(args ...string) (restore func()) { + old := os.Args + os.Args = args + return func() { os.Args = old } +} + +func mockVersion(v string) (restore func()) { + old := cmd.Version + cmd.Version = v + return func() { cmd.Version = old } +} + +const TestAuthFileEnvKey = "SNAPPY_STORE_AUTH_DATA_FILENAME" +const TestAuthFileContents = `{"id":123,"email":"hello@mail.com","macaroon":"MDAxM2xvY2F0aW9uIHNuYXBkCjAwMTJpZGVudGlmaWVyIDQzCjAwMmZzaWduYXR1cmUg5RfMua72uYop4t3cPOBmGUuaoRmoDH1HV62nMJq7eqAK"}` + +func (s *SnapSuite) TestErrorResult(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "cannot do something"}}`) + }) + + restore := mockArgs("snap", "install", "foo") + defer restore() + + err := snap.RunMain() + c.Assert(err, ErrorMatches, `cannot do something`) +} + +func (s *SnapSuite) TestAccessDeniedHint(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "access denied", "kind": "login-required"}, "status-code": 401}`) + }) + + restore := mockArgs("snap", "install", "foo") + defer restore() + + err := snap.RunMain() + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, `access denied (try with sudo)`) +} + +func (s *SnapSuite) TestExtraArgs(c *C) { + restore := mockArgs("snap", "abort", "1", "xxx", "zzz") + defer restore() + + err := snap.RunMain() + c.Assert(err, ErrorMatches, `too many arguments for command`) +} + +func (s *SnapSuite) TestVersionOnClassic(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic":true,"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`) + }) + restore := mockArgs("snap", "--version") + defer restore() + restore = mockVersion("4.56") + defer restore() + + c.Assert(func() { snap.RunMain() }, PanicMatches, `internal error: exitStatus\{0\} .*`) + c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\nubuntu 12.34\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestVersionOnAllSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`) + }) + restore := mockArgs("snap", "--version") + defer restore() + restore = mockVersion("4.56") + defer restore() + + c.Assert(func() { snap.RunMain() }, PanicMatches, `internal error: exitStatus\{0\} .*`) + c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestUnknownCommand(c *C) { + restore := mockArgs("snap", "unknowncmd") + defer restore() + + err := snap.RunMain() + c.Assert(err, ErrorMatches, `unknown command "unknowncmd", see "snap --help"`) +} + +func (s *SnapSuite) TestResolveApp(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, IsNil) + + // "wrapper" symlinks + err = os.Symlink("/usr/bin/snap", filepath.Join(dirs.SnapBinariesDir, "foo")) + c.Assert(err, IsNil) + err = os.Symlink("/usr/bin/snap", filepath.Join(dirs.SnapBinariesDir, "foo.bar")) + c.Assert(err, IsNil) + + // alias symlinks + err = os.Symlink("foo", filepath.Join(dirs.SnapBinariesDir, "foo_")) + c.Assert(err, IsNil) + err = os.Symlink("foo.bar", filepath.Join(dirs.SnapBinariesDir, "foo_bar-1")) + c.Assert(err, IsNil) + + snapApp, err := snap.ResolveApp("foo") + c.Assert(err, IsNil) + c.Check(snapApp, Equals, "foo") + + snapApp, err = snap.ResolveApp("foo.bar") + c.Assert(err, IsNil) + c.Check(snapApp, Equals, "foo.bar") + + snapApp, err = snap.ResolveApp("foo_") + c.Assert(err, IsNil) + c.Check(snapApp, Equals, "foo") + + snapApp, err = snap.ResolveApp("foo_bar-1") + c.Assert(err, IsNil) + c.Check(snapApp, Equals, "foo.bar") + + _, err = snap.ResolveApp("baz") + c.Check(err, NotNil) +} diff --git a/cmd/snap/notes.go b/cmd/snap/notes.go new file mode 100644 index 00000000..63751235 --- /dev/null +++ b/cmd/snap/notes.go @@ -0,0 +1,151 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/snap" +) + +func getPriceString(prices map[string]float64, suggestedCurrency, status string) string { + price, currency, err := getPrice(prices, suggestedCurrency) + + // If there are no prices, then the snap is free + if err != nil { + return "" + } + + // If the snap is priced, but has been purchased + if status == "available" { + return i18n.G("bought") + } + + return formatPrice(price, currency) +} + +func formatPrice(val float64, currency string) string { + return fmt.Sprintf("%.2f%s", val, currency) +} + +// Notes encapsulate everything that might be interesting about a +// snap, in order to present a brief summary of it. +type Notes struct { + Price string + Private bool + DevMode bool + JailMode bool + Classic bool + TryMode bool + Disabled bool + Broken bool +} + +func NotesFromChannelSnapInfo(ref *snap.ChannelSnapInfo) *Notes { + return &Notes{ + DevMode: ref.Confinement == client.DevModeConfinement, + Classic: ref.Confinement == client.ClassicConfinement, + } +} + +func NotesFromRemote(snap *client.Snap, resInfo *client.ResultInfo) *Notes { + notes := &Notes{ + Private: snap.Private, + DevMode: snap.Confinement == client.DevModeConfinement, + Classic: snap.Confinement == client.ClassicConfinement, + } + if resInfo != nil { + notes.Price = getPriceString(snap.Prices, resInfo.SuggestedCurrency, snap.Status) + } + + return notes +} + +func NotesFromLocal(snap *client.Snap) *Notes { + return &Notes{ + Private: snap.Private, + DevMode: !snap.JailMode && (snap.DevMode || snap.Confinement == client.DevModeConfinement), + Classic: !snap.JailMode && (snap.Confinement == client.ClassicConfinement), + JailMode: snap.JailMode, + TryMode: snap.TryMode, + Disabled: snap.Status != client.StatusActive, + Broken: snap.Broken != "", + } +} + +func NotesFromInfo(info *snap.Info) *Notes { + return &Notes{ + Private: info.Private, + DevMode: info.Confinement == client.DevModeConfinement, + Classic: info.Confinement == client.ClassicConfinement, + Broken: info.Broken != "", + } +} + +func (n *Notes) String() string { + if n == nil { + return "" + } + var ns []string + + if n.Disabled { + // TRANSLATORS: if possible, a single short word + ns = append(ns, i18n.G("disabled")) + } + + if n.Price != "" { + ns = append(ns, n.Price) + } + + if n.DevMode { + ns = append(ns, "devmode") + } + + if n.JailMode { + ns = append(ns, "jailmode") + } + + if n.Classic { + ns = append(ns, "classic") + } + + if n.Private { + // TRANSLATORS: if possible, a single short word + ns = append(ns, i18n.G("private")) + } + + if n.TryMode { + ns = append(ns, "try") + } + + if n.Broken { + // TRANSLATORS: if possible, a single short word + ns = append(ns, i18n.G("broken")) + } + + if len(ns) == 0 { + return "-" + } + + return strings.Join(ns, ",") +} diff --git a/cmd/snap/notes_test.go b/cmd/snap/notes_test.go new file mode 100644 index 00000000..b03a6e5e --- /dev/null +++ b/cmd/snap/notes_test.go @@ -0,0 +1,93 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main_test + +import ( + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +type notesSuite struct{} + +var _ = check.Suite(¬esSuite{}) + +func (notesSuite) TestNoNotes(c *check.C) { + c.Check((&snap.Notes{}).String(), check.Equals, "-") +} + +func (notesSuite) TestNotesPrice(c *check.C) { + c.Check((&snap.Notes{ + Price: "3.50GBP", + }).String(), check.Equals, "3.50GBP") +} + +func (notesSuite) TestNotesPrivate(c *check.C) { + c.Check((&snap.Notes{ + Private: true, + }).String(), check.Equals, "private") +} + +func (notesSuite) TestNotesDevMode(c *check.C) { + c.Check((&snap.Notes{ + DevMode: true, + }).String(), check.Equals, "devmode") +} + +func (notesSuite) TestNotesJailMode(c *check.C) { + c.Check((&snap.Notes{ + JailMode: true, + }).String(), check.Equals, "jailmode") +} + +func (notesSuite) TestNotesClassic(c *check.C) { + c.Check((&snap.Notes{ + Classic: true, + }).String(), check.Equals, "classic") +} + +func (notesSuite) TestNotesTryMode(c *check.C) { + c.Check((&snap.Notes{ + TryMode: true, + }).String(), check.Equals, "try") +} + +func (notesSuite) TestNotesDisabled(c *check.C) { + c.Check((&snap.Notes{ + Disabled: true, + }).String(), check.Equals, "disabled") +} + +func (notesSuite) TestNotesBroken(c *check.C) { + c.Check((&snap.Notes{ + Broken: true, + }).String(), check.Equals, "broken") +} + +func (notesSuite) TestNotesNothing(c *check.C) { + c.Check((&snap.Notes{}).String(), check.Equals, "-") +} + +func (notesSuite) TestNotesTwo(c *check.C) { + c.Check((&snap.Notes{ + DevMode: true, + Broken: true, + }).String(), check.Matches, "(devmode,broken|broken,devmode)") +} diff --git a/cmd/snap/test-data/pubring.gpg b/cmd/snap/test-data/pubring.gpg new file mode 100644 index 00000000..8d2ff84e Binary files /dev/null and b/cmd/snap/test-data/pubring.gpg differ diff --git a/cmd/snap/test-data/secring.gpg b/cmd/snap/test-data/secring.gpg new file mode 100644 index 00000000..d7851a85 Binary files /dev/null and b/cmd/snap/test-data/secring.gpg differ diff --git a/cmd/snap/test-data/trustdb.gpg b/cmd/snap/test-data/trustdb.gpg new file mode 100644 index 00000000..2f7c9ad5 Binary files /dev/null and b/cmd/snap/test-data/trustdb.gpg differ diff --git a/cmd/snapctl/main.go b/cmd/snapctl/main.go new file mode 100644 index 00000000..c1e8efa7 --- /dev/null +++ b/cmd/snapctl/main.go @@ -0,0 +1,54 @@ +// -*- 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 . + * + */ + +package main + +import ( + "fmt" + "os" + + "github.com/snapcore/snapd/client" +) + +var clientConfig client.Config + +func main() { + stdout, stderr, err := run() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %s\n", err) + os.Exit(1) + } + + if stdout != nil { + os.Stdout.Write(stdout) + } + + if stderr != nil { + os.Stderr.Write(stderr) + } +} + +func run() (stdout, stderr []byte, err error) { + cli := client.New(&clientConfig) + + return cli.RunSnapctl(&client.SnapCtlOptions{ + ContextID: os.Getenv("SNAP_CONTEXT"), + Args: os.Args[1:], + }) +} diff --git a/cmd/snapctl/main_test.go b/cmd/snapctl/main_test.go new file mode 100644 index 00000000..ae7251dd --- /dev/null +++ b/cmd/snapctl/main_test.go @@ -0,0 +1,108 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/snapcore/snapd/client" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type snapctlSuite struct { + server *httptest.Server + oldArgs []string + expectedContextID string + expectedArgs []string +} + +var _ = Suite(&snapctlSuite{}) + +func (s *snapctlSuite) SetUpTest(c *C) { + os.Setenv("SNAP_CONTEXT", "snap-context-test") + n := 0 + s.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Assert(r.Method, Equals, "POST") + c.Assert(r.URL.Path, Equals, "/v2/snapctl") + + var snapctlOptions client.SnapCtlOptions + decoder := json.NewDecoder(r.Body) + c.Assert(decoder.Decode(&snapctlOptions), IsNil) + c.Assert(snapctlOptions.ContextID, Equals, s.expectedContextID) + c.Assert(snapctlOptions.Args, DeepEquals, s.expectedArgs) + + fmt.Fprintln(w, `{"type": "sync", "result": {"stdout": "test stdout", "stderr": "test stderr"}}`) + default: + c.Fatalf("expected to get 1 request, now on %d", n+1) + } + + n++ + })) + clientConfig.BaseURL = s.server.URL + s.oldArgs = os.Args + os.Args = []string{"snapctl"} + s.expectedContextID = "snap-context-test" + s.expectedArgs = []string{} +} + +func (s *snapctlSuite) TearDownTest(c *C) { + os.Unsetenv("SNAP_CONTEXT") + clientConfig.BaseURL = "" + s.server.Close() + os.Args = s.oldArgs +} + +func (s *snapctlSuite) TestSnapctl(c *C) { + stdout, stderr, err := run() + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "test stdout") + c.Check(string(stderr), Equals, "test stderr") +} + +func (s *snapctlSuite) TestSnapctlWithArgs(c *C) { + os.Args = []string{"snapctl", "foo", "--bar"} + + s.expectedArgs = []string{"foo", "--bar"} + stdout, stderr, err := run() + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "test stdout") + c.Check(string(stderr), Equals, "test stderr") +} + +func (s *snapctlSuite) TestSnapctlHelp(c *C) { + os.Unsetenv("SNAP_CONTEXT") + s.expectedContextID = "" + + os.Args = []string{"snapctl", "-h"} + s.expectedArgs = []string{"-h"} + + _, _, err := run() + c.Check(err, IsNil) +} diff --git a/cmd/snapd/main.go b/cmd/snapd/main.go new file mode 100644 index 00000000..196fb61c --- /dev/null +++ b/cmd/snapd/main.go @@ -0,0 +1,73 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/daemon" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/store" +) + +func init() { + err := logger.SimpleSetup() + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: failed to activate logging: %s\n", err) + } +} + +func main() { + cmd.ExecInCoreSnap() + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run() error { + store.SetUserAgentFromVersion(cmd.Version) + + d, err := daemon.New() + if err != nil { + return err + } + if err := d.Init(); err != nil { + return err + } + d.Version = cmd.Version + + d.Start() + + ch := make(chan os.Signal) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + select { + case sig := <-ch: + logger.Noticef("Exiting on %s signal.\n", sig) + case <-d.Dying(): + // something called Stop() + } + + return d.Stop() +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 00000000..f75f31ab --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,31 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package cmd + +//go:generate mkversion.sh + +// Version will be overwritten at build-time via mkversion.sh +var Version = "unknown" + +func MockVersion(version string) (restore func()) { + old := Version + Version = version + return func() { Version = old } +} diff --git a/daemon/api.go b/daemon/api.go new file mode 100644 index 00000000..b16c98a8 --- /dev/null +++ b/daemon/api.go @@ -0,0 +1,2299 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package daemon + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime" + "mime/multipart" + "net/http" + "os" + "os/user" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/snapasserts" + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n/dumb" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/configstate" + "github.com/snapcore/snapd/overlord/devicestate" + "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" + "github.com/snapcore/snapd/overlord/ifacestate" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/strutil" +) + +var api = []*Command{ + rootCmd, + sysInfoCmd, + loginCmd, + logoutCmd, + appIconCmd, + findCmd, + snapsCmd, + snapCmd, + snapConfCmd, + interfacesCmd, + assertsCmd, + assertsFindManyCmd, + stateChangeCmd, + stateChangesCmd, + createUserCmd, + buyCmd, + readyToBuyCmd, + snapctlCmd, + usersCmd, + sectionsCmd, + aliasesCmd, +} + +var ( + rootCmd = &Command{ + Path: "/", + GuestOK: true, + GET: tbd, + } + + sysInfoCmd = &Command{ + Path: "/v2/system-info", + GuestOK: true, + GET: sysInfo, + } + + loginCmd = &Command{ + Path: "/v2/login", + POST: loginUser, + } + + logoutCmd = &Command{ + Path: "/v2/logout", + POST: logoutUser, + UserOK: true, + } + + appIconCmd = &Command{ + Path: "/v2/icons/{name}/icon", + UserOK: true, + GET: appIconGet, + } + + findCmd = &Command{ + Path: "/v2/find", + UserOK: true, + GET: searchStore, + } + + snapsCmd = &Command{ + Path: "/v2/snaps", + UserOK: true, + GET: getSnapsInfo, + POST: postSnaps, + } + + snapCmd = &Command{ + Path: "/v2/snaps/{name}", + UserOK: true, + GET: getSnapInfo, + POST: postSnap, + } + + snapConfCmd = &Command{ + Path: "/v2/snaps/{name}/conf", + GET: getSnapConf, + PUT: setSnapConf, + } + + interfacesCmd = &Command{ + Path: "/v2/interfaces", + UserOK: true, + GET: getInterfaces, + POST: changeInterfaces, + } + + // TODO: allow to post assertions for UserOK? they are verified anyway + assertsCmd = &Command{ + Path: "/v2/assertions", + POST: doAssert, + } + + assertsFindManyCmd = &Command{ + Path: "/v2/assertions/{assertType}", + UserOK: true, + GET: assertsFindMany, + } + + stateChangeCmd = &Command{ + Path: "/v2/changes/{id}", + UserOK: true, + GET: getChange, + POST: abortChange, + } + + stateChangesCmd = &Command{ + Path: "/v2/changes", + UserOK: true, + GET: getChanges, + } + + createUserCmd = &Command{ + Path: "/v2/create-user", + UserOK: false, + POST: postCreateUser, + } + + buyCmd = &Command{ + Path: "/v2/buy", + UserOK: false, + POST: postBuy, + } + + readyToBuyCmd = &Command{ + Path: "/v2/buy/ready", + UserOK: false, + GET: readyToBuy, + } + + snapctlCmd = &Command{ + Path: "/v2/snapctl", + SnapOK: true, + POST: runSnapctl, + } + + usersCmd = &Command{ + Path: "/v2/users", + UserOK: false, + GET: getUsers, + } + + sectionsCmd = &Command{ + Path: "/v2/sections", + UserOK: true, + GET: getSections, + } + + aliasesCmd = &Command{ + Path: "/v2/aliases", + UserOK: true, + GET: getAliases, + POST: changeAliases, + } +) + +func tbd(c *Command, r *http.Request, user *auth.UserState) Response { + return SyncResponse([]string{"TBD"}, nil) +} + +func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response { + st := c.d.overlord.State() + st.Lock() + users, err := auth.Users(st) + st.Unlock() + if err != nil && err != state.ErrNoState { + return InternalError("cannot get user auth data: %s", err) + } + + m := map[string]interface{}{ + "series": release.Series, + "version": c.d.Version, + "os-release": release.ReleaseInfo, + "on-classic": release.OnClassic, + "managed": len(users) > 0, + } + + // TODO: set the store-id here from the model information + if storeID := os.Getenv("UBUNTU_STORE_ID"); storeID != "" { + m["store"] = storeID + } + + return SyncResponse(m, nil) +} + +// userResponseData contains the data releated to user creation/login/query +type userResponseData struct { + ID int `json:"id,omitempty"` + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + SSHKeys []string `json:"ssh-keys,omitempty"` + + Macaroon string `json:"macaroon,omitempty"` + Discharges []string `json:"discharges,omitempty"` +} + +var isEmailish = regexp.MustCompile(`.@.*\..`).MatchString + +func loginUser(c *Command, r *http.Request, user *auth.UserState) Response { + var loginData struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + Otp string `json:"otp"` + } + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&loginData); err != nil { + return BadRequest("cannot decode login data from request body: %v", err) + } + + if loginData.Email == "" && isEmailish(loginData.Username) { + // for backwards compatibility, if no email is provided assume username is the email + loginData.Email = loginData.Username + loginData.Username = "" + } + + if loginData.Email == "" && user != nil && user.Email != "" { + loginData.Email = user.Email + } + + // the "username" needs to look a lot like an email address + if !isEmailish(loginData.Email) { + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: "please use a valid email address.", + Kind: errorKindInvalidAuthData, + Value: map[string][]string{"email": {"invalid"}}, + }, + Status: http.StatusBadRequest, + }, nil) + } + + macaroon, discharge, err := store.LoginUser(loginData.Email, loginData.Password, loginData.Otp) + switch err { + case store.ErrAuthenticationNeeds2fa: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Kind: errorKindTwoFactorRequired, + Message: err.Error(), + }, + Status: http.StatusUnauthorized, + }, nil) + case store.Err2faFailed: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Kind: errorKindTwoFactorFailed, + Message: err.Error(), + }, + Status: http.StatusUnauthorized, + }, nil) + default: + if err, ok := err.(store.ErrInvalidAuthData); ok { + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindInvalidAuthData, + Value: err, + }, + Status: http.StatusBadRequest, + }, nil) + } + return Unauthorized(err.Error()) + case nil: + // continue + } + overlord := c.d.overlord + state := overlord.State() + state.Lock() + if user != nil { + // local user logged-in, set its store macaroons + user.StoreMacaroon = macaroon + user.StoreDischarges = []string{discharge} + err = auth.UpdateUser(state, user) + } else { + user, err = auth.NewUser(state, loginData.Username, loginData.Email, macaroon, []string{discharge}) + } + state.Unlock() + if err != nil { + return InternalError("cannot persist authentication details: %v", err) + } + + result := userResponseData{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Macaroon: user.Macaroon, + Discharges: user.Discharges, + } + return SyncResponse(result, nil) +} + +func logoutUser(c *Command, r *http.Request, user *auth.UserState) Response { + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + + if user == nil { + return BadRequest("not logged in") + } + err := auth.RemoveUser(state, user.ID) + if err != nil { + return InternalError(err.Error()) + } + + return SyncResponse(nil, nil) +} + +// UserFromRequest extracts user information from request and return the respective user in state, if valid +// It requires the state to be locked +func UserFromRequest(st *state.State, req *http.Request) (*auth.UserState, error) { + // extract macaroons data from request + header := req.Header.Get("Authorization") + if header == "" { + return nil, auth.ErrInvalidAuth + } + + authorizationData := strings.SplitN(header, " ", 2) + if len(authorizationData) != 2 || authorizationData[0] != "Macaroon" { + return nil, fmt.Errorf("authorization header misses Macaroon prefix") + } + + var macaroon string + var discharges []string + for _, field := range strings.Split(authorizationData[1], ",") { + field := strings.TrimSpace(field) + if strings.HasPrefix(field, `root="`) { + macaroon = strings.TrimSuffix(field[6:], `"`) + } + if strings.HasPrefix(field, `discharge="`) { + discharges = append(discharges, strings.TrimSuffix(field[11:], `"`)) + } + } + + if macaroon == "" { + return nil, fmt.Errorf("invalid authorization header") + } + + user, err := auth.CheckMacaroon(st, macaroon, discharges) + return user, err +} + +var muxVars = mux.Vars + +func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response { + vars := muxVars(r) + name := vars["name"] + + about, err := localSnapInfo(c.d.overlord.State(), name) + if err != nil { + if err == errNoSnap { + return NotFound("cannot find %q snap", name) + } + + return InternalError("%v", err) + } + + route := c.d.router.Get(c.Path) + if route == nil { + return InternalError("cannot find route for %q snap", name) + } + + url, err := route.URL("name", name) + if err != nil { + return InternalError("cannot build URL for %q snap: %v", name, err) + } + + result := webify(mapLocal(about), url.String()) + + return SyncResponse(result, nil) +} + +func webify(result map[string]interface{}, resource string) map[string]interface{} { + result["resource"] = resource + + icon, ok := result["icon"].(string) + if !ok || icon == "" || strings.HasPrefix(icon, "http") { + return result + } + result["icon"] = "" + + route := appIconCmd.d.router.Get(appIconCmd.Path) + if route != nil { + name, _ := result["name"].(string) + url, err := route.URL("name", name) + if err == nil { + result["icon"] = url.String() + } + } + + return result +} + +func getStore(c *Command) snapstate.StoreService { + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + return snapstate.Store(st) +} + +func getSections(c *Command, r *http.Request, user *auth.UserState) Response { + route := c.d.router.Get(snapCmd.Path) + if route == nil { + return InternalError("cannot find route for snaps") + } + + theStore := getStore(c) + + sections, err := theStore.Sections(user) + switch err { + case nil: + // pass + case store.ErrEmptyQuery, store.ErrBadQuery: + return BadRequest("%v", err) + case store.ErrUnauthenticated: + return Unauthorized("%v", err) + default: + return InternalError("%v", err) + } + + return SyncResponse(sections, &Meta{}) +} + +func searchStore(c *Command, r *http.Request, user *auth.UserState) Response { + route := c.d.router.Get(snapCmd.Path) + if route == nil { + return InternalError("cannot find route for snaps") + } + query := r.URL.Query() + q := query.Get("q") + section := query.Get("section") + name := query.Get("name") + private := false + prefix := false + + if name != "" { + if q != "" { + return BadRequest("cannot use 'q' and 'name' together") + } + + if name[len(name)-1] != '*' { + return findOne(c, r, user, name) + } + + prefix = true + q = name[:len(name)-1] + } + + if sel := query.Get("select"); sel != "" { + switch sel { + case "refresh": + if prefix { + return BadRequest("cannot use 'name' with 'select=refresh'") + } + if q != "" { + return BadRequest("cannot use 'q' with 'select=refresh'") + } + return storeUpdates(c, r, user) + case "private": + private = true + } + } + + theStore := getStore(c) + found, err := theStore.Find(&store.Search{ + Query: q, + Section: section, + Private: private, + Prefix: prefix, + }, user) + switch err { + case nil: + // pass + case store.ErrEmptyQuery, store.ErrBadQuery: + return BadRequest("%v", err) + case store.ErrUnauthenticated: + return Unauthorized(err.Error()) + default: + return InternalError("%v", err) + } + + meta := &Meta{ + SuggestedCurrency: theStore.SuggestedCurrency(), + Sources: []string{"store"}, + } + + return sendStorePackages(route, meta, found) +} + +func findOne(c *Command, r *http.Request, user *auth.UserState, name string) Response { + if err := snap.ValidateName(name); err != nil { + return BadRequest(err.Error()) + } + + theStore := getStore(c) + spec := store.SnapSpec{ + Name: name, + Channel: "", + Revision: snap.R(0), + } + snapInfo, err := theStore.SnapInfo(spec, user) + if err != nil { + return InternalError("%v", err) + } + + meta := &Meta{ + SuggestedCurrency: theStore.SuggestedCurrency(), + Sources: []string{"store"}, + } + + results := make([]*json.RawMessage, 1) + data, err := json.Marshal(webify(mapRemote(snapInfo), r.URL.String())) + if err != nil { + return InternalError(err.Error()) + } + results[0] = (*json.RawMessage)(&data) + return SyncResponse(results, meta) +} + +func shouldSearchStore(r *http.Request) bool { + // we should jump to the old behaviour iff q is given, or if + // sources is given and either empty or contains the word + // 'store'. Otherwise, local results only. + + query := r.URL.Query() + + if _, ok := query["q"]; ok { + logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL) + return true + } + + if src, ok := query["sources"]; ok { + logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL) + if len(src) == 0 || strings.Contains(src[0], "store") { + return true + } + } + + return false +} + +func storeUpdates(c *Command, r *http.Request, user *auth.UserState) Response { + route := c.d.router.Get(snapCmd.Path) + if route == nil { + return InternalError("cannot find route for snaps") + } + + state := c.d.overlord.State() + state.Lock() + updates, err := snapstateRefreshCandidates(state, user) + state.Unlock() + if err != nil { + return InternalError("cannot list updates: %v", err) + } + + return sendStorePackages(route, nil, updates) +} + +func sendStorePackages(route *mux.Route, meta *Meta, found []*snap.Info) Response { + results := make([]*json.RawMessage, 0, len(found)) + for _, x := range found { + url, err := route.URL("name", x.Name()) + if err != nil { + logger.Noticef("Cannot build URL for snap %q revision %s: %v", x.Name(), x.Revision, err) + continue + } + + data, err := json.Marshal(webify(mapRemote(x), url.String())) + if err != nil { + return InternalError("%v", err) + } + raw := json.RawMessage(data) + results = append(results, &raw) + } + + return SyncResponse(results, meta) +} + +// plural! +func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response { + + if shouldSearchStore(r) { + logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL) + return searchStore(c, r, user) + } + + route := c.d.router.Get(snapCmd.Path) + if route == nil { + return InternalError("cannot find route for snaps") + } + + var all bool + sel := r.URL.Query().Get("select") + switch sel { + case "all": + all = true + case "enabled", "": + all = false + default: + return BadRequest("invalid select parameter: %q", sel) + } + found, err := allLocalSnapInfos(c.d.overlord.State(), all) + if err != nil { + return InternalError("cannot list local snaps! %v", err) + } + + results := make([]*json.RawMessage, len(found)) + + for i, x := range found { + name := x.info.Name() + rev := x.info.Revision + + url, err := route.URL("name", name) + if err != nil { + logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err) + continue + } + + data, err := json.Marshal(webify(mapLocal(x), url.String())) + if err != nil { + return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err) + } + raw := json.RawMessage(data) + results[i] = &raw + } + + return SyncResponse(results, &Meta{Sources: []string{"local"}}) +} + +func resultHasType(r map[string]interface{}, allowedTypes []string) bool { + for _, t := range allowedTypes { + if r["type"] == t { + return true + } + } + return false +} + +// 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 `json:"intro"` + License string `json:"license"` + Agreed bool `json:"agreed"` +} + +func (*licenseData) Error() string { + return "license agreement required" +} + +type snapInstruction struct { + progress.NullProgress + Action string `json:"action"` + Channel string `json:"channel"` + Revision snap.Revision `json:"revision"` + DevMode bool `json:"devmode"` + JailMode bool `json:"jailmode"` + Classic bool `json:"classic"` + IgnoreValidation bool `json:"ignore-validation"` + // dropping support temporarely until flag confusion is sorted, + // this isn't supported by client atm anyway + LeaveOld bool `json:"temp-dropped-leave-old"` + License *licenseData `json:"license"` + Snaps []string `json:"snaps"` + + // The fields below should not be unmarshalled into. Do not export them. + userID int +} + +var ( + snapstateCoreInfo = snapstate.CoreInfo + snapstateInstall = snapstate.Install + snapstateInstallPath = snapstate.InstallPath + snapstateRefreshCandidates = snapstate.RefreshCandidates + snapstateTryPath = snapstate.TryPath + snapstateUpdate = snapstate.Update + snapstateUpdateMany = snapstate.UpdateMany + snapstateInstallMany = snapstate.InstallMany + snapstateRemoveMany = snapstate.RemoveMany + + assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations +) + +func ensureStateSoonImpl(st *state.State) { + st.EnsureBefore(0) +} + +var ensureStateSoon = ensureStateSoonImpl + +var errNothingToInstall = errors.New("nothing to install") + +const oldDefaultSnapCoreName = "ubuntu-core" +const defaultCoreSnapName = "core" + +func ensureUbuntuCore(st *state.State, targetSnap string, userID int) (*state.TaskSet, error) { + if targetSnap == defaultCoreSnapName || targetSnap == oldDefaultSnapCoreName { + return nil, errNothingToInstall + } + + _, err := snapstateCoreInfo(st) + if err != state.ErrNoState { + return nil, err + } + + return snapstateInstall(st, defaultCoreSnapName, "stable", snap.R(0), userID, snapstate.Flags{}) +} + +func withEnsureUbuntuCore(st *state.State, targetSnap string, userID int, install func() (*state.TaskSet, error)) ([]*state.TaskSet, error) { + ubuCoreTs, err := ensureUbuntuCore(st, targetSnap, userID) + if err != nil && err != errNothingToInstall { + return nil, err + } + + ts, err := install() + if err != nil { + return nil, err + } + + // ensure main install waits on ubuntu core install + if ubuCoreTs != nil { + ts.WaitAll(ubuCoreTs) + return []*state.TaskSet{ubuCoreTs, ts}, nil + } + + return []*state.TaskSet{ts}, nil +} + +var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together") +var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together") +var errNoJailMode = errors.New("this system cannot honour the jailmode flag") + +func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) { + flags := snapstate.Flags{} + devModeOS := release.ReleaseInfo.ForceDevMode() + switch { + case jailMode && devModeOS: + return flags, errNoJailMode + case jailMode && devMode: + return flags, errDevJailModeConflict + case devMode && classic: + return flags, errClassicDevmodeConflict + } + // NOTE: jailmode and classic are allowed together. In that setting, + // jailmode overrides classic and the app gets regular (non-classic) + // confinement. + flags.JailMode = jailMode + flags.Classic = classic + flags.DevMode = devMode || devModeOS && !classic + return flags, nil +} + +func snapUpdateMany(inst *snapInstruction, st *state.State) (msg string, updated []string, tasksets []*state.TaskSet, err error) { + // we need refreshed snap-declarations to enforce refresh-control as best as we can, this also ensures that snap-declarations and their prerequisite assertions are updated regularly + if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { + return "", nil, nil, err + } + + updated, tasksets, err = snapstateUpdateMany(st, inst.Snaps, inst.userID) + if err != nil { + return "", nil, nil, err + } + + switch len(updated) { + case 0: + // not really needed but be paranoid + if len(inst.Snaps) != 0 { + return "", nil, nil, fmt.Errorf("internal error: when asking for a refresh of %s no update was found but no error was generated", strutil.Quoted(inst.Snaps)) + } + // FIXME: instead don't generated a change(?) at all + msg = fmt.Sprintf(i18n.G("Refresh all snaps: no updates")) + case 1: + msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0]) + default: + quoted := strutil.Quoted(updated) + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted) + } + + return msg, updated, tasksets, nil +} + +func snapInstallMany(inst *snapInstruction, st *state.State) (msg string, installed []string, tasksets []*state.TaskSet, err error) { + installed, tasksets, err = snapstateInstallMany(st, inst.Snaps, inst.userID) + if err != nil { + return "", nil, nil, err + } + + switch len(inst.Snaps) { + case 0: + return "", nil, nil, fmt.Errorf("cannot install zero snaps") + case 1: + msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0]) + default: + quoted := strutil.Quoted(inst.Snaps) + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted) + } + + return msg, installed, tasksets, nil +} + +func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + flags, err := modeFlags(inst.DevMode, inst.JailMode, inst.Classic) + if err != nil { + return "", nil, err + } + + logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision) + + tsets, err := withEnsureUbuntuCore(st, inst.Snaps[0], inst.userID, + func() (*state.TaskSet, error) { + return snapstateInstall(st, inst.Snaps[0], inst.Channel, inst.Revision, inst.userID, flags) + }, + ) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0]) + if inst.Channel != "stable" && inst.Channel != "" { + msg = fmt.Sprintf(i18n.G("Install %q snap from %q channel"), inst.Snaps[0], inst.Channel) + } + return msg, tsets, nil +} + +func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + // TODO: bail if revision is given (and != current?), *or* behave as with install --revision? + flags, err := modeFlags(inst.DevMode, inst.JailMode, inst.Classic) + if err != nil { + return "", nil, err + } + if inst.IgnoreValidation { + flags.IgnoreValidation = true + } + + // we need refreshed snap-declarations to enforce refresh-control as best as we can + if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { + return "", nil, err + } + + ts, err := snapstateUpdate(st, inst.Snaps[0], inst.Channel, inst.Revision, inst.userID, flags) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0]) + if inst.Channel != "stable" && inst.Channel != "" { + msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel) + } + + return msg, []*state.TaskSet{ts}, nil +} + +func snapRemoveMany(inst *snapInstruction, st *state.State) (msg string, removed []string, tasksets []*state.TaskSet, err error) { + removed, tasksets, err = snapstateRemoveMany(st, inst.Snaps) + if err != nil { + return "", nil, nil, err + } + + switch len(inst.Snaps) { + case 0: + return "", nil, nil, fmt.Errorf("cannot remove zero snaps") + case 1: + msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0]) + default: + quoted := strutil.Quoted(inst.Snaps) + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted) + } + + return msg, removed, tasksets, nil +} + +func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0]) + return msg, []*state.TaskSet{ts}, nil +} + +func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + var ts *state.TaskSet + + flags, err := modeFlags(inst.DevMode, inst.JailMode, inst.Classic) + if err != nil { + return "", nil, err + } + + if inst.Revision.Unset() { + ts, err = snapstate.Revert(st, inst.Snaps[0], flags) + } else { + ts, err = snapstate.RevertToRevision(st, inst.Snaps[0], inst.Revision, flags) + } + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0]) + return msg, []*state.TaskSet{ts}, nil +} + +func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + if !inst.Revision.Unset() { + return "", nil, errors.New("enable takes no revision") + } + ts, err := snapstate.Enable(st, inst.Snaps[0]) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0]) + return msg, []*state.TaskSet{ts}, nil +} + +func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + if !inst.Revision.Unset() { + return "", nil, errors.New("disable takes no revision") + } + ts, err := snapstate.Disable(st, inst.Snaps[0]) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0]) + return msg, []*state.TaskSet{ts}, nil +} + +type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) + +var snapInstructionDispTable = map[string]snapActionFunc{ + "install": snapInstall, + "refresh": snapUpdate, + "remove": snapRemove, + "revert": snapRevert, + "enable": snapEnable, + "disable": snapDisable, +} + +func (inst *snapInstruction) dispatch() snapActionFunc { + if len(inst.Snaps) != 1 { + logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps)) + } + return snapInstructionDispTable[inst.Action] +} + +func (inst *snapInstruction) errToResponse(err error) Response { + if _, ok := err.(*snap.AlreadyInstalledError); ok { + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindSnapAlreadyInstalled, + }, + Status: http.StatusBadRequest, + }, nil) + } + if _, ok := err.(*snap.NotInstalledError); ok { + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindSnapNotInstalled, + }, + Status: http.StatusBadRequest, + }, nil) + } + if _, ok := err.(*snap.NoUpdateAvailableError); ok { + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindSnapNoUpdateAvailable, + }, + Status: http.StatusBadRequest, + }, nil) + } + return BadRequest("cannot %s %q: %v", inst.Action, inst.Snaps[0], err) +} + +func postSnap(c *Command, r *http.Request, user *auth.UserState) Response { + route := c.d.router.Get(stateChangeCmd.Path) + if route == nil { + return InternalError("cannot find route for change") + } + + decoder := json.NewDecoder(r.Body) + var inst snapInstruction + if err := decoder.Decode(&inst); err != nil { + return BadRequest("cannot decode request body into snap instruction: %v", err) + } + + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + + if user != nil { + inst.userID = user.ID + } + + vars := muxVars(r) + inst.Snaps = []string{vars["name"]} + + impl := inst.dispatch() + if impl == nil { + return BadRequest("unknown action %s", inst.Action) + } + + msg, tsets, err := impl(&inst, state) + if err != nil { + return inst.errToResponse(err) + } + + chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps) + + ensureStateSoon(state) + + return AsyncResponse(nil, &Meta{Change: chg.ID()}) +} + +func newChange(st *state.State, kind, summary string, tsets []*state.TaskSet, snapNames []string) *state.Change { + chg := st.NewChange(kind, summary) + for _, ts := range tsets { + chg.AddAll(ts) + } + if snapNames != nil { + chg.Set("snap-names", snapNames) + } + return chg +} + +const maxReadBuflen = 1024 * 1024 + +func trySnap(c *Command, r *http.Request, user *auth.UserState, trydir string, flags snapstate.Flags) Response { + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + if !filepath.IsAbs(trydir) { + return BadRequest("cannot try %q: need an absolute path", trydir) + } + if !osutil.IsDirectory(trydir) { + return BadRequest("cannot try %q: not a snap directory", trydir) + } + + // the developer asked us to do this with a trusted snap dir + info, err := unsafeReadSnapInfo(trydir) + if err != nil { + return BadRequest("cannot read snap info for %s: %s", trydir, err) + } + + var userID int + if user != nil { + userID = user.ID + } + tsets, err := withEnsureUbuntuCore(st, info.Name(), userID, + func() (*state.TaskSet, error) { + return snapstateTryPath(st, info.Name(), trydir, flags) + }, + ) + if err != nil { + return BadRequest("cannot try %s: %s", trydir, err) + } + + msg := fmt.Sprintf(i18n.G("Try %q snap from %s"), info.Name(), trydir) + chg := newChange(st, "try-snap", msg, tsets, []string{info.Name()}) + chg.Set("api-data", map[string]string{"snap-name": info.Name()}) + + ensureStateSoon(st) + + return AsyncResponse(nil, &Meta{Change: chg.ID()}) +} + +func isTrue(form *multipart.Form, key string) bool { + value := form.Value[key] + if len(value) == 0 { + return false + } + b, err := strconv.ParseBool(value[0]) + if err != nil { + return false + } + + return b +} + +func snapsOp(c *Command, r *http.Request, user *auth.UserState) Response { + route := c.d.router.Get(stateChangeCmd.Path) + if route == nil { + return InternalError("cannot find route for change") + } + + decoder := json.NewDecoder(r.Body) + var inst snapInstruction + if err := decoder.Decode(&inst); err != nil { + return BadRequest("cannot decode request body into snap instruction: %v", err) + } + + if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode { + return BadRequest("unsupported option provided for multi-snap operation") + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + if user != nil { + inst.userID = user.ID + } + + var msg string + var affected []string + var tsets []*state.TaskSet + var err error + switch inst.Action { + case "refresh": + msg, affected, tsets, err = snapUpdateMany(&inst, st) + case "install": + msg, affected, tsets, err = snapInstallMany(&inst, st) + case "remove": + msg, affected, tsets, err = snapRemoveMany(&inst, st) + default: + return BadRequest("unsupported multi-snap operation %q", inst.Action) + } + if err != nil { + return InternalError("cannot %s %q: %v", inst.Action, inst.Snaps, err) + } + + var chg *state.Change + if len(tsets) == 0 { + chg = st.NewChange(inst.Action+"-snap", msg) + chg.SetStatus(state.DoneStatus) + } else { + chg = newChange(st, inst.Action+"-snap", msg, tsets, affected) + ensureStateSoon(st) + } + chg.Set("api-data", map[string]interface{}{"snap-names": affected}) + + return AsyncResponse(nil, &Meta{Change: chg.ID()}) +} + +func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response { + contentType := r.Header.Get("Content-Type") + + if contentType == "application/json" { + return snapsOp(c, r, user) + } + + if !strings.HasPrefix(contentType, "multipart/") { + return BadRequest("unknown content type: %s", contentType) + } + + route := c.d.router.Get(stateChangeCmd.Path) + if route == nil { + return InternalError("cannot find route for change") + } + + // POSTs to sideload snaps must be a multipart/form-data file upload. + _, params, err := mime.ParseMediaType(contentType) + if err != nil { + return BadRequest("cannot parse POST body: %v", err) + } + + form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(maxReadBuflen) + if err != nil { + return BadRequest("cannot read POST form: %v", err) + } + + dangerousOK := isTrue(form, "dangerous") + flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic")) + if err != nil { + return BadRequest(err.Error()) + } + flags.RemoveSnapPath = true + + if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" { + if len(form.Value["snap-path"]) == 0 { + return BadRequest("need 'snap-path' value in form") + } + return trySnap(c, r, user, form.Value["snap-path"][0], flags) + } + + // find the file for the "snap" form field + var snapBody multipart.File + var origPath string +out: + for name, fheaders := range form.File { + if name != "snap" { + continue + } + for _, fheader := range fheaders { + snapBody, err = fheader.Open() + origPath = fheader.Filename + if err != nil { + return BadRequest(`cannot open uploaded "snap" file: %v`, err) + } + defer snapBody.Close() + + break out + } + } + defer form.RemoveAll() + + if snapBody == nil { + return BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`) + } + + // we are in charge of the tempfile life cycle until we hand it off to the change + changeTriggered := false + // if you change this prefix, look for it in the tests + tmpf, err := ioutil.TempFile("", "snapd-sideload-pkg-") + if err != nil { + return InternalError("cannot create temporary file: %v", err) + } + + tempPath := tmpf.Name() + + defer func() { + if !changeTriggered { + os.Remove(tempPath) + } + }() + + if _, err := io.Copy(tmpf, snapBody); err != nil { + return InternalError("cannot copy request into temporary file: %v", err) + } + tmpf.Sync() + + if len(form.Value["snap-path"]) > 0 { + origPath = form.Value["snap-path"][0] + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + var snapName string + var sideInfo *snap.SideInfo + + if !dangerousOK { + si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st)) + switch err { + case nil: + snapName = si.RealName + sideInfo = si + case asserts.ErrNotFound: + // with devmode we try to find assertions but it's ok + // if they are not there (implies --dangerous) + if !isTrue(form, "devmode") { + msg := "cannot find signatures with metadata for snap" + if origPath != "" { + msg = fmt.Sprintf("%s %q", msg, origPath) + } + return BadRequest(msg) + } + // TODO: set a warning if devmode + default: + return BadRequest(err.Error()) + } + } + + if snapName == "" { + // potentially dangerous but dangerous or devmode params were set + info, err := unsafeReadSnapInfo(tempPath) + if err != nil { + return BadRequest("cannot read snap file: %v", err) + } + snapName = info.Name() + sideInfo = &snap.SideInfo{RealName: snapName} + } + + msg := fmt.Sprintf(i18n.G("Install %q snap from file"), snapName) + if origPath != "" { + msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), snapName, origPath) + } + + var userID int + if user != nil { + userID = user.ID + } + + tsets, err := withEnsureUbuntuCore(st, snapName, userID, + func() (*state.TaskSet, error) { + return snapstateInstallPath(st, sideInfo, tempPath, "", flags) + }, + ) + if err != nil { + return InternalError("cannot install snap file: %v", err) + } + + chg := newChange(st, "install-snap", msg, tsets, []string{snapName}) + chg.Set("api-data", map[string]string{"snap-name": snapName}) + + ensureStateSoon(st) + + // only when the unlock succeeds (as opposed to panicing) is the handoff done + // but this is good enough + changeTriggered = true + + return AsyncResponse(nil, &Meta{Change: chg.ID()}) +} + +func unsafeReadSnapInfoImpl(snapPath string) (*snap.Info, error) { + // Condider using DeriveSideInfo before falling back to this! + snapf, err := snap.Open(snapPath) + if err != nil { + return nil, err + } + return snap.ReadInfoFromSnapFile(snapf, nil) +} + +var unsafeReadSnapInfo = unsafeReadSnapInfoImpl + +func iconGet(st *state.State, name string) Response { + about, err := localSnapInfo(st, name) + if err != nil { + if err == errNoSnap { + return NotFound("cannot find snap %q", name) + } + return InternalError("%v", err) + } + + path := filepath.Clean(snapIcon(about.info)) + if !strings.HasPrefix(path, dirs.SnapMountDir) { + // XXX: how could this happen? + return BadRequest("requested icon is not in snap path") + } + + return FileResponse(path) +} + +func appIconGet(c *Command, r *http.Request, user *auth.UserState) Response { + vars := muxVars(r) + name := vars["name"] + + return iconGet(c.d.overlord.State(), name) +} + +func getSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { + vars := muxVars(r) + snapName := vars["name"] + + keys := strings.Split(r.URL.Query().Get("keys"), ",") + if len(keys) == 0 { + return BadRequest("cannot obtain configuration: no keys supplied") + } + + s := c.d.overlord.State() + s.Lock() + transaction := configstate.NewTransaction(s) + s.Unlock() + + currentConfValues := make(map[string]interface{}) + for _, key := range keys { + var value interface{} + if err := transaction.Get(snapName, key, &value); err != nil { + return BadRequest("%s", err) + } + + currentConfValues[key] = value + } + + return SyncResponse(currentConfValues, nil) +} + +func setSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { + vars := muxVars(r) + snapName := vars["name"] + + var patchValues map[string]interface{} + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&patchValues); err != nil { + return BadRequest("cannot decode request body into patch values: %v", err) + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + var snapst snapstate.SnapState + if err := snapstate.Get(st, snapName, &snapst); err == state.ErrNoState { + return NotFound("cannot find %q snap", snapName) + } else if err != nil { + return InternalError("%v", err) + } + + taskset := configstate.Configure(st, snapName, patchValues) + + summary := fmt.Sprintf("Change configuration of %q snap", snapName) + change := newChange(st, "configure-snap", summary, []*state.TaskSet{taskset}, []string{snapName}) + + st.EnsureBefore(0) + + return AsyncResponse(nil, &Meta{Change: change.ID()}) +} + +// getInterfaces returns all plugs and slots. +func getInterfaces(c *Command, r *http.Request, user *auth.UserState) Response { + repo := c.d.overlord.InterfaceManager().Repository() + return SyncResponse(repo.Interfaces(), nil) +} + +// plugJSON aids in marshaling Plug into JSON. +type plugJSON struct { + Snap string `json:"snap"` + Name string `json:"plug"` + Interface string `json:"interface"` + Attrs map[string]interface{} `json:"attrs,omitempty"` + Apps []string `json:"apps,omitempty"` + Label string `json:"label"` + Connections []interfaces.SlotRef `json:"connections,omitempty"` +} + +// slotJSON aids in marshaling Slot into JSON. +type slotJSON struct { + Snap string `json:"snap"` + Name string `json:"slot"` + Interface string `json:"interface"` + Attrs map[string]interface{} `json:"attrs,omitempty"` + Apps []string `json:"apps,omitempty"` + Label string `json:"label"` + Connections []interfaces.PlugRef `json:"connections,omitempty"` +} + +// interfaceAction is an action performed on the interface system. +type interfaceAction struct { + Action string `json:"action"` + Plugs []plugJSON `json:"plugs,omitempty"` + Slots []slotJSON `json:"slots,omitempty"` +} + +// changeInterfaces controls the interfaces system. +// Plugs can be connected to and disconnected from slots. +// When enableInternalInterfaceActions is true plugs and slots can also be +// explicitly added and removed. +func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Response { + var a interfaceAction + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&a); err != nil { + return BadRequest("cannot decode request body into an interface action: %v", err) + } + if a.Action == "" { + return BadRequest("interface action not specified") + } + if !c.d.enableInternalInterfaceActions && a.Action != "connect" && a.Action != "disconnect" { + return BadRequest("internal interface actions are disabled") + } + if len(a.Plugs) > 1 || len(a.Slots) > 1 { + return NotImplemented("many-to-many operations are not implemented") + } + if a.Action != "connect" && a.Action != "disconnect" { + return BadRequest("unsupported interface action: %q", a.Action) + } + if len(a.Plugs) == 0 || len(a.Slots) == 0 { + return BadRequest("at least one plug and slot is required") + } + + var summary string + var taskset *state.TaskSet + var err error + + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + + switch a.Action { + case "connect": + var connRef interfaces.ConnRef + repo := c.d.overlord.InterfaceManager().Repository() + connRef, err = repo.ResolveConnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + if err == nil { + summary = fmt.Sprintf("Connect %s:%s to %s:%s", connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) + taskset, err = ifacestate.Connect(state, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) + } + case "disconnect": + summary = fmt.Sprintf("Disconnect %s:%s from %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + taskset, err = ifacestate.Disconnect(state, a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + } + if err != nil { + return BadRequest("%v", err) + } + + change := state.NewChange(a.Action+"-snap", summary) + change.Set("snap-names", []string{a.Plugs[0].Snap, a.Slots[0].Snap}) + change.AddAll(taskset) + + state.EnsureBefore(0) + + return AsyncResponse(nil, &Meta{Change: change.ID()}) +} + +func doAssert(c *Command, r *http.Request, user *auth.UserState) Response { + batch := assertstate.NewBatch() + _, err := batch.AddStream(r.Body) + if err != nil { + return BadRequest("cannot decode request body into assertions: %v", err) + } + + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + + if err := batch.Commit(state); err != nil { + return BadRequest("assert failed: %v", err) + } + // TODO: what more info do we want to return on success? + return &resp{ + Type: ResponseTypeSync, + Status: http.StatusOK, + } +} + +func assertsFindMany(c *Command, r *http.Request, user *auth.UserState) Response { + assertTypeName := muxVars(r)["assertType"] + assertType := asserts.Type(assertTypeName) + if assertType == nil { + return BadRequest("invalid assert type: %q", assertTypeName) + } + headers := map[string]string{} + q := r.URL.Query() + for k := range q { + headers[k] = q.Get(k) + } + + state := c.d.overlord.State() + state.Lock() + db := assertstate.DB(state) + state.Unlock() + + assertions, err := db.FindMany(assertType, headers) + if err == asserts.ErrNotFound { + return AssertResponse(nil, true) + } else if err != nil { + return InternalError("searching assertions failed: %v", err) + } + return AssertResponse(assertions, true) +} + +type changeInfo struct { + ID string `json:"id"` + Kind string `json:"kind"` + Summary string `json:"summary"` + Status string `json:"status"` + Tasks []*taskInfo `json:"tasks,omitempty"` + Ready bool `json:"ready"` + Err string `json:"err,omitempty"` + + SpawnTime time.Time `json:"spawn-time,omitempty"` + ReadyTime *time.Time `json:"ready-time,omitempty"` + + Data map[string]*json.RawMessage `json:"data,omitempty"` +} + +type taskInfo struct { + ID string `json:"id"` + Kind string `json:"kind"` + Summary string `json:"summary"` + Status string `json:"status"` + Log []string `json:"log,omitempty"` + Progress taskInfoProgress `json:"progress"` + + SpawnTime time.Time `json:"spawn-time,omitempty"` + ReadyTime *time.Time `json:"ready-time,omitempty"` +} + +type taskInfoProgress struct { + Label string `json:"label"` + Done int `json:"done"` + Total int `json:"total"` +} + +func change2changeInfo(chg *state.Change) *changeInfo { + status := chg.Status() + chgInfo := &changeInfo{ + ID: chg.ID(), + Kind: chg.Kind(), + Summary: chg.Summary(), + Status: status.String(), + Ready: status.Ready(), + + SpawnTime: chg.SpawnTime(), + } + readyTime := chg.ReadyTime() + if !readyTime.IsZero() { + chgInfo.ReadyTime = &readyTime + } + if err := chg.Err(); err != nil { + chgInfo.Err = err.Error() + } + + tasks := chg.Tasks() + taskInfos := make([]*taskInfo, len(tasks)) + for j, t := range tasks { + label, done, total := t.Progress() + + taskInfo := &taskInfo{ + ID: t.ID(), + Kind: t.Kind(), + Summary: t.Summary(), + Status: t.Status().String(), + Log: t.Log(), + Progress: taskInfoProgress{ + Label: label, + Done: done, + Total: total, + }, + SpawnTime: t.SpawnTime(), + } + readyTime := t.ReadyTime() + if !readyTime.IsZero() { + taskInfo.ReadyTime = &readyTime + } + taskInfos[j] = taskInfo + } + chgInfo.Tasks = taskInfos + + var data map[string]*json.RawMessage + if chg.Get("api-data", &data) == nil { + chgInfo.Data = data + } + + return chgInfo +} + +func getChange(c *Command, r *http.Request, user *auth.UserState) Response { + chID := muxVars(r)["id"] + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + chg := state.Change(chID) + if chg == nil { + return NotFound("cannot find change with id %q", chID) + } + + return SyncResponse(change2changeInfo(chg), nil) +} + +func getChanges(c *Command, r *http.Request, user *auth.UserState) Response { + query := r.URL.Query() + qselect := query.Get("select") + if qselect == "" { + qselect = "in-progress" + } + var filter func(*state.Change) bool + switch qselect { + case "all": + filter = func(*state.Change) bool { return true } + case "in-progress": + filter = func(chg *state.Change) bool { return !chg.Status().Ready() } + case "ready": + filter = func(chg *state.Change) bool { return chg.Status().Ready() } + default: + return BadRequest("select should be one of: all,in-progress,ready") + } + + if wantedName := query.Get("for"); wantedName != "" { + outerFilter := filter + filter = func(chg *state.Change) bool { + if !outerFilter(chg) { + return false + } + + var snapNames []string + if err := chg.Get("snap-names", &snapNames); err != nil { + logger.Noticef("Cannot get snap-name for change %v", chg.ID()) + return false + } + + for _, snapName := range snapNames { + if snapName == wantedName { + return true + } + } + + return false + } + } + + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + chgs := state.Changes() + chgInfos := make([]*changeInfo, 0, len(chgs)) + for _, chg := range chgs { + if !filter(chg) { + continue + } + chgInfos = append(chgInfos, change2changeInfo(chg)) + } + return SyncResponse(chgInfos, nil) +} + +func abortChange(c *Command, r *http.Request, user *auth.UserState) Response { + chID := muxVars(r)["id"] + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + chg := state.Change(chID) + if chg == nil { + return NotFound("cannot find change with id %q", chID) + } + + var reqData struct { + Action string `json:"action"` + } + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&reqData); err != nil { + return BadRequest("cannot decode data from request body: %v", err) + } + + if reqData.Action != "abort" { + return BadRequest("change action %q is unsupported", reqData.Action) + } + + if chg.Status().Ready() { + return BadRequest("cannot abort change %s with nothing pending", chID) + } + + // flag the change + chg.Abort() + + // actually ask to proceed with the abort + ensureStateSoon(state) + + return SyncResponse(change2changeInfo(chg), nil) +} + +var ( + postCreateUserUcrednetGetUID = ucrednetGetUID + storeUserInfo = store.UserInfo + osutilAddUser = osutil.AddUser +) + +func getUserDetailsFromStore(email string) (string, *osutil.AddUserOptions, error) { + v, err := storeUserInfo(email) + if err != nil { + return "", nil, fmt.Errorf("cannot create user %q: %s", email, err) + } + if len(v.SSHKeys) == 0 { + return "", nil, fmt.Errorf("cannot create user for %q: no ssh keys found", email) + } + + gecos := fmt.Sprintf("%s,%s", email, v.OpenIDIdentifier) + opts := &osutil.AddUserOptions{ + SSHKeys: v.SSHKeys, + Gecos: gecos, + } + return v.Username, opts, nil +} + +func createAllKnownSystemUsers(st *state.State, createData *postUserCreateData) Response { + var createdUsers []userResponseData + + st.Lock() + db := assertstate.DB(st) + modelAs, err := devicestate.Model(st) + st.Unlock() + if err != nil { + return InternalError("cannot get model assertion") + } + + headers := map[string]string{ + "brand-id": modelAs.BrandID(), + } + st.Lock() + assertions, err := db.FindMany(asserts.SystemUserType, headers) + st.Unlock() + if err != nil && err != asserts.ErrNotFound { + return BadRequest("cannot find system-user assertion: %s", err) + } + + for _, as := range assertions { + email := as.(*asserts.SystemUser).Email() + // we need to use getUserDetailsFromAssertion as this verifies + // the assertion against the current brand/model/time + username, opts, err := getUserDetailsFromAssertion(st, email) + if err != nil { + logger.Noticef("ignoring system-user assertion for %q: %s", email, err) + continue + } + // ignore already existing users + if _, err := user.Lookup(username); err == nil { + continue + } + + // FIXME: duplicated code + opts.Sudoer = createData.Sudoer + opts.ExtraUsers = !release.OnClassic + + if err := osutilAddUser(username, opts); err != nil { + return InternalError("cannot add user %q: %s", username, err) + } + if err := setupLocalUser(st, username, email); err != nil { + return InternalError("%s", err) + } + createdUsers = append(createdUsers, userResponseData{ + Username: username, + SSHKeys: opts.SSHKeys, + }) + } + + return SyncResponse(createdUsers, nil) +} + +func getUserDetailsFromAssertion(st *state.State, email string) (string, *osutil.AddUserOptions, error) { + errorPrefix := fmt.Sprintf("cannot add system-user %q: ", email) + + st.Lock() + db := assertstate.DB(st) + modelAs, err := devicestate.Model(st) + st.Unlock() + if err != nil { + return "", nil, fmt.Errorf(errorPrefix+"cannot get model assertion: %s", err) + } + + brandID := modelAs.BrandID() + series := modelAs.Series() + model := modelAs.Model() + + a, err := db.Find(asserts.SystemUserType, map[string]string{ + "brand-id": brandID, + "email": email, + }) + if err != nil { + return "", nil, fmt.Errorf(errorPrefix+"%v", err) + } + // the asserts package guarantees that this cast will work + su := a.(*asserts.SystemUser) + + // cross check that the assertion is valid for the given series/model + contains := func(needle string, haystack []string) bool { + for _, s := range haystack { + if needle == s { + return true + } + } + return false + } + // check that the signer of the assertion is one of the accepted ones + sysUserAuths := modelAs.SystemUserAuthority() + if len(sysUserAuths) > 0 && !contains(su.AuthorityID(), sysUserAuths) { + return "", nil, fmt.Errorf(errorPrefix+"%q not in accepted authorities %q", email, su.AuthorityID(), sysUserAuths) + } + if len(su.Series()) > 0 && !contains(series, su.Series()) { + return "", nil, fmt.Errorf(errorPrefix+"%q not in series %q", email, series, su.Series()) + } + if len(su.Models()) > 0 && !contains(model, su.Models()) { + return "", nil, fmt.Errorf(errorPrefix+"%q not in models %q", model, su.Models()) + } + if !su.ValidAt(time.Now()) { + return "", nil, fmt.Errorf(errorPrefix + "assertion not valid anymore") + } + + gecos := fmt.Sprintf("%s,%s", email, su.Name()) + opts := &osutil.AddUserOptions{ + SSHKeys: su.SSHKeys(), + Gecos: gecos, + Password: su.Password(), + } + return su.Username(), opts, nil +} + +type postUserCreateData struct { + Email string `json:"email"` + Sudoer bool `json:"sudoer"` + Known bool `json:"known"` + ForceManaged bool `json:"force-managed"` +} + +var userLookup = user.Lookup + +func setupLocalUser(st *state.State, username, email string) error { + user, err := userLookup(username) + if err != nil { + return fmt.Errorf("cannot lookup user %q: %s", username, err) + } + uid, err := strconv.Atoi(user.Uid) + if err != nil { + return fmt.Errorf("cannot get uid of user %q: %s", username, err) + } + gid, err := strconv.Atoi(user.Gid) + if err != nil { + return fmt.Errorf("cannot get gid of user %q: %s", username, err) + } + authDataFn := filepath.Join(user.HomeDir, ".snap", "auth.json") + if err := osutil.MkdirAllChown(filepath.Dir(authDataFn), 0700, uid, gid); err != nil { + return err + } + + // setup new user, local-only + st.Lock() + authUser, err := auth.NewUser(st, username, email, "", nil) + st.Unlock() + if err != nil { + return fmt.Errorf("cannot persist authentication details: %v", err) + } + // store macaroon auth in auth.json in the new users home dir + outStr, err := json.Marshal(struct { + Macaroon string `json:"macaroon"` + }{ + Macaroon: authUser.Macaroon, + }) + if err != nil { + return fmt.Errorf("cannot marshal auth data: %s", err) + } + if err := osutil.AtomicWriteFileChown(authDataFn, []byte(outStr), 0600, 0, uid, gid); err != nil { + return fmt.Errorf("cannot write auth file %q: %s", authDataFn, err) + } + + return nil +} + +func postCreateUser(c *Command, r *http.Request, user *auth.UserState) Response { + uid, err := postCreateUserUcrednetGetUID(r.RemoteAddr) + if err != nil { + return BadRequest("cannot get ucrednet uid: %v", err) + } + if uid != 0 { + return BadRequest("cannot use create-user as non-root") + } + + var createData postUserCreateData + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&createData); err != nil { + return BadRequest("cannot decode create-user data from request body: %v", err) + } + + // verify request + st := c.d.overlord.State() + st.Lock() + users, err := auth.Users(st) + st.Unlock() + if err != nil { + return InternalError("cannot get user count: %s", err) + } + + if !createData.ForceManaged { + if len(users) > 0 { + return BadRequest("cannot create user: device already managed") + } + if release.OnClassic { + return BadRequest("cannot create user: device is a classic system") + } + } + + // special case: the user requested the creation of all known + // system-users + if createData.Email == "" && createData.Known { + return createAllKnownSystemUsers(c.d.overlord.State(), &createData) + } + if createData.Email == "" { + return BadRequest("cannot create user: 'email' field is empty") + } + + var username string + var opts *osutil.AddUserOptions + if createData.Known { + username, opts, err = getUserDetailsFromAssertion(st, createData.Email) + } else { + username, opts, err = getUserDetailsFromStore(createData.Email) + } + if err != nil { + return BadRequest("%s", err) + } + + // FIXME: duplicated code + opts.Sudoer = createData.Sudoer + opts.ExtraUsers = !release.OnClassic + + if err := osutilAddUser(username, opts); err != nil { + return BadRequest("cannot create user %s: %s", username, err) + } + + if err := setupLocalUser(c.d.overlord.State(), username, createData.Email); err != nil { + return InternalError("%s", err) + } + + return SyncResponse(&userResponseData{ + Username: username, + SSHKeys: opts.SSHKeys, + }, nil) +} + +func convertBuyError(err error) Response { + switch err { + case nil: + return nil + case store.ErrInvalidCredentials: + return Unauthorized(err.Error()) + case store.ErrUnauthenticated: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindLoginRequired, + }, + Status: http.StatusBadRequest, + }, nil) + case store.ErrTOSNotAccepted: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindTermsNotAccepted, + }, + Status: http.StatusBadRequest, + }, nil) + case store.ErrNoPaymentMethods: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindNoPaymentMethods, + }, + Status: http.StatusBadRequest, + }, nil) + case store.ErrPaymentDeclined: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindPaymentDeclined, + }, + Status: http.StatusBadRequest, + }, nil) + default: + return InternalError("%v", err) + } +} + +func postBuy(c *Command, r *http.Request, user *auth.UserState) Response { + var opts store.BuyOptions + + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&opts) + if err != nil { + return BadRequest("cannot decode buy options from request body: %v", err) + } + + s := getStore(c) + + buyResult, err := s.Buy(&opts, user) + + if resp := convertBuyError(err); resp != nil { + return resp + } + + return SyncResponse(buyResult, nil) +} + +func readyToBuy(c *Command, r *http.Request, user *auth.UserState) Response { + s := getStore(c) + + if resp := convertBuyError(s.ReadyToBuy(user)); resp != nil { + return resp + } + + return SyncResponse(true, nil) +} + +func runSnapctl(c *Command, r *http.Request, user *auth.UserState) Response { + var snapctlOptions client.SnapCtlOptions + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&snapctlOptions); err != nil { + return BadRequest("cannot decode snapctl request: %s", err) + } + + if len(snapctlOptions.Args) == 0 { + return BadRequest("snapctl cannot run without args") + } + + // Right now snapctl is only used for hooks. If at some point it grows + // beyond that, this probably shouldn't go straight to the HookManager. + context, _ := c.d.overlord.HookManager().Context(snapctlOptions.ContextID) + stdout, stderr, err := ctlcmd.Run(context, snapctlOptions.Args) + if err != nil { + if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp { + stdout = []byte(e.Error()) + } else { + return BadRequest("error running snapctl: %s", err) + } + } + + result := map[string]string{ + "stdout": string(stdout), + "stderr": string(stderr), + } + + return SyncResponse(result, nil) +} + +func getUsers(c *Command, r *http.Request, user *auth.UserState) Response { + uid, err := postCreateUserUcrednetGetUID(r.RemoteAddr) + if err != nil { + return BadRequest("cannot get ucrednet uid: %v", err) + } + if uid != 0 { + return BadRequest("cannot use create-user as non-root") + } + + st := c.d.overlord.State() + st.Lock() + users, err := auth.Users(st) + st.Unlock() + if err != nil { + return InternalError("cannot get users: %s", err) + } + + resp := make([]userResponseData, len(users)) + for i, u := range users { + resp[i] = userResponseData{ + Username: u.Username, + Email: u.Email, + ID: u.ID, + } + } + return SyncResponse(resp, nil) +} + +// aliasAction is an action performed on aliases +type aliasAction struct { + Action string `json:"action"` + Snap string `json:"snap"` + Aliases []string `json:"aliases"` +} + +func changeAliases(c *Command, r *http.Request, user *auth.UserState) Response { + var a aliasAction + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&a); err != nil { + return BadRequest("cannot decode request body into an alias action: %v", err) + } + if len(a.Aliases) == 0 { + return BadRequest("at least one alias name is required") + } + + var summary string + var taskset *state.TaskSet + var err error + + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + + switch a.Action { + default: + return BadRequest("unsupported alias action: %q", a.Action) + case "alias": + summary = fmt.Sprintf("Enable aliases %s for snap %q", strutil.Quoted(a.Aliases), a.Snap) + taskset, err = snapstate.Alias(state, a.Snap, a.Aliases) + case "unalias": + summary = fmt.Sprintf("Disable aliases %s for snap %q", strutil.Quoted(a.Aliases), a.Snap) + taskset, err = snapstate.Unalias(state, a.Snap, a.Aliases) + case "reset": + summary = fmt.Sprintf("Reset aliases %s for snap %q", strutil.Quoted(a.Aliases), a.Snap) + taskset, err = snapstate.ResetAliases(state, a.Snap, a.Aliases) + } + if err != nil { + return BadRequest("%v", err) + } + + change := state.NewChange(a.Action, summary) + change.Set("snap-names", []string{a.Snap}) + change.AddAll(taskset) + + state.EnsureBefore(0) + + return AsyncResponse(nil, &Meta{Change: change.ID()}) +} + +type aliasStatus struct { + App string `json:"app,omitempty"` + Status string `json:"status,omitempty"` +} + +// getAliases produces a response with a map snap -> alias -> aliasStatus +func getAliases(c *Command, r *http.Request, user *auth.UserState) Response { + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + + res := make(map[string]map[string]aliasStatus) + + allStates, err := snapstate.All(state) + if err != nil { + return InternalError("cannot list local snaps: %v", err) + } + + allAliases, err := snapstate.Aliases(state) + if err != nil { + return InternalError("cannot list aliases: %v", err) + } + + for snapName, snapst := range allStates { + info, err := snapst.CurrentInfo() + if err != nil { + return InternalError("cannot retrieve info for snap %q: %v", snapName, err) + } + if len(info.Aliases) != 0 { + snapAliases := make(map[string]aliasStatus) + res[snapName] = snapAliases + for alias, aliasApp := range info.Aliases { + snapAliases[alias] = aliasStatus{ + App: filepath.Base(aliasApp.WrapperPath()), + } + } + } + } + + for snapName, aliasStatuses := range allAliases { + snapAliases := res[snapName] + if snapAliases == nil { + snapAliases = make(map[string]aliasStatus) + res[snapName] = snapAliases + } + for alias, status := range aliasStatuses { + entry := snapAliases[alias] + entry.Status = status + snapAliases[alias] = entry + } + } + + return SyncResponse(res, nil) +} diff --git a/daemon/api_mock_test.go b/daemon/api_mock_test.go new file mode 100644 index 00000000..a9484da7 --- /dev/null +++ b/daemon/api_mock_test.go @@ -0,0 +1,127 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package daemon + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" +) + +func (s *apiSuite) mockSnap(c *C, yamlText string) *snap.Info { + if s.d == nil { + panic("call s.daemon(c) in your test first") + } + + snapInfo := snaptest.MockSnap(c, yamlText, "", &snap.SideInfo{Revision: snap.R(1)}) + snap.AddImplicitSlots(snapInfo) + + st := s.d.overlord.State() + + st.Lock() + defer st.Unlock() + + // Put a side info into the state + snapstate.Set(st, snapInfo.Name(), &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + { + RealName: snapInfo.Name(), + Revision: snapInfo.Revision, + SnapID: "ididid", + }, + }, + Current: snapInfo.Revision, + }) + + // Put the snap into the interface repository + repo := s.d.overlord.InterfaceManager().Repository() + err := repo.AddSnap(snapInfo) + c.Assert(err, IsNil) + return snapInfo +} + +func (s *apiSuite) mockIface(c *C, iface interfaces.Interface) { + if s.d == nil { + panic("call s.daemon(c) in your test first") + } + err := s.d.overlord.InterfaceManager().Repository().AddInterface(iface) + c.Assert(err, IsNil) +} + +var simpleYaml = ` +name: simple +version: 1 +` + +var consumerYaml = ` +name: consumer +version: 1 +apps: + app: +plugs: + plug: + interface: test + key: value + label: label +` + +var producerYaml = ` +name: producer +version: 1 +apps: + app: +slots: + slot: + interface: test + key: value + label: label +` + +var differentProducerYaml = ` +name: producer +version: 1 +apps: + app: +slots: + slot: + interface: different + key: value + label: label +` + +var configYaml = ` +name: config-snap +version: 1 +hooks: + configure: +` +var aliasYaml = ` +name: alias-snap +version: 1 +apps: + app: + aliases: [alias1] + app2: + aliases: [alias2] +` diff --git a/daemon/api_test.go b/daemon/api_test.go new file mode 100644 index 00000000..d4942d30 --- /dev/null +++ b/daemon/api_test.go @@ -0,0 +1,4884 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package daemon + +import ( + "bytes" + "crypto" + "encoding/json" + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "os/user" + "path/filepath" + "sort" + "strings" + "time" + + "golang.org/x/crypto/sha3" + "golang.org/x/net/context" + "gopkg.in/check.v1" + "gopkg.in/macaroon.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/configstate" + "github.com/snapcore/snapd/overlord/ifacestate" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/testutil" +) + +type apiBaseSuite struct { + rsnaps []*snap.Info + err error + vars map[string]string + storeSearch store.Search + suggestedCurrency string + d *Daemon + user *auth.UserState + restoreBackends func() + refreshCandidates []*store.RefreshCandidate + buyOptions *store.BuyOptions + buyResult *store.BuyResult + storeSigning *assertstest.StoreStack + restoreRelease func() + trustedRestorer func() +} + +func (s *apiBaseSuite) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { + s.user = user + if len(s.rsnaps) > 0 { + return s.rsnaps[0], s.err + } + return nil, s.err +} + +func (s *apiBaseSuite) Find(search *store.Search, user *auth.UserState) ([]*snap.Info, error) { + s.storeSearch = *search + s.user = user + + return s.rsnaps, s.err +} + +func (s *apiBaseSuite) ListRefresh(snaps []*store.RefreshCandidate, user *auth.UserState) ([]*snap.Info, error) { + s.refreshCandidates = snaps + s.user = user + + return s.rsnaps, s.err +} + +func (s *apiBaseSuite) SuggestedCurrency() string { + return s.suggestedCurrency +} + +func (s *apiBaseSuite) Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState) error { + panic("Download not expected to be called") +} + +func (s *apiBaseSuite) Buy(options *store.BuyOptions, user *auth.UserState) (*store.BuyResult, error) { + s.buyOptions = options + s.user = user + return s.buyResult, s.err +} + +func (s *apiBaseSuite) ReadyToBuy(user *auth.UserState) error { + s.user = user + return s.err +} + +func (s *apiBaseSuite) Assertion(*asserts.AssertionType, []string, *auth.UserState) (asserts.Assertion, error) { + panic("Assertion not expected to be called") +} + +func (s *apiBaseSuite) Sections(*auth.UserState) ([]string, error) { + panic("Sections not expected to be called") +} + +func (s *apiBaseSuite) muxVars(*http.Request) map[string]string { + return s.vars +} + +func (s *apiBaseSuite) SetUpSuite(c *check.C) { + muxVars = s.muxVars + s.restoreRelease = release.MockReleaseInfo(&release.OS{ + ID: "ubuntu", + VersionID: "mocked", + }) +} + +func (s *apiBaseSuite) TearDownSuite(c *check.C) { + muxVars = nil + s.restoreRelease() +} + +var ( + rootPrivKey, _ = assertstest.GenerateKey(1024) + storePrivKey, _ = assertstest.GenerateKey(752) +) + +func (s *apiBaseSuite) SetUpTest(c *check.C) { + dirs.SetRootDir(c.MkDir()) + err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) + c.Assert(err, check.IsNil) + c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), check.IsNil) + + s.rsnaps = nil + s.suggestedCurrency = "" + s.storeSearch = store.Search{} + s.err = nil + s.vars = nil + s.user = nil + s.d = nil + s.refreshCandidates = nil + // Disable real security backends for all API tests + s.restoreBackends = ifacestate.MockSecurityBackends(nil) + + s.buyOptions = nil + s.buyResult = nil + + s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey) + s.trustedRestorer = sysdb.InjectTrusted(s.storeSigning.Trusted) +} + +func (s *apiBaseSuite) TearDownTest(c *check.C) { + s.trustedRestorer() + s.d = nil + s.restoreBackends() + snapstateInstall = snapstate.Install + snapstateCoreInfo = snapstate.CoreInfo + snapstateInstallPath = snapstate.InstallPath + assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations + unsafeReadSnapInfo = unsafeReadSnapInfoImpl + ensureStateSoon = ensureStateSoonImpl + dirs.SetRootDir("") +} + +func (s *apiBaseSuite) daemon(c *check.C) *Daemon { + if s.d != nil { + panic("called daemon() twice") + } + d, err := New() + c.Assert(err, check.IsNil) + d.addRoutes() + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + snapstate.ReplaceStore(st, s) + + s.d = d + return d +} + +func (s *apiBaseSuite) mkInstalled(c *check.C, name, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info { + return s.mkInstalledInState(c, nil, name, developer, version, revision, active, extraYaml) +} + +func (s *apiBaseSuite) mkInstalledInState(c *check.C, daemon *Daemon, name, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info { + snapID := name + "-id" + // Collect arguments into a snap.SideInfo structure + sideInfo := &snap.SideInfo{ + SnapID: snapID, + RealName: name, + Revision: revision, + Channel: "stable", + } + + // Collect other arguments into a yaml string + yamlText := fmt.Sprintf(` +name: %s +version: %s +%s`, name, version, extraYaml) + contents := "" + + // Mock the snap on disk + snapInfo := snaptest.MockSnap(c, yamlText, contents, sideInfo) + + c.Assert(os.MkdirAll(snapInfo.DataDir(), 0755), check.IsNil) + metadir := filepath.Join(snapInfo.MountDir(), "meta") + guidir := filepath.Join(metadir, "gui") + c.Assert(os.MkdirAll(guidir, 0755), check.IsNil) + c.Check(ioutil.WriteFile(filepath.Join(guidir, "icon.svg"), []byte("yadda icon"), 0644), check.IsNil) + + if daemon != nil { + st := daemon.overlord.State() + st.Lock() + defer st.Unlock() + + err := assertstate.Add(st, s.storeSigning.StoreAccountKey("")) + if _, ok := err.(*asserts.RevisionError); !ok { + c.Assert(err, check.IsNil) + } + + devAcct := assertstest.NewAccount(s.storeSigning, developer, map[string]interface{}{ + "account-id": developer + "-id", + }, "") + err = assertstate.Add(st, devAcct) + if _, ok := err.(*asserts.RevisionError); !ok { + c.Assert(err, check.IsNil) + } + + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": snapID, + "snap-name": name, + "publisher-id": devAcct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + err = assertstate.Add(st, snapDecl) + if _, ok := err.(*asserts.RevisionError); !ok { + c.Assert(err, check.IsNil) + } + + h := sha3.Sum384([]byte(fmt.Sprintf("%s%s", name, revision))) + dgst, err := asserts.EncodeDigest(crypto.SHA3_384, h[:]) + c.Assert(err, check.IsNil) + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ + "snap-sha3-384": string(dgst), + "snap-size": "999", + "snap-id": snapID, + "snap-revision": fmt.Sprintf("%s", revision), + "developer-id": devAcct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + err = assertstate.Add(st, snapRev) + c.Assert(err, check.IsNil) + + var snapst snapstate.SnapState + snapstate.Get(st, name, &snapst) + snapst.Active = active + snapst.Sequence = append(snapst.Sequence, &snapInfo.SideInfo) + snapst.Current = snapInfo.SideInfo.Revision + snapst.Channel = "beta" + + snapstate.Set(st, name, &snapst) + } + + return snapInfo +} + +func (s *apiBaseSuite) mkGadget(c *check.C, store string) { + yamlText := fmt.Sprintf(`name: test +version: 1 +type: gadget +gadget: {store: {id: %q}} +`, store) + contents := "" + snaptest.MockSnap(c, yamlText, contents, &snap.SideInfo{Revision: snap.R(1)}) + c.Assert(os.Symlink("1", filepath.Join(dirs.SnapMountDir, "test", "current")), check.IsNil) +} + +type apiSuite struct { + apiBaseSuite +} + +var _ = check.Suite(&apiSuite{}) + +func (s *apiSuite) TestSnapInfoOneIntegration(c *check.C) { + d := s.daemon(c) + s.vars = map[string]string{"name": "foo"} + + // we have v0 [r5] installed + s.mkInstalledInState(c, d, "foo", "bar", "v0", snap.R(5), false, "") + // and v1 [r10] is current + s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "description: description\nsummary: summary") + + req, err := http.NewRequest("GET", "/v2/snaps/foo", nil) + c.Assert(err, check.IsNil) + rsp, ok := getSnapInfo(snapCmd, req, nil).(*resp) + c.Assert(ok, check.Equals, true) + + c.Assert(rsp, check.NotNil) + c.Assert(rsp.Result, check.FitsTypeOf, map[string]interface{}{}) + m := rsp.Result.(map[string]interface{}) + + // installed-size depends on vagaries of the filesystem, just check type + c.Check(m["installed-size"], check.FitsTypeOf, int64(0)) + delete(m, "installed-size") + // ditto install-date + c.Check(m["install-date"], check.FitsTypeOf, time.Time{}) + delete(m, "install-date") + + meta := &Meta{} + expected := &resp{ + Type: ResponseTypeSync, + Status: http.StatusOK, + Result: map[string]interface{}{ + "id": "foo-id", + "name": "foo", + "revision": snap.R(10), + "version": "v1", + "channel": "stable", + "tracking-channel": "beta", + "summary": "summary", + "description": "description", + "developer": "bar", + "status": "active", + "icon": "/v2/icons/foo/icon", + "type": string(snap.TypeApp), + "resource": "/v2/snaps/foo", + "private": false, + "devmode": false, + "jailmode": false, + "confinement": snap.StrictConfinement, + "trymode": false, + "apps": []appJSON{}, + "broken": "", + }, + Meta: meta, + } + + c.Check(rsp.Result, check.DeepEquals, expected.Result) +} + +func (s *apiSuite) TestSnapInfoWithAuth(c *check.C) { + state := snapCmd.d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + req, err := http.NewRequest("GET", "/v2/find/?q=name:gfoo", nil) + c.Assert(err, check.IsNil) + + c.Assert(s.user, check.IsNil) + + _, ok := searchStore(findCmd, req, user).(*resp) + c.Assert(ok, check.Equals, true) + // ensure user was set + c.Assert(s.user, check.DeepEquals, user) +} + +func (s *apiSuite) TestSnapInfoNotFound(c *check.C) { + req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil) + c.Assert(err, check.IsNil) + c.Check(getSnapInfo(snapCmd, req, nil).(*resp).Status, check.Equals, http.StatusNotFound) +} + +func (s *apiSuite) TestSnapInfoNoneFound(c *check.C) { + s.vars = map[string]string{"name": "foo"} + + req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil) + c.Assert(err, check.IsNil) + c.Check(getSnapInfo(snapCmd, req, nil).(*resp).Status, check.Equals, http.StatusNotFound) +} + +func (s *apiSuite) TestSnapInfoIgnoresRemoteErrors(c *check.C) { + s.vars = map[string]string{"name": "foo"} + s.err = errors.New("weird") + + req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil) + c.Assert(err, check.IsNil) + rsp := getSnapInfo(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusNotFound) + c.Check(rsp.Result, check.NotNil) +} + +func (s *apiSuite) TestListIncludesAll(c *check.C) { + // Very basic check to help stop us from not adding all the + // commands to the command list. + // + // It could get fancier, looking deeper into the AST to see + // exactly what's being defined, but it's probably not worth + // it; this gives us most of the benefits of that, with a + // fraction of the work. + // + // NOTE: there's probably a + // better/easier way of doing this (patches welcome) + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "api.go", nil, 0) + if err != nil { + panic(err) + } + + found := 0 + + ast.Inspect(f, func(n ast.Node) bool { + switch v := n.(type) { + case *ast.ValueSpec: + found += len(v.Values) + return false + } + return true + }) + + exceptions := []string{ // keep sorted, for scanning ease + "isEmailish", + "api", + "maxReadBuflen", + "muxVars", + "errNothingToInstall", + "defaultCoreSnapName", + "oldDefaultSnapCoreName", + "errDevJailModeConflict", + "errNoJailMode", + "errClassicDevmodeConflict", + // snapInstruction vars: + "snapInstructionDispTable", + "snapstateInstall", + "snapstateUpdate", + "snapstateInstallPath", + "snapstateTryPath", + "snapstateCoreInfo", + "snapstateUpdateMany", + "snapstateInstallMany", + "snapstateRemoveMany", + "snapstateRefreshCandidates", + "assertstateRefreshSnapDeclarations", + "unsafeReadSnapInfo", + "osutilAddUser", + "setupLocalUser", + "storeUserInfo", + "postCreateUserUcrednetGetUID", + "ensureStateSoon", + } + 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.`)) +} + +func (s *apiSuite) TestRootCmd(c *check.C) { + // check it only does GET + c.Check(rootCmd.PUT, check.IsNil) + c.Check(rootCmd.POST, check.IsNil) + c.Check(rootCmd.DELETE, check.IsNil) + c.Assert(rootCmd.GET, check.NotNil) + + rec := httptest.NewRecorder() + c.Check(rootCmd.Path, check.Equals, "/") + + rootCmd.GET(rootCmd, nil, nil).ServeHTTP(rec, nil) + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json") + + expected := []interface{}{"TBD"} + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Result, check.DeepEquals, expected) +} + +func (s *apiSuite) TestSysInfo(c *check.C) { + // check it only does GET + c.Check(sysInfoCmd.PUT, check.IsNil) + c.Check(sysInfoCmd.POST, check.IsNil) + c.Check(sysInfoCmd.DELETE, check.IsNil) + c.Assert(sysInfoCmd.GET, check.NotNil) + + rec := httptest.NewRecorder() + c.Check(sysInfoCmd.Path, check.Equals, "/v2/system-info") + + s.daemon(c).Version = "42b1" + + restore := release.MockReleaseInfo(&release.OS{ID: "distro-id", VersionID: "1.2"}) + defer restore() + restore = release.MockOnClassic(true) + defer restore() + sysInfoCmd.GET(sysInfoCmd, nil, nil).ServeHTTP(rec, nil) + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json") + + expected := map[string]interface{}{ + "series": "16", + "version": "42b1", + "os-release": map[string]interface{}{ + "id": "distro-id", + "version-id": "1.2", + }, + "on-classic": true, + "managed": false, + } + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.DeepEquals, expected) +} + +func (s *apiSuite) makeMyAppsServer(statusCode int, data string) *httptest.Server { + mockMyAppsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(statusCode) + io.WriteString(w, data) + })) + store.MyAppsMacaroonACLAPI = mockMyAppsServer.URL + "/acl/" + return mockMyAppsServer +} + +func (s *apiSuite) makeSSOServer(statusCode int, data string) *httptest.Server { + mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(statusCode) + io.WriteString(w, data) + })) + store.UbuntuoneDischargeAPI = mockSSOServer.URL + "/tokens/discharge" + return mockSSOServer +} + +func (s *apiSuite) makeStoreMacaroon() (string, error) { + m, err := macaroon.New([]byte("secret"), "some id", "location") + if err != nil { + return "", err + } + err = m.AddFirstPartyCaveat("caveat") + if err != nil { + return "", err + } + err = m.AddThirdPartyCaveat([]byte("shared-secret"), "third-party-caveat", store.UbuntuoneLocation) + if err != nil { + return "", err + } + + return auth.MacaroonSerialize(m) +} + +func (s *apiSuite) makeStoreMacaroonResponse(serializedMacaroon string) (string, error) { + data := map[string]string{ + "macaroon": serializedMacaroon, + } + expectedData, err := json.Marshal(data) + if err != nil { + return "", err + } + + return string(expectedData), nil +} + +func (s *apiSuite) TestLoginUser(c *check.C) { + d := s.daemon(c) + state := d.overlord.State() + + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}` + mockSSOServer := s.makeSSOServer(200, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(loginCmd, req, nil).(*resp) + + state.Lock() + user, err := auth.User(state, 1) + state.Unlock() + c.Check(err, check.IsNil) + + expected := userResponseData{ + ID: 1, + Email: "email@.com", + + Macaroon: user.Macaroon, + Discharges: user.Discharges, + } + + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + c.Check(user.ID, check.Equals, 1) + c.Check(user.Username, check.Equals, "") + c.Check(user.Email, check.Equals, "email@.com") + c.Check(user.Discharges, check.IsNil) + c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon) + c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) + // snapd macaroon was setup too + snapdMacaroon, err := auth.MacaroonDeserialize(user.Macaroon) + c.Check(err, check.IsNil) + c.Check(snapdMacaroon.Id(), check.Equals, "1") + c.Check(snapdMacaroon.Location(), check.Equals, "snapd") +} + +func (s *apiSuite) TestLoginUserWithUsername(c *check.C) { + d := s.daemon(c) + state := d.overlord.State() + + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}` + mockSSOServer := s.makeSSOServer(200, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "username", "email": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(loginCmd, req, nil).(*resp) + + state.Lock() + user, err := auth.User(state, 1) + state.Unlock() + c.Check(err, check.IsNil) + + expected := userResponseData{ + ID: 1, + Username: "username", + Email: "email@.com", + Macaroon: user.Macaroon, + Discharges: user.Discharges, + } + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + c.Check(user.ID, check.Equals, 1) + c.Check(user.Username, check.Equals, "username") + c.Check(user.Email, check.Equals, "email@.com") + c.Check(user.Discharges, check.IsNil) + c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon) + c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) + // snapd macaroon was setup too + snapdMacaroon, err := auth.MacaroonDeserialize(user.Macaroon) + c.Check(err, check.IsNil) + c.Check(snapdMacaroon.Id(), check.Equals, "1") + c.Check(snapdMacaroon.Location(), check.Equals, "snapd") +} + +func (s *apiSuite) TestLoginUserNoEmailWithExistentLocalUser(c *check.C) { + d := s.daemon(c) + state := d.overlord.State() + + // setup local-only user + state.Lock() + localUser, err := auth.NewUser(state, "username", "email@test.com", "", nil) + state.Unlock() + c.Assert(err, check.IsNil) + + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}` + mockSSOServer := s.makeSSOServer(200, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "username", "email": "", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, localUser.Macaroon)) + + rsp := loginUser(loginCmd, req, localUser).(*resp) + + expected := userResponseData{ + ID: 1, + Username: "username", + Email: "email@test.com", + + Macaroon: localUser.Macaroon, + Discharges: localUser.Discharges, + } + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + state.Lock() + user, err := auth.User(state, localUser.ID) + state.Unlock() + c.Check(err, check.IsNil) + c.Check(user.Username, check.Equals, "username") + c.Check(user.Email, check.Equals, localUser.Email) + c.Check(user.Macaroon, check.Equals, localUser.Macaroon) + c.Check(user.Discharges, check.IsNil) + c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon) + c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) +} + +func (s *apiSuite) TestLoginUserWithExistentLocalUser(c *check.C) { + d := s.daemon(c) + state := d.overlord.State() + + // setup local-only user + state.Lock() + localUser, err := auth.NewUser(state, "username", "email@test.com", "", nil) + state.Unlock() + c.Assert(err, check.IsNil) + + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}` + mockSSOServer := s.makeSSOServer(200, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "username", "email": "email@test.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, localUser.Macaroon)) + + rsp := loginUser(loginCmd, req, localUser).(*resp) + + expected := userResponseData{ + ID: 1, + Username: "username", + Email: "email@test.com", + + Macaroon: localUser.Macaroon, + Discharges: localUser.Discharges, + } + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + state.Lock() + user, err := auth.User(state, localUser.ID) + state.Unlock() + c.Check(err, check.IsNil) + c.Check(user.Username, check.Equals, "username") + c.Check(user.Email, check.Equals, localUser.Email) + c.Check(user.Macaroon, check.Equals, localUser.Macaroon) + c.Check(user.Discharges, check.IsNil) + c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon) + c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) +} + +func (s *apiSuite) TestLogoutUser(c *check.C) { + d := s.daemon(c) + state := d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Assert(err, check.IsNil) + + req, err := http.NewRequest("POST", "/v2/logout", nil) + c.Assert(err, check.IsNil) + req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`) + + rsp := logoutUser(logoutCmd, req, user).(*resp) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + + state.Lock() + _, err = auth.User(state, user.ID) + state.Unlock() + c.Check(err, check.ErrorMatches, "invalid user") +} + +func (s *apiSuite) TestLoginUserBadRequest(c *check.C) { + buf := bytes.NewBufferString(`hello`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusBadRequest) + c.Check(rsp.Result, check.NotNil) +} + +func (s *apiSuite) TestLoginUserMyAppsError(c *check.C) { + mockMyAppsServer := s.makeMyAppsServer(200, "{}") + defer mockMyAppsServer.Close() + + buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusUnauthorized) + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "cannot get snap access permission") +} + +func (s *apiSuite) TestLoginUserTwoFactorRequiredError(c *check.C) { + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"code": "TWOFACTOR_REQUIRED"}` + mockSSOServer := s.makeSSOServer(401, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusUnauthorized) + c.Check(rsp.Result.(*errorResult).Kind, check.Equals, errorKindTwoFactorRequired) +} + +func (s *apiSuite) TestLoginUserTwoFactorFailedError(c *check.C) { + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"code": "TWOFACTOR_FAILURE"}` + mockSSOServer := s.makeSSOServer(403, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusUnauthorized) + c.Check(rsp.Result.(*errorResult).Kind, check.Equals, errorKindTwoFactorFailed) +} + +func (s *apiSuite) TestLoginUserInvalidCredentialsError(c *check.C) { + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"code": "INVALID_CREDENTIALS"}` + mockSSOServer := s.makeSSOServer(401, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusUnauthorized) + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "cannot authenticate to snap store") +} + +func (s *apiSuite) TestUserFromRequestNoHeader(c *check.C) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := UserFromRequest(state, req) + state.Unlock() + + c.Check(err, check.Equals, auth.ErrInvalidAuth) + c.Check(user, check.IsNil) +} + +func (s *apiSuite) TestUserFromRequestHeaderNoMacaroons(c *check.C) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Authorization", "Invalid") + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := UserFromRequest(state, req) + state.Unlock() + + c.Check(err, check.ErrorMatches, "authorization header misses Macaroon prefix") + c.Check(user, check.IsNil) +} + +func (s *apiSuite) TestUserFromRequestHeaderIncomplete(c *check.C) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Authorization", `Macaroon root=""`) + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := UserFromRequest(state, req) + state.Unlock() + + c.Check(err, check.ErrorMatches, "invalid authorization header") + c.Check(user, check.IsNil) +} + +func (s *apiSuite) TestUserFromRequestHeaderCorrectMissingUser(c *check.C) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`) + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := UserFromRequest(state, req) + state.Unlock() + + c.Check(err, check.Equals, auth.ErrInvalidAuth) + c.Check(user, check.IsNil) +} + +func (s *apiSuite) TestUserFromRequestHeaderValidUser(c *check.C) { + state := snapCmd.d.overlord.State() + state.Lock() + expectedUser, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, expectedUser.Macaroon)) + + state.Lock() + user, err := UserFromRequest(state, req) + state.Unlock() + + c.Check(err, check.IsNil) + c.Check(user, check.DeepEquals, expectedUser) +} + +func (s *apiSuite) TestSnapsInfoOnePerIntegration(c *check.C) { + d := s.daemon(c) + + req, err := http.NewRequest("GET", "/v2/snaps", nil) + c.Assert(err, check.IsNil) + + type tsnap struct { + name string + dev string + ver string + rev int + } + + tsnaps := []tsnap{ + {"foo", "bar", "v1", 5}, + {"bar", "baz", "v2", 10}, + {"baz", "qux", "v3", 15}, + {"qux", "mip", "v4", 20}, + } + + for _, snp := range tsnaps { + s.mkInstalledInState(c, d, snp.name, snp.dev, snp.ver, snap.R(snp.rev), false, "") + } + + rsp, ok := getSnapsInfo(snapsCmd, req, nil).(*resp) + c.Assert(ok, check.Equals, true) + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, http.StatusOK) + c.Check(rsp.Result, check.NotNil) + + snaps := snapList(rsp.Result) + c.Check(snaps, check.HasLen, len(tsnaps)) + + for _, s := range tsnaps { + var got map[string]interface{} + for _, got = range snaps { + if got["name"].(string) == s.name { + break + } + } + c.Check(got["name"], check.Equals, s.name) + c.Check(got["version"], check.Equals, s.ver) + c.Check(got["revision"], check.Equals, snap.R(s.rev).String()) + c.Check(got["developer"], check.Equals, s.dev) + c.Check(got["confinement"], check.Equals, "strict") + } +} + +func (s *apiSuite) TestSnapsInfoOnlyLocal(c *check.C) { + d := s.daemon(c) + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + }} + s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") + + req, err := http.NewRequest("GET", "/v2/snaps?sources=local", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + c.Assert(rsp.Sources, check.DeepEquals, []string{"local"}) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Assert(snaps[0]["name"], check.Equals, "local") +} + +func (s *apiSuite) TestSnapsInfoAll(c *check.C) { + d := s.daemon(c) + + s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(1), false, "") + s.mkInstalledInState(c, d, "local", "foo", "v2", snap.R(2), false, "") + s.mkInstalledInState(c, d, "local", "foo", "v3", snap.R(3), true, "") + + for _, t := range []struct { + q string + numSnaps int + typ ResponseType + }{ + {"?select=enabled", 1, "sync"}, + {`?select=`, 1, "sync"}, + {"", 1, "sync"}, + {"?select=all", 3, "sync"}, + {"?select=invalid-field", 0, "error"}, + } { + req, err := http.NewRequest("GET", fmt.Sprintf("/v2/snaps%s", t.q), nil) + c.Assert(err, check.IsNil) + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, t.typ) + + if rsp.Type != "error" { + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, t.numSnaps) + c.Assert(snaps[0]["name"], check.Equals, "local") + } + } +} + +func (s *apiSuite) TestFind(c *check.C) { + s.suggestedCurrency = "EUR" + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + }} + + req, err := http.NewRequest("GET", "/v2/find?q=hi", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Assert(snaps[0]["name"], check.Equals, "store") + c.Check(snaps[0]["prices"], check.IsNil) + c.Check(snaps[0]["screenshots"], check.IsNil) + c.Check(snaps[0]["channels"], check.IsNil) + + c.Check(rsp.SuggestedCurrency, check.Equals, "EUR") + + c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "hi"}) + c.Check(s.refreshCandidates, check.HasLen, 0) +} + +func (s *apiSuite) TestFindRefreshes(c *check.C) { + s.daemon(c) + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + }} + s.mockSnap(c, "name: store\nversion: 1.0") + + req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Assert(snaps[0]["name"], check.Equals, "store") + c.Check(s.refreshCandidates, check.HasLen, 1) +} + +func (s *apiSuite) TestFindRefreshSideloaded(c *check.C) { + s.daemon(c) + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + }} + + s.mockSnap(c, "name: store\nversion: 1.0") + + var snapst snapstate.SnapState + st := s.d.overlord.State() + st.Lock() + err := snapstate.Get(st, "store", &snapst) + st.Unlock() + c.Assert(err, check.IsNil) + c.Assert(snapst.Sequence, check.HasLen, 1) + + // clear the snapid + snapst.Sequence[0].SnapID = "" + st.Lock() + snapstate.Set(st, "store", &snapst) + st.Unlock() + + req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Assert(snaps[0]["name"], check.Equals, "store") + c.Check(s.refreshCandidates, check.HasLen, 0) +} + +func (s *apiSuite) TestFindPrivate(c *check.C) { + s.daemon(c) + + s.rsnaps = []*snap.Info{} + + req, err := http.NewRequest("GET", "/v2/find?q=foo&select=private", nil) + c.Assert(err, check.IsNil) + + _ = searchStore(findCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{ + Query: "foo", + Private: true, + }) +} + +func (s *apiSuite) TestFindPrefix(c *check.C) { + s.daemon(c) + + s.rsnaps = []*snap.Info{} + + req, err := http.NewRequest("GET", "/v2/find?name=foo*", nil) + c.Assert(err, check.IsNil) + + _ = searchStore(findCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo", Prefix: true}) +} + +func (s *apiSuite) TestFindSection(c *check.C) { + s.daemon(c) + + s.rsnaps = []*snap.Info{} + + req, err := http.NewRequest("GET", "/v2/find?q=foo§ion=bar", nil) + c.Assert(err, check.IsNil) + + _ = searchStore(findCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{ + Query: "foo", + Section: "bar", + }) +} + +func (s *apiSuite) TestFindOne(c *check.C) { + s.daemon(c) + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + Channels: map[string]*snap.ChannelSnapInfo{ + "stable": { + Revision: snap.R(42), + }, + }, + }} + s.mockSnap(c, "name: store\nversion: 1.0") + + req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{}) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Check(snaps[0]["name"], check.Equals, "store") + m := snaps[0]["channels"].(map[string]interface{})["stable"].(map[string]interface{}) + + c.Check(m["revision"], check.Equals, "42") +} + +func (s *apiSuite) TestFindRefreshNotQ(c *check.C) { + req, err := http.NewRequest("GET", "/v2/find?select=refresh&q=foo", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusBadRequest) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, "cannot use 'q' with 'select=refresh'") +} + +func (s *apiSuite) TestFindPriced(c *check.C) { + s.suggestedCurrency = "GBP" + + s.rsnaps = []*snap.Info{{ + Type: snap.TypeApp, + Version: "v2", + Prices: map[string]float64{ + "GBP": 1.23, + "EUR": 2.34, + }, + MustBuy: true, + SideInfo: snap.SideInfo{ + RealName: "banana", + }, + Publisher: "foo", + }} + + req, err := http.NewRequest("GET", "/v2/find?q=banana&channel=stable", nil) + c.Assert(err, check.IsNil) + rsp, ok := searchStore(findCmd, req, nil).(*resp) + c.Assert(ok, check.Equals, true) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + + snap := snaps[0] + c.Check(snap["name"], check.Equals, "banana") + c.Check(snap["prices"], check.DeepEquals, map[string]interface{}{ + "EUR": 2.34, + "GBP": 1.23, + }) + c.Check(snap["status"], check.Equals, "priced") + + c.Check(rsp.SuggestedCurrency, check.Equals, "GBP") +} + +func (s *apiSuite) TestFindScreenshotted(c *check.C) { + s.rsnaps = []*snap.Info{{ + Type: snap.TypeApp, + Version: "v2", + Screenshots: []snap.ScreenshotInfo{ + { + URL: "http://example.com/screenshot.png", + Width: 800, + Height: 1280, + }, + { + URL: "http://example.com/screenshot2.png", + }, + }, + MustBuy: true, + SideInfo: snap.SideInfo{ + RealName: "test-screenshot", + }, + Publisher: "foo", + }} + + req, err := http.NewRequest("GET", "/v2/find?q=test-screenshot", nil) + c.Assert(err, check.IsNil) + rsp, ok := searchStore(findCmd, req, nil).(*resp) + c.Assert(ok, check.Equals, true) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + + c.Check(snaps[0]["name"], check.Equals, "test-screenshot") + c.Check(snaps[0]["screenshots"], check.DeepEquals, []interface{}{ + map[string]interface{}{ + "url": "http://example.com/screenshot.png", + "width": float64(800), + "height": float64(1280), + }, + map[string]interface{}{ + "url": "http://example.com/screenshot2.png", + }, + }) +} + +func (s *apiSuite) TestSnapsInfoOnlyStore(c *check.C) { + d := s.daemon(c) + + s.suggestedCurrency = "EUR" + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + }} + s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") + + req, err := http.NewRequest("GET", "/v2/snaps?sources=store", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + c.Assert(rsp.Sources, check.DeepEquals, []string{"store"}) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Assert(snaps[0]["name"], check.Equals, "store") + c.Check(snaps[0]["prices"], check.IsNil) + + c.Check(rsp.SuggestedCurrency, check.Equals, "EUR") +} + +func (s *apiSuite) TestSnapsStoreConfinement(c *check.C) { + s.rsnaps = []*snap.Info{ + { + // no explicit confinement in this one + SideInfo: snap.SideInfo{ + RealName: "foo", + }, + }, + { + Confinement: snap.StrictConfinement, + SideInfo: snap.SideInfo{ + RealName: "bar", + }, + }, + { + Confinement: snap.DevModeConfinement, + SideInfo: snap.SideInfo{ + RealName: "baz", + }, + }, + } + + req, err := http.NewRequest("GET", "/v2/find", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 3) + + for i, ss := range [][2]string{ + {"foo", string(snap.StrictConfinement)}, + {"bar", string(snap.StrictConfinement)}, + {"baz", string(snap.DevModeConfinement)}, + } { + name, mode := ss[0], ss[1] + c.Check(snaps[i]["name"], check.Equals, name, check.Commentf(name)) + c.Check(snaps[i]["confinement"], check.Equals, mode, check.Commentf(name)) + } +} + +func (s *apiSuite) TestSnapsInfoStoreWithAuth(c *check.C) { + state := snapCmd.d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + req, err := http.NewRequest("GET", "/v2/snaps?sources=store", nil) + c.Assert(err, check.IsNil) + + c.Assert(s.user, check.IsNil) + + _ = getSnapsInfo(snapsCmd, req, user).(*resp) + + // ensure user was set + c.Assert(s.user, check.DeepEquals, user) +} + +func (s *apiSuite) TestSnapsInfoLocalAndStore(c *check.C) { + d := s.daemon(c) + + s.rsnaps = []*snap.Info{{ + Version: "v42", + SideInfo: snap.SideInfo{ + RealName: "remote", + }, + Publisher: "foo", + }} + s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") + + req, err := http.NewRequest("GET", "/v2/snaps?sources=local,store", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + // presence of 'store' in sources bounces request over to /find + c.Assert(rsp.Sources, check.DeepEquals, []string{"store"}) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Check(snaps[0]["version"], check.Equals, "v42") + + // as does a 'q' + req, err = http.NewRequest("GET", "/v2/snaps?q=what", nil) + c.Assert(err, check.IsNil) + rsp = getSnapsInfo(snapsCmd, req, nil).(*resp) + snaps = snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Check(snaps[0]["version"], check.Equals, "v42") + + // otherwise, local only + req, err = http.NewRequest("GET", "/v2/snaps", nil) + c.Assert(err, check.IsNil) + rsp = getSnapsInfo(snapsCmd, req, nil).(*resp) + snaps = snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Check(snaps[0]["version"], check.Equals, "v1") +} + +func (s *apiSuite) TestSnapsInfoDefaultSources(c *check.C) { + d := s.daemon(c) + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "remote", + }, + Publisher: "foo", + }} + s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") + + req, err := http.NewRequest("GET", "/v2/snaps", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + c.Assert(rsp.Sources, check.DeepEquals, []string{"local"}) + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) +} + +func (s *apiSuite) TestSnapsInfoUnknownSource(c *check.C) { + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "remote", + }, + Publisher: "foo", + }} + s.mkInstalled(c, "local", "foo", "v1", snap.R(10), true, "") + + req, err := http.NewRequest("GET", "/v2/snaps?sources=unknown", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + c.Check(rsp.Sources, check.DeepEquals, []string{"local"}) + + snaps := snapList(rsp.Result) + c.Check(snaps, check.HasLen, 1) +} + +func (s *apiSuite) TestSnapsInfoFilterRemote(c *check.C) { + s.rsnaps = nil + + req, err := http.NewRequest("GET", "/v2/snaps?q=foo&sources=store", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo"}) + + c.Assert(rsp.Result, check.NotNil) +} + +func (s *apiSuite) TestPostSnapBadRequest(c *check.C) { + buf := bytes.NewBufferString(`hello`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusBadRequest) + c.Check(rsp.Result, check.NotNil) +} + +func (s *apiSuite) TestPostSnapBadAction(c *check.C) { + buf := bytes.NewBufferString(`{"action": "potato"}`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusBadRequest) + c.Check(rsp.Result, check.NotNil) +} + +func (s *apiSuite) TestPostSnap(c *check.C) { + d := s.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + soon := 0 + ensureStateSoon = func(st *state.State) { + soon++ + ensureStateSoonImpl(st) + } + + s.vars = map[string]string{"name": "foo"} + + snapInstructionDispTable["install"] = func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) { + return "foooo", nil, nil + } + defer func() { + snapInstructionDispTable["install"] = snapInstall + }() + + buf := bytes.NewBufferString(`{"action": "install"}`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, "foooo") + var names []string + err = chg.Get("snap-names", &names) + c.Assert(err, check.IsNil) + c.Check(names, check.DeepEquals, []string{"foo"}) + st.Unlock() + + c.Check(soon, check.Equals, 1) +} + +func (s *apiSuite) TestPostSnapSetsUser(c *check.C) { + d := s.daemon(c) + ensureStateSoon = func(st *state.State) {} + + snapInstructionDispTable["install"] = func(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + return fmt.Sprintf("", inst.userID), nil, nil + } + defer func() { + snapInstructionDispTable["install"] = snapInstall + }() + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + buf := bytes.NewBufferString(`{"action": "install"}`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`) + + rsp := postSnap(snapCmd, req, user).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, "") + st.Unlock() +} + +func (s *apiSuite) TestPostSnapDispatch(c *check.C) { + inst := &snapInstruction{Snaps: []string{"foo"}} + + type T struct { + s string + impl snapActionFunc + } + + actions := []T{ + {"install", snapInstall}, + {"refresh", snapUpdate}, + {"remove", snapRemove}, + {"revert", snapRevert}, + {"enable", snapEnable}, + {"disable", snapDisable}, + {"xyzzy", nil}, + } + + for _, action := range actions { + inst.Action = action.s + // do you feel dirty yet? + c.Check(fmt.Sprintf("%p", action.impl), check.Equals, fmt.Sprintf("%p", inst.dispatch())) + } +} + +func (s *apiSuite) TestPostSnapEnableDisableRevision(c *check.C) { + for _, action := range []string{"enable", "disable"} { + buf := bytes.NewBufferString(`{"action": "` + action + `", "revision": "42"}`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusBadRequest) + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "takes no revision") + } +} + +var sideLoadBodyWithoutDevMode = "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"dangerous\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap-path\"\r\n" + + "\r\n" + + "a/b/local.snap\r\n" + + "----hello--\r\n" + +func (s *apiSuite) TestSideloadSnapOnNonDevModeDistro(c *check.C) { + // try a multipart/form-data upload + body := sideLoadBodyWithoutDevMode + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + chgSummary := s.sideloadCheck(c, body, head, snapstate.Flags{RemoveSnapPath: true}, false) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) +} + +func (s *apiSuite) TestSideloadSnapOnDevModeDistro(c *check.C) { + // try a multipart/form-data upload + body := sideLoadBodyWithoutDevMode + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + restore := release.MockReleaseInfo(&release.OS{ID: "x-devmode-distro"}) + defer restore() + flags := snapstate.Flags{DevMode: true, RemoveSnapPath: true} + chgSummary := s.sideloadCheck(c, body, head, flags, false) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) +} + +func (s *apiSuite) TestSideloadSnapDevMode(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + // try a multipart/form-data upload + restore := release.MockReleaseInfo(&release.OS{ID: "ubuntu"}) + defer restore() + flags := snapstate.Flags{RemoveSnapPath: true} + flags.DevMode = true + chgSummary := s.sideloadCheck(c, body, head, flags, true) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) +} + +func (s *apiSuite) TestSideloadSnapJailMode(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"jailmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"dangerous\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + // try a multipart/form-data upload + flags := snapstate.Flags{JailMode: true, RemoveSnapPath: true} + chgSummary := s.sideloadCheck(c, body, head, flags, true) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) +} + +func (s *apiSuite) TestSideloadSnapJailModeAndDevmode(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"jailmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + d := newTestDaemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, "cannot use devmode and jailmode flags together") +} + +func (s *apiSuite) TestSideloadSnapJailModeInDevModeOS(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"jailmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + d := newTestDaemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + + restore := release.MockReleaseInfo(&release.OS{ID: "x-devmode-distro"}) + defer restore() + + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, "this system cannot honour the jailmode flag") +} + +func (s *apiSuite) TestLocalInstallSnapDeriveSideInfo(c *check.C) { + d := newTestDaemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + // add the assertions first + st := d.overlord.State() + assertAdd(st, s.storeSigning.StoreAccountKey("")) + + dev1Acct := assertstest.NewAccount(s.storeSigning, "devel1", nil, "") + assertAdd(st, dev1Acct) + + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "x-id", + "snap-name": "x", + "publisher-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + assertAdd(st, snapDecl) + + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ + "snap-sha3-384": "YK0GWATaZf09g_fvspYPqm_qtaiqf-KjaNj5uMEQCjQpuXWPjqQbeBINL5H_A0Lo", + "snap-size": "5", + "snap-id": "x-id", + "snap-revision": "41", + "developer-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + assertAdd(st, snapRev) + + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x.snap\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + return nil, nil + } + snapstateInstallPath = func(s *state.State, si *snap.SideInfo, path, channel string, flags snapstate.Flags) (*state.TaskSet, error) { + c.Check(flags, check.Equals, snapstate.Flags{RemoveSnapPath: true}) + c.Check(si, check.DeepEquals, &snap.SideInfo{ + RealName: "x", + SnapID: "x-id", + Revision: snap.R(41), + }) + + return state.NewTaskSet(), nil + } + + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) + + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, `Install "x" snap from file "x.snap"`) + var names []string + err = chg.Get("snap-names", &names) + c.Assert(err, check.IsNil) + c.Check(names, check.DeepEquals, []string{"x"}) + var apiData map[string]interface{} + err = chg.Get("api-data", &apiData) + c.Assert(err, check.IsNil) + c.Check(apiData, check.DeepEquals, map[string]interface{}{ + "snap-name": "x", + }) +} + +func (s *apiSuite) TestSideloadSnapNoSignaturesDangerOff(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + d := newTestDaemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + + // this is the prefix used for tempfiles for sideloading + glob := filepath.Join(os.TempDir(), "snapd-sideload-pkg-*") + glbBefore, _ := filepath.Glob(glob) + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, `cannot find signatures with metadata for snap "x"`) + glbAfter, _ := filepath.Glob(glob) + c.Check(len(glbBefore), check.Equals, len(glbAfter)) +} + +func (s *apiSuite) TestSideloadSnapNotValidFormFile(c *check.C) { + newTestDaemon(c) + + // try a multipart/form-data upload with missing "name" + content := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + + buf := bytes.NewBufferString(content) + req, err := http.NewRequest("POST", "/v2/snaps", buf) + c.Assert(err, check.IsNil) + for k, v := range head { + req.Header.Set(k, v) + } + + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Assert(rsp.Result.(*errorResult).Message, check.Matches, `cannot find "snap" file field in provided multipart/form-data payload`) +} + +func (s *apiSuite) TestTrySnap(c *check.C) { + d := newTestDaemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + req, err := http.NewRequest("POST", "/v2/snaps", nil) + c.Assert(err, check.IsNil) + + // mock a try dir + tryDir := c.MkDir() + snapYaml := filepath.Join(tryDir, "meta", "snap.yaml") + err = os.MkdirAll(filepath.Dir(snapYaml), 0755) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(snapYaml, []byte("name: foo\nversion: 1.0\n"), 0644) + c.Assert(err, check.IsNil) + + for _, t := range []struct { + coreInfoErr error + nTasks int + installSnap string + }{ + // core installed + {nil, 1, ""}, + // no-core-installed + {state.ErrNoState, 2, "core"}, + } { + soon := 0 + ensureStateSoon = func(st *state.State) { + soon++ + ensureStateSoonImpl(st) + } + + tryWasCalled := true + snapstateTryPath = func(s *state.State, name, path string, flags snapstate.Flags) (*state.TaskSet, error) { + tryWasCalled = true + t := s.NewTask("fake-install-snap", "Doing a fake try") + return state.NewTaskSet(t), nil + } + + installSnap := "" + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + installSnap = name + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + return nil, t.coreInfoErr + } + + // try the snap (without an installed core) + rsp := trySnap(snapsCmd, req, nil, tryDir, snapstate.Flags{}).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) + c.Assert(tryWasCalled, check.Equals, true) + + st := d.overlord.State() + st.Lock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + + c.Assert(chg.Tasks(), check.HasLen, t.nTasks) + c.Check(installSnap, check.Equals, t.installSnap) + + st.Unlock() + <-chg.Ready() + st.Lock() + + c.Check(chg.Kind(), check.Equals, "try-snap") + c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Try "%s" snap from %s`, "foo", tryDir)) + var names []string + err = chg.Get("snap-names", &names) + c.Assert(err, check.IsNil) + c.Check(names, check.DeepEquals, []string{"foo"}) + var apiData map[string]interface{} + err = chg.Get("api-data", &apiData) + c.Assert(err, check.IsNil) + c.Check(apiData, check.DeepEquals, map[string]interface{}{ + "snap-name": "foo", + }) + + c.Check(soon, check.Equals, 1) + st.Unlock() + } +} + +func (s *apiSuite) TestTrySnapRelative(c *check.C) { + req, err := http.NewRequest("POST", "/v2/snaps", nil) + c.Assert(err, check.IsNil) + + rsp := trySnap(snapsCmd, req, nil, "relative-path", snapstate.Flags{}).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "need an absolute path") +} + +func (s *apiSuite) TestTrySnapNotDir(c *check.C) { + req, err := http.NewRequest("POST", "/v2/snaps", nil) + c.Assert(err, check.IsNil) + + rsp := trySnap(snapsCmd, req, nil, "/does/not/exist", snapstate.Flags{}).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "not a snap directory") +} + +func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedFlags snapstate.Flags, hasCoreSnap bool) string { + d := newTestDaemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + soon := 0 + ensureStateSoon = func(st *state.State) { + soon++ + ensureStateSoonImpl(st) + } + + // setup done + installQueue := []string{} + unsafeReadSnapInfo = func(path string) (*snap.Info, error) { + return &snap.Info{SuggestedName: "local"}, nil + } + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + if hasCoreSnap { + return nil, nil + } + // pretend we do not have a state for ubuntu-core + return nil, state.ErrNoState + } + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + // NOTE: ubuntu-core is not installed in developer mode + c.Check(flags, check.Equals, snapstate.Flags{}) + installQueue = append(installQueue, name) + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + snapstateInstallPath = func(s *state.State, si *snap.SideInfo, path, channel string, flags snapstate.Flags) (*state.TaskSet, error) { + c.Check(flags, check.DeepEquals, expectedFlags) + + bs, err := ioutil.ReadFile(path) + c.Check(err, check.IsNil) + c.Check(string(bs), check.Equals, "xyzzy") + + installQueue = append(installQueue, si.RealName+"::"+path) + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + buf := bytes.NewBufferString(content) + req, err := http.NewRequest("POST", "/v2/snaps", buf) + c.Assert(err, check.IsNil) + for k, v := range head { + req.Header.Set(k, v) + } + + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) + n := 1 + if !hasCoreSnap { + n++ + } + c.Assert(installQueue, check.HasLen, n) + if !hasCoreSnap { + c.Check(installQueue[0], check.Equals, defaultCoreSnapName) + } + c.Check(installQueue[n-1], check.Matches, "local::.*/snapd-sideload-pkg-.*") + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + + c.Check(soon, check.Equals, 1) + + c.Assert(chg.Tasks(), check.HasLen, n) + + st.Unlock() + <-chg.Ready() + st.Lock() + + c.Check(chg.Kind(), check.Equals, "install-snap") + var names []string + err = chg.Get("snap-names", &names) + c.Assert(err, check.IsNil) + c.Check(names, check.DeepEquals, []string{"local"}) + var apiData map[string]interface{} + err = chg.Get("api-data", &apiData) + c.Assert(err, check.IsNil) + c.Check(apiData, check.DeepEquals, map[string]interface{}{ + "snap-name": "local", + }) + + return chg.Summary() +} + +func (s *apiSuite) runGetConf(c *check.C, keys []string) map[string]interface{} { + s.vars = map[string]string{"name": "test-snap"} + req, err := http.NewRequest("GET", "/v2/snaps/test-snap/conf?keys="+strings.Join(keys, ","), nil) + c.Check(err, check.IsNil) + rec := httptest.NewRecorder() + snapConfCmd.GET(snapConfCmd, req, nil).ServeHTTP(rec, req) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + return body["result"].(map[string]interface{}) +} + +func (s *apiSuite) TestGetConfSingleKey(c *check.C) { + d := s.daemon(c) + + // Set a config that we'll get in a moment + d.overlord.State().Lock() + transaction := configstate.NewTransaction(d.overlord.State()) + transaction.Set("test-snap", "test-key1", "test-value1") + transaction.Set("test-snap", "test-key2", "test-value2") + transaction.Commit() + d.overlord.State().Unlock() + + result := s.runGetConf(c, []string{"test-key1"}) + c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1"}) + + result = s.runGetConf(c, []string{"test-key1", "test-key2"}) + c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1", "test-key2": "test-value2"}) +} + +func (s *apiSuite) TestSetConf(c *check.C) { + d := s.daemon(c) + s.mockSnap(c, configYaml) + + // Mock the hook runner + hookRunner := testutil.MockCommand(c, "snap", "") + defer hookRunner.Restore() + + d.overlord.Loop() + defer d.overlord.Stop() + + text, err := json.Marshal(map[string]interface{}{"key": "value"}) + c.Assert(err, check.IsNil) + + buffer := bytes.NewBuffer(text) + req, err := http.NewRequest("PUT", "/v2/snaps/config-snap/conf", buffer) + c.Assert(err, check.IsNil) + + s.vars = map[string]string{"name": "config-snap"} + + rec := httptest.NewRecorder() + snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.IsNil) + + // Check that the configure hook was run correctly + c.Check(hookRunner.Calls(), check.DeepEquals, [][]string{{ + "snap", "run", "--hook", "configure", "-r", "unset", "config-snap", + }}) +} + +func (s *apiSuite) TestAppIconGet(c *check.C) { + d := s.daemon(c) + + // have an active foo in the system + info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "") + + // have an icon for it in the package itself + iconfile := filepath.Join(info.MountDir(), "meta", "gui", "icon.ick") + c.Assert(os.MkdirAll(filepath.Dir(iconfile), 0755), check.IsNil) + c.Check(ioutil.WriteFile(iconfile, []byte("ick"), 0644), check.IsNil) + + s.vars = map[string]string{"name": "foo"} + req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + + appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.Body.String(), check.Equals, "ick") +} + +func (s *apiSuite) TestAppIconGetInactive(c *check.C) { + d := s.daemon(c) + + // have an *in*active foo in the system + info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), false, "") + + // have an icon for it in the package itself + iconfile := filepath.Join(info.MountDir(), "meta", "gui", "icon.ick") + c.Assert(os.MkdirAll(filepath.Dir(iconfile), 0755), check.IsNil) + c.Check(ioutil.WriteFile(iconfile, []byte("ick"), 0644), check.IsNil) + + s.vars = map[string]string{"name": "foo"} + req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + + appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.Body.String(), check.Equals, "ick") +} + +func (s *apiSuite) TestAppIconGetNoIcon(c *check.C) { + d := s.daemon(c) + + // have an *in*active foo in the system + info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "") + + // NO ICON! + err := os.RemoveAll(filepath.Join(info.MountDir(), "meta", "gui", "icon.svg")) + c.Assert(err, check.IsNil) + + s.vars = map[string]string{"name": "foo"} + req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + + appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code/100, check.Equals, 4) +} + +func (s *apiSuite) TestAppIconGetNoApp(c *check.C) { + s.daemon(c) + + s.vars = map[string]string{"name": "foo"} + req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + + appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 404) +} + +func (s *apiSuite) TestNotInstalledSnapIcon(c *check.C) { + info := &snap.Info{SuggestedName: "notInstalledSnap", IconURL: "icon.svg"} + iconfile := snapIcon(info) + c.Check(iconfile, testutil.Contains, "icon.svg") +} + +func (s *apiSuite) TestInstallOnNonDevModeDistro(c *check.C) { + s.testInstall(c, &release.OS{ID: "ubuntu"}, snapstate.Flags{}, snap.R(0)) +} +func (s *apiSuite) TestInstallOnDevModeDistro(c *check.C) { + flags := snapstate.Flags{} + flags.DevMode = true + s.testInstall(c, &release.OS{ID: "x-devmode-distro"}, flags, snap.R(0)) +} +func (s *apiSuite) TestInstallRevision(c *check.C) { + s.testInstall(c, &release.OS{ID: "ubuntu"}, snapstate.Flags{}, snap.R(42)) +} + +func (s *apiSuite) testInstall(c *check.C, releaseInfo *release.OS, flags snapstate.Flags, revision snap.Revision) { + calledFlags := snapstate.Flags{} + installQueue := []string{} + restore := release.MockReleaseInfo(releaseInfo) + defer restore() + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have core + return nil, nil + } + snapstateInstall = func(s *state.State, name, channel string, revno snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + installQueue = append(installQueue, name) + c.Check(revision, check.Equals, revno) + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + d := s.daemon(c) + + d.overlord.Loop() + defer d.overlord.Stop() + + var buf bytes.Buffer + if revision.Unset() { + buf.WriteString(`{"action": "install"}`) + } else { + fmt.Fprintf(&buf, `{"action": "install", "revision": %s}`, revision.String()) + } + req, err := http.NewRequest("POST", "/v2/snaps/some-snap", &buf) + c.Assert(err, check.IsNil) + + s.vars = map[string]string{"name": "some-snap"} + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + + c.Check(chg.Tasks(), check.HasLen, 1) + + st.Unlock() + <-chg.Ready() + st.Lock() + + c.Check(chg.Status(), check.Equals, state.DoneStatus) + c.Check(calledFlags, check.Equals, flags) + c.Check(err, check.IsNil) + c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) + c.Check(chg.Kind(), check.Equals, "install-snap") + c.Check(chg.Summary(), check.Equals, `Install "some-snap" snap`) +} + +func (s *apiSuite) TestRefresh(c *check.C) { + var calledFlags snapstate.Flags + calledUserID := 0 + installQueue := []string{} + assertstateCalledUserID := 0 + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have core + return nil, nil + } + snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + calledUserID = userID + installQueue = append(installQueue, name) + + t := s.NewTask("fake-refresh-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + assertstateCalledUserID = userID + return nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "refresh", + Snaps: []string{"some-snap"}, + userID: 17, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + summary, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(assertstateCalledUserID, check.Equals, 17) + c.Check(calledFlags, check.DeepEquals, snapstate.Flags{}) + c.Check(calledUserID, check.Equals, 17) + c.Check(err, check.IsNil) + c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) + c.Check(summary, check.Equals, `Refresh "some-snap" snap`) +} + +func (s *apiSuite) TestRefreshDevMode(c *check.C) { + var calledFlags snapstate.Flags + calledUserID := 0 + installQueue := []string{} + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have core + return nil, nil + } + snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + calledUserID = userID + installQueue = append(installQueue, name) + + t := s.NewTask("fake-refresh-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + return nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "refresh", + DevMode: true, + Snaps: []string{"some-snap"}, + userID: 17, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + summary, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + flags := snapstate.Flags{} + flags.DevMode = true + c.Check(calledFlags, check.DeepEquals, flags) + c.Check(calledUserID, check.Equals, 17) + c.Check(err, check.IsNil) + c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) + c.Check(summary, check.Equals, `Refresh "some-snap" snap`) +} + +func (s *apiSuite) TestRefreshClassic(c *check.C) { + var calledFlags snapstate.Flags + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have ubuntu-core + return nil, nil + } + snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + return nil, nil + } + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + return nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "refresh", + Classic: true, + Snaps: []string{"some-snap"}, + userID: 17, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(calledFlags, check.DeepEquals, snapstate.Flags{Classic: true}) +} + +func (s *apiSuite) TestRefreshIgnoreValidation(c *check.C) { + var calledFlags snapstate.Flags + calledUserID := 0 + installQueue := []string{} + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have ubuntu-core + return nil, nil + } + snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + calledUserID = userID + installQueue = append(installQueue, name) + + t := s.NewTask("fake-refresh-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + return nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "refresh", + IgnoreValidation: true, + Snaps: []string{"some-snap"}, + userID: 17, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + summary, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + flags := snapstate.Flags{} + flags.IgnoreValidation = true + + c.Check(calledFlags, check.DeepEquals, flags) + c.Check(calledUserID, check.Equals, 17) + c.Check(err, check.IsNil) + c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) + c.Check(summary, check.Equals, `Refresh "some-snap" snap`) +} + +func (s *apiSuite) TestPostSnapsOp(c *check.C) { + snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 0) + t := s.NewTask("fake-refresh-all", "Refreshing everything") + return []string{"fake1", "fake2"}, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + d := s.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + buf := bytes.NewBufferString(`{"action": "refresh"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "application/json") + + rsp, ok := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(ok, check.Equals, true) + c.Check(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Check(chg.Summary(), check.Equals, `Refresh snaps "fake1", "fake2"`) + var apiData map[string]interface{} + c.Check(chg.Get("api-data", &apiData), check.IsNil) + c.Check(apiData["snap-names"], check.DeepEquals, []interface{}{"fake1", "fake2"}) +} + +func (s *apiSuite) TestRefreshAll(c *check.C) { + refreshSnapDecls := false + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + refreshSnapDecls = true + return assertstate.RefreshSnapDeclarations(s, userID) + } + d := s.daemon(c) + + for _, tst := range []struct { + snaps []string + msg string + }{ + {nil, "Refresh all snaps: no updates"}, + {[]string{"fake"}, `Refresh snap "fake"`}, + {[]string{"fake1", "fake2"}, `Refresh snaps "fake1", "fake2"`}, + } { + refreshSnapDecls = false + + snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 0) + t := s.NewTask("fake-refresh-all", "Refreshing everything") + return tst.snaps, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + inst := &snapInstruction{Action: "refresh"} + st := d.overlord.State() + st.Lock() + summary, _, _, err := snapUpdateMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, tst.msg) + c.Check(refreshSnapDecls, check.Equals, true) + } +} + +func (s *apiSuite) TestRefreshAllNoChanges(c *check.C) { + refreshSnapDecls := false + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + refreshSnapDecls = true + return assertstate.RefreshSnapDeclarations(s, userID) + } + + snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 0) + return nil, nil, nil + } + + d := s.daemon(c) + inst := &snapInstruction{Action: "refresh"} + st := d.overlord.State() + st.Lock() + summary, _, _, err := snapUpdateMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, `Refresh all snaps: no updates`) + c.Check(refreshSnapDecls, check.Equals, true) +} + +func (s *apiSuite) TestRefreshMany(c *check.C) { + refreshSnapDecls := false + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + refreshSnapDecls = true + return nil + } + + snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 2) + t := s.NewTask("fake-refresh-2", "Refreshing two") + return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + d := s.daemon(c) + inst := &snapInstruction{Action: "refresh", Snaps: []string{"foo", "bar"}} + st := d.overlord.State() + st.Lock() + summary, updates, _, err := snapUpdateMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, `Refresh snaps "foo", "bar"`) + c.Check(updates, check.DeepEquals, inst.Snaps) + c.Check(refreshSnapDecls, check.Equals, true) +} + +func (s *apiSuite) TestRefreshMany1(c *check.C) { + refreshSnapDecls := false + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + refreshSnapDecls = true + return nil + } + + snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 1) + t := s.NewTask("fake-refresh-1", "Refreshing one") + return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + d := s.daemon(c) + inst := &snapInstruction{Action: "refresh", Snaps: []string{"foo"}} + st := d.overlord.State() + st.Lock() + summary, updates, _, err := snapUpdateMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, `Refresh snap "foo"`) + c.Check(updates, check.DeepEquals, inst.Snaps) + c.Check(refreshSnapDecls, check.Equals, true) +} + +func (s *apiSuite) TestInstallMany(c *check.C) { + snapstateInstallMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 2) + t := s.NewTask("fake-install-2", "Install two") + return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + d := s.daemon(c) + inst := &snapInstruction{Action: "install", Snaps: []string{"foo", "bar"}} + st := d.overlord.State() + st.Lock() + summary, installs, _, err := snapInstallMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, `Install snaps "foo", "bar"`) + c.Check(installs, check.DeepEquals, inst.Snaps) +} + +func (s *apiSuite) TestRemoveMany(c *check.C) { + snapstateRemoveMany = func(s *state.State, names []string) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 2) + t := s.NewTask("fake-remove-2", "Remove two") + return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + d := s.daemon(c) + inst := &snapInstruction{Action: "remove", Snaps: []string{"foo", "bar"}} + st := d.overlord.State() + st.Lock() + summary, removes, _, err := snapRemoveMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, `Remove snaps "foo", "bar"`) + c.Check(removes, check.DeepEquals, inst.Snaps) +} + +func (s *apiSuite) TestInstallMissingCoreSnap(c *check.C) { + installQueue := []*state.Task{} + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // pretend we do not have a core + return nil, state.ErrNoState + } + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + t1 := s.NewTask("fake-install-snap", name) + t2 := s.NewTask("fake-install-snap", "second task is just here so that we can check that the wait is correctly added to all tasks") + installQueue = append(installQueue, t1, t2) + return state.NewTaskSet(t1, t2), nil + } + + d := s.daemon(c) + + d.overlord.Loop() + defer d.overlord.Stop() + + buf := bytes.NewBufferString(`{"action": "install"}`) + req, err := http.NewRequest("POST", "/v2/snaps/some-snap", buf) + c.Assert(err, check.IsNil) + + s.vars = map[string]string{"name": "some-snap"} + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + + c.Check(chg.Tasks(), check.HasLen, 4) + + c.Check(installQueue, check.HasLen, 4) + // the two OS snap install tasks + c.Check(installQueue[0].Summary(), check.Equals, defaultCoreSnapName) + c.Check(installQueue[0].WaitTasks(), check.HasLen, 0) + c.Check(installQueue[1].WaitTasks(), check.HasLen, 0) + // the two "some-snap" install tasks + c.Check(installQueue[2].Summary(), check.Equals, "some-snap") + c.Check(installQueue[2].WaitTasks(), check.HasLen, 2) + c.Check(installQueue[3].WaitTasks(), check.HasLen, 2) +} + +// Installing ubuntu-core when not having ubuntu-core doesn't misbehave and try +// to install ubuntu-core twice. +func (s *apiSuite) TestInstallCoreSnapWhenMissing(c *check.C) { + installQueue := []*state.Task{} + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // pretend we do not have a core + return nil, state.ErrNoState + } + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + t1 := s.NewTask("fake-install-snap", name) + t2 := s.NewTask("fake-install-snap", "second task is just here so that we can check that the wait is correctly added to all tasks") + installQueue = append(installQueue, t1, t2) + return state.NewTaskSet(t1, t2), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + Snaps: []string{defaultCoreSnapName}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(installQueue, check.HasLen, 2) + // the only OS snap install tasks + c.Check(installQueue[0].Summary(), check.Equals, defaultCoreSnapName) + c.Check(installQueue[0].WaitTasks(), check.HasLen, 0) + c.Check(installQueue[1].WaitTasks(), check.HasLen, 0) +} + +func (s *apiSuite) TestInstallFails(c *check.C) { + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have core + return nil, nil + } + + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + t := s.NewTask("fake-install-snap-error", "Install task") + return state.NewTaskSet(t), nil + } + + d := s.daemon(c) + + d.overlord.Loop() + defer d.overlord.Stop() + + buf := bytes.NewBufferString(`{"action": "install"}`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + + c.Check(chg.Tasks(), check.HasLen, 1) + + st.Unlock() + <-chg.Ready() + st.Lock() + + c.Check(chg.Err(), check.ErrorMatches, `(?sm).*Install task \(fake-install-snap-error errored\)`) +} + +func (s *apiSuite) TestInstallLeaveOld(c *check.C) { + c.Skip("temporarily dropped half-baked support while sorting out flag mess") + var calledFlags snapstate.Flags + + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + LeaveOld: true, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Assert(err, check.IsNil) + + c.Check(calledFlags, check.DeepEquals, snapstate.Flags{}) + c.Check(err, check.IsNil) +} + +func (s *apiSuite) TestInstallDevMode(c *check.C) { + var calledFlags snapstate.Flags + + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + // Install the snap in developer mode + DevMode: true, + Snaps: []string{"fake"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(calledFlags.DevMode, check.Equals, true) +} + +func (s *apiSuite) TestInstallJailMode(c *check.C) { + var calledFlags snapstate.Flags + + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + JailMode: true, + Snaps: []string{"fake"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(calledFlags.JailMode, check.Equals, true) +} + +func (s *apiSuite) TestInstallJailModeDevModeOS(c *check.C) { + restore := release.MockReleaseInfo(&release.OS{ID: "x-devmode-distro"}) + defer restore() + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + JailMode: true, + Snaps: []string{"foo"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.ErrorMatches, "this system cannot honour the jailmode flag") +} + +func (s *apiSuite) TestInstallJailModeDevMode(c *check.C) { + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + DevMode: true, + JailMode: true, + Snaps: []string{"foo"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.ErrorMatches, "cannot use devmode and jailmode flags together") +} + +func snapList(rawSnaps interface{}) []map[string]interface{} { + snaps := make([]map[string]interface{}, len(rawSnaps.([]*json.RawMessage))) + for i, raw := range rawSnaps.([]*json.RawMessage) { + err := json.Unmarshal([]byte(*raw), &snaps[i]) + if err != nil { + panic(err) + } + } + return snaps +} + +// Tests for GET /v2/interfaces + +func (s *apiSuite) TestInterfaces(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + repo := d.overlord.InterfaceManager().Repository() + connRef := interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + c.Assert(repo.Connect(connRef), check.IsNil) + + req, err := http.NewRequest("GET", "/v2/interfaces", nil) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.GET(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 200) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +// Test for POST /v2/interfaces + +func (s *apiSuite) TestConnectPlugSuccess(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &interfaceAction{ + Action: "connect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.IsNil) + + repo := d.overlord.InterfaceManager().Repository() + plug := repo.Plug("consumer", "plug") + slot := repo.Slot("producer", "slot") + c.Assert(plug.Connections, check.HasLen, 1) + c.Assert(slot.Connections, check.HasLen, 1) + c.Check(plug.Connections[0], check.DeepEquals, interfaces.SlotRef{Snap: "producer", Name: "slot"}) + c.Check(slot.Connections[0], check.DeepEquals, interfaces.PlugRef{Snap: "consumer", Name: "plug"}) +} + +func (s *apiSuite) TestConnectPlugFailureInterfaceMismatch(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "different"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, differentProducerYaml) + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &interfaceAction{ + Action: "connect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "cannot connect consumer:plug (\"test\" interface) to producer:slot (\"different\" interface)", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) + repo := d.overlord.InterfaceManager().Repository() + plug := repo.Plug("consumer", "plug") + slot := repo.Slot("producer", "slot") + c.Assert(plug.Connections, check.HasLen, 0) + c.Assert(slot.Connections, check.HasLen, 0) +} + +func (s *apiSuite) TestConnectPlugFailureNoSuchPlug(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + // there is no consumer, no plug defined + s.mockSnap(c, producerYaml) + s.mockSnap(c, consumerYaml) + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &interfaceAction{ + Action: "connect", + Plugs: []plugJSON{{Snap: "consumer", Name: "missingplug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "snap \"consumer\" has no plug named \"missingplug\"", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) + + repo := d.overlord.InterfaceManager().Repository() + slot := repo.Slot("producer", "slot") + c.Assert(slot.Connections, check.HasLen, 0) +} + +func (s *apiSuite) TestConnectPlugFailureNoSuchSlot(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + // there is no producer, no slot defined + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &interfaceAction{ + Action: "connect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "missingslot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "snap \"producer\" has no slot named \"missingslot\"", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) + + repo := d.overlord.InterfaceManager().Repository() + plug := repo.Plug("consumer", "plug") + c.Assert(plug.Connections, check.HasLen, 0) +} + +func (s *apiSuite) TestDisconnectPlugSuccess(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + repo := d.overlord.InterfaceManager().Repository() + connRef := interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + c.Assert(repo.Connect(connRef), check.IsNil) + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &interfaceAction{ + Action: "disconnect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.IsNil) + + plug := repo.Plug("consumer", "plug") + slot := repo.Slot("producer", "slot") + c.Assert(plug.Connections, check.HasLen, 0) + c.Assert(slot.Connections, check.HasLen, 0) +} + +func (s *apiSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + // there is no consumer, no plug defined + s.mockSnap(c, producerYaml) + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &interfaceAction{ + Action: "disconnect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `cannot perform the following tasks: +- Disconnect consumer:plug from producer:slot (snap "consumer" has no plug named "plug")`) + + repo := d.overlord.InterfaceManager().Repository() + slot := repo.Slot("producer", "slot") + c.Assert(slot.Connections, check.HasLen, 0) +} + +func (s *apiSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + // there is no producer, no slot defined + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &interfaceAction{ + Action: "disconnect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `cannot perform the following tasks: +- Disconnect consumer:plug from producer:slot (snap "producer" has no slot named "slot")`) + + repo := d.overlord.InterfaceManager().Repository() + plug := repo.Plug("consumer", "plug") + c.Assert(plug.Connections, check.HasLen, 0) +} + +func (s *apiSuite) TestDisconnectPlugFailureNotConnected(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &interfaceAction{ + Action: "disconnect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `cannot perform the following tasks: +- Disconnect consumer:plug from producer:slot (cannot disconnect consumer:plug from producer:slot, it is not connected)`) + + repo := d.overlord.InterfaceManager().Repository() + plug := repo.Plug("consumer", "plug") + slot := repo.Slot("producer", "slot") + c.Assert(plug.Connections, check.HasLen, 0) + c.Assert(slot.Connections, check.HasLen, 0) +} + +func (s *apiSuite) TestUnsupportedInterfaceRequest(c *check.C) { + buf := bytes.NewBuffer([]byte(`garbage`)) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "cannot decode request body into an interface action: invalid character 'g' looking for beginning of value", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func (s *apiSuite) TestMissingInterfaceAction(c *check.C) { + action := &interfaceAction{} + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "interface action not specified", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func (s *apiSuite) TestUnsupportedInterfaceAction(c *check.C) { + s.daemon(c) + action := &interfaceAction{Action: "foo"} + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "unsupported interface action: \"foo\"", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func assertAdd(st *state.State, a asserts.Assertion) { + st.Lock() + defer st.Unlock() + err := assertstate.Add(st, a) + if err != nil { + panic(err) + } +} + +func (s *apiSuite) TestAssertOK(c *check.C) { + // Setup + d := s.daemon(c) + st := d.overlord.State() + // add store key + assertAdd(st, s.storeSigning.StoreAccountKey("")) + + acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + buf := bytes.NewBuffer(asserts.Encode(acct)) + // Execute + req, err := http.NewRequest("POST", "/v2/assertions", buf) + c.Assert(err, check.IsNil) + rsp := doAssert(assertsCmd, req, nil).(*resp) + // Verify (external) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, http.StatusOK) + // Verify (internal) + st.Lock() + defer st.Unlock() + _, err = assertstate.DB(st).Find(asserts.AccountType, map[string]string{ + "account-id": acct.AccountID(), + }) + c.Check(err, check.IsNil) +} + +func (s *apiSuite) TestAssertStreamOK(c *check.C) { + // Setup + d := s.daemon(c) + st := d.overlord.State() + + acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + err := enc.Encode(acct) + c.Assert(err, check.IsNil) + err = enc.Encode(s.storeSigning.StoreAccountKey("")) + c.Assert(err, check.IsNil) + + // Execute + req, err := http.NewRequest("POST", "/v2/assertions", buf) + c.Assert(err, check.IsNil) + rsp := doAssert(assertsCmd, req, nil).(*resp) + // Verify (external) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, http.StatusOK) + // Verify (internal) + st.Lock() + defer st.Unlock() + _, err = assertstate.DB(st).Find(asserts.AccountType, map[string]string{ + "account-id": acct.AccountID(), + }) + c.Check(err, check.IsNil) +} + +func (s *apiSuite) TestAssertInvalid(c *check.C) { + // Setup + buf := bytes.NewBufferString("blargh") + req, err := http.NewRequest("POST", "/v2/assertions", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + // Execute + assertsCmd.POST(assertsCmd, req, nil).ServeHTTP(rec, req) + // Verify (external) + c.Check(rec.Code, check.Equals, 400) + c.Check(rec.Body.String(), testutil.Contains, + "cannot decode request body into assertions") +} + +func (s *apiSuite) TestAssertError(c *check.C) { + s.daemon(c) + // Setup + acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + buf := bytes.NewBuffer(asserts.Encode(acct)) + req, err := http.NewRequest("POST", "/v2/assertions", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + // Execute + assertsCmd.POST(assertsCmd, req, nil).ServeHTTP(rec, req) + // Verify (external) + c.Check(rec.Code, check.Equals, 400) + c.Check(rec.Body.String(), testutil.Contains, "assert failed") +} + +func (s *apiSuite) TestAssertsFindManyAll(c *check.C) { + // Setup + d := s.daemon(c) + // add store key + st := d.overlord.State() + assertAdd(st, s.storeSigning.StoreAccountKey("")) + acct := assertstest.NewAccount(s.storeSigning, "developer1", map[string]interface{}{ + "account-id": "developer1-id", + }, "") + assertAdd(st, acct) + + // Execute + req, err := http.NewRequest("POST", "/v2/assertions/account", nil) + c.Assert(err, check.IsNil) + s.vars = map[string]string{"assertType": "account"} + rec := httptest.NewRecorder() + assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req) + // Verify + c.Check(rec.Code, check.Equals, http.StatusOK, check.Commentf("body %q", rec.Body)) + c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/x.ubuntu.assertion; bundle=y") + c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "3") + dec := asserts.NewDecoder(rec.Body) + a1, err := dec.Decode() + c.Assert(err, check.IsNil) + c.Check(a1.Type(), check.Equals, asserts.AccountType) + + a2, err := dec.Decode() + c.Assert(err, check.IsNil) + + a3, err := dec.Decode() + c.Assert(err, check.IsNil) + + _, err = dec.Decode() + c.Assert(err, check.Equals, io.EOF) + + ids := []string{a1.(*asserts.Account).AccountID(), a2.(*asserts.Account).AccountID(), a3.(*asserts.Account).AccountID()} + sort.Strings(ids) + c.Check(ids, check.DeepEquals, []string{"can0nical", "canonical", "developer1-id"}) +} + +func (s *apiSuite) TestAssertsFindManyFilter(c *check.C) { + // Setup + d := s.daemon(c) + // add store key + st := d.overlord.State() + assertAdd(st, s.storeSigning.StoreAccountKey("")) + acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + assertAdd(st, acct) + + // Execute + req, err := http.NewRequest("POST", "/v2/assertions/account?username=developer1", nil) + c.Assert(err, check.IsNil) + s.vars = map[string]string{"assertType": "account"} + rec := httptest.NewRecorder() + assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req) + // Verify + c.Check(rec.Code, check.Equals, http.StatusOK, check.Commentf("body %q", rec.Body)) + c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "1") + dec := asserts.NewDecoder(rec.Body) + a1, err := dec.Decode() + c.Assert(err, check.IsNil) + c.Check(a1.Type(), check.Equals, asserts.AccountType) + c.Check(a1.(*asserts.Account).Username(), check.Equals, "developer1") + c.Check(a1.(*asserts.Account).AccountID(), check.Equals, acct.AccountID()) + _, err = dec.Decode() + c.Check(err, check.Equals, io.EOF) +} + +func (s *apiSuite) TestAssertsFindManyNoResults(c *check.C) { + // Setup + d := s.daemon(c) + // add store key + st := d.overlord.State() + assertAdd(st, s.storeSigning.StoreAccountKey("")) + acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + assertAdd(st, acct) + + // Execute + req, err := http.NewRequest("POST", "/v2/assertions/account?username=xyzzyx", nil) + c.Assert(err, check.IsNil) + s.vars = map[string]string{"assertType": "account"} + rec := httptest.NewRecorder() + assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req) + // Verify + c.Check(rec.Code, check.Equals, http.StatusOK, check.Commentf("body %q", rec.Body)) + c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "0") + dec := asserts.NewDecoder(rec.Body) + _, err = dec.Decode() + c.Check(err, check.Equals, io.EOF) +} + +func (s *apiSuite) TestAssertsInvalidType(c *check.C) { + // Execute + req, err := http.NewRequest("POST", "/v2/assertions/foo", nil) + c.Assert(err, check.IsNil) + s.vars = map[string]string{"assertType": "foo"} + rec := httptest.NewRecorder() + assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req) + // Verify + c.Check(rec.Code, check.Equals, 400) + c.Check(rec.Body.String(), testutil.Contains, "invalid assert type") +} + +func setupChanges(st *state.State) []string { + chg1 := st.NewChange("install", "install...") + chg1.Set("snap-names", []string{"funky-snap-name"}) + t1 := st.NewTask("download", "1...") + t2 := st.NewTask("activate", "2...") + chg1.AddAll(state.NewTaskSet(t1, t2)) + t1.Logf("l11") + t1.Logf("l12") + chg2 := st.NewChange("remove", "remove..") + t3 := st.NewTask("unlink", "1...") + chg2.AddTask(t3) + t3.SetStatus(state.ErrorStatus) + t3.Errorf("rm failed") + + return []string{chg1.ID(), chg2.ID(), t1.ID(), t2.ID(), t3.ID()} +} + +func (s *apiSuite) TestStateChangesDefaultToInProgress(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + setupChanges(st) + st.Unlock() + + // Execute + req, err := http.NewRequest("GET", "/v2/changes", nil) + c.Assert(err, check.IsNil) + rsp := getChanges(stateChangesCmd, req, nil).(*resp) + + // Verify + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, http.StatusOK) + c.Assert(rsp.Result, check.HasLen, 1) + + res, err := rsp.MarshalJSON() + c.Assert(err, check.IsNil) + + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*`) +} + +func (s *apiSuite) TestStateChangesInProgress(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + setupChanges(st) + st.Unlock() + + // Execute + req, err := http.NewRequest("GET", "/v2/changes?select=in-progress", nil) + c.Assert(err, check.IsNil) + rsp := getChanges(stateChangesCmd, req, nil).(*resp) + + // Verify + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, http.StatusOK) + c.Assert(rsp.Result, check.HasLen, 1) + + res, err := rsp.MarshalJSON() + c.Assert(err, check.IsNil) + + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*],"ready":false,"spawn-time":"2016-04-21T01:02:03Z"}.*`) +} + +func (s *apiSuite) TestStateChangesAll(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + setupChanges(st) + st.Unlock() + + // Execute + req, err := http.NewRequest("GET", "/v2/changes?select=all", nil) + c.Assert(err, check.IsNil) + rsp := getChanges(stateChangesCmd, req, nil).(*resp) + + // Verify + c.Check(rsp.Status, check.Equals, http.StatusOK) + c.Assert(rsp.Result, check.HasLen, 2) + + res, err := rsp.MarshalJSON() + c.Assert(err, check.IsNil) + + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*],"ready":false,"spawn-time":"2016-04-21T01:02:03Z"}.*`) + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"id":"\w+","kind":"unlink","summary":"1...","status":"Error","log":\["2016-04-21T01:02:03Z ERROR rm failed"],"progress":{"label":"","done":1,"total":1},"spawn-time":"2016-04-21T01:02:03Z","ready-time":"2016-04-21T01:02:03Z"}.*],"ready":true,"err":"[^"]+".*`) +} + +func (s *apiSuite) TestStateChangesReady(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + setupChanges(st) + st.Unlock() + + // Execute + req, err := http.NewRequest("GET", "/v2/changes?select=ready", nil) + c.Assert(err, check.IsNil) + rsp := getChanges(stateChangesCmd, req, nil).(*resp) + + // Verify + c.Check(rsp.Status, check.Equals, http.StatusOK) + c.Assert(rsp.Result, check.HasLen, 1) + + res, err := rsp.MarshalJSON() + c.Assert(err, check.IsNil) + + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"id":"\w+","kind":"unlink","summary":"1...","status":"Error","log":\["2016-04-21T01:02:03Z ERROR rm failed"],"progress":{"label":"","done":1,"total":1},"spawn-time":"2016-04-21T01:02:03Z","ready-time":"2016-04-21T01:02:03Z"}.*],"ready":true,"err":"[^"]+".*`) +} + +func (s *apiSuite) TestStateChangesForSnapName(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + setupChanges(st) + st.Unlock() + + // Execute + req, err := http.NewRequest("GET", "/v2/changes?for=funky-snap-name&select=all", nil) + c.Assert(err, check.IsNil) + rsp := getChanges(stateChangesCmd, req, nil).(*resp) + + // Verify + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, http.StatusOK) + c.Assert(rsp.Result, check.FitsTypeOf, []*changeInfo(nil)) + + res := rsp.Result.([]*changeInfo) + c.Assert(res, check.HasLen, 1) + c.Check(res[0].Kind, check.Equals, `install`) + + _, err = rsp.MarshalJSON() + c.Assert(err, check.IsNil) +} + +func (s *apiSuite) TestStateChange(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + ids := setupChanges(st) + chg := st.Change(ids[0]) + chg.Set("api-data", map[string]int{"n": 42}) + st.Unlock() + s.vars = map[string]string{"id": ids[0]} + + // Execute + req, err := http.NewRequest("POST", "/v2/change/"+ids[0], nil) + c.Assert(err, check.IsNil) + rsp := getChange(stateChangeCmd, req, nil).(*resp) + rec := httptest.NewRecorder() + rsp.ServeHTTP(rec, req) + + // Verify + c.Check(rec.Code, check.Equals, 200) + c.Check(rsp.Status, check.Equals, http.StatusOK) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.NotNil) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body["result"], check.DeepEquals, map[string]interface{}{ + "id": ids[0], + "kind": "install", + "summary": "install...", + "status": "Do", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "tasks": []interface{}{ + map[string]interface{}{ + "id": ids[2], + "kind": "download", + "summary": "1...", + "status": "Do", + "log": []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"}, + "progress": map[string]interface{}{"label": "", "done": 0., "total": 1.}, + "spawn-time": "2016-04-21T01:02:03Z", + }, + map[string]interface{}{ + "id": ids[3], + "kind": "activate", + "summary": "2...", + "status": "Do", + "progress": map[string]interface{}{"label": "", "done": 0., "total": 1.}, + "spawn-time": "2016-04-21T01:02:03Z", + }, + }, + "data": map[string]interface{}{ + "n": float64(42), + }, + }) +} + +func (s *apiSuite) TestStateChangeAbort(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + soon := 0 + ensureStateSoon = func(st *state.State) { + soon++ + } + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + ids := setupChanges(st) + st.Unlock() + s.vars = map[string]string{"id": ids[0]} + + buf := bytes.NewBufferString(`{"action": "abort"}`) + + // Execute + req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf) + c.Assert(err, check.IsNil) + rsp := abortChange(stateChangeCmd, req, nil).(*resp) + rec := httptest.NewRecorder() + rsp.ServeHTTP(rec, req) + + // Ensure scheduled + c.Check(soon, check.Equals, 1) + + // Verify + c.Check(rec.Code, check.Equals, 200) + c.Check(rsp.Status, check.Equals, http.StatusOK) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.NotNil) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body["result"], check.DeepEquals, map[string]interface{}{ + "id": ids[0], + "kind": "install", + "summary": "install...", + "status": "Hold", + "ready": true, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:03Z", + "tasks": []interface{}{ + map[string]interface{}{ + "id": ids[2], + "kind": "download", + "summary": "1...", + "status": "Hold", + "log": []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"}, + "progress": map[string]interface{}{"label": "", "done": 1., "total": 1.}, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:03Z", + }, + map[string]interface{}{ + "id": ids[3], + "kind": "activate", + "summary": "2...", + "status": "Hold", + "progress": map[string]interface{}{"label": "", "done": 1., "total": 1.}, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:03Z", + }, + }, + }) +} + +func (s *apiSuite) TestStateChangeAbortIsReady(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + ids := setupChanges(st) + st.Change(ids[0]).SetStatus(state.DoneStatus) + st.Unlock() + s.vars = map[string]string{"id": ids[0]} + + buf := bytes.NewBufferString(`{"action": "abort"}`) + + // Execute + req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf) + c.Assert(err, check.IsNil) + rsp := abortChange(stateChangeCmd, req, nil).(*resp) + rec := httptest.NewRecorder() + rsp.ServeHTTP(rec, req) + + // Verify + c.Check(rec.Code, check.Equals, 400) + c.Check(rsp.Status, check.Equals, http.StatusBadRequest) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result, check.NotNil) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body["result"], check.DeepEquals, map[string]interface{}{ + "message": fmt.Sprintf("cannot abort change %s with nothing pending", ids[0]), + }) +} + +const validBuyInput = `{ + "snap-id": "the-snap-id-1234abcd", + "snap-name": "the snap name", + "price": 1.23, + "currency": "EUR" + }` + +var validBuyOptions = &store.BuyOptions{ + SnapID: "the-snap-id-1234abcd", + Price: 1.23, + Currency: "EUR", +} + +var buyTests = []struct { + input string + result *store.BuyResult + err error + expectedStatus int + expectedResult interface{} + expectedResponseType ResponseType + expectedBuyOptions *store.BuyOptions +}{ + { + // Success + input: validBuyInput, + result: &store.BuyResult{ + State: "Complete", + }, + expectedStatus: http.StatusOK, + expectedResult: &store.BuyResult{ + State: "Complete", + }, + expectedResponseType: ResponseTypeSync, + expectedBuyOptions: validBuyOptions, + }, + { + // Fail with internal error + input: `{ + "snap-id": "the-snap-id-1234abcd", + "price": 1.23, + "currency": "EUR" + }`, + err: fmt.Errorf("internal error banana"), + expectedStatus: http.StatusInternalServerError, + expectedResponseType: ResponseTypeError, + expectedResult: &errorResult{ + Message: "internal error banana", + }, + expectedBuyOptions: &store.BuyOptions{ + SnapID: "the-snap-id-1234abcd", + Price: 1.23, + Currency: "EUR", + }, + }, + { + // Fail with unauthenticated error + input: validBuyInput, + err: store.ErrUnauthenticated, + expectedStatus: http.StatusBadRequest, + expectedResponseType: ResponseTypeError, + expectedResult: &errorResult{ + Message: "you need to log in first", + Kind: "login-required", + }, + expectedBuyOptions: validBuyOptions, + }, + { + // Fail with TOS not accepted + input: validBuyInput, + err: store.ErrTOSNotAccepted, + expectedStatus: http.StatusBadRequest, + expectedResponseType: ResponseTypeError, + expectedResult: &errorResult{ + Message: "terms of service not accepted", + Kind: "terms-not-accepted", + }, + expectedBuyOptions: validBuyOptions, + }, + { + // Fail with no payment methods + input: validBuyInput, + err: store.ErrNoPaymentMethods, + expectedStatus: http.StatusBadRequest, + expectedResponseType: ResponseTypeError, + expectedResult: &errorResult{ + Message: "no payment methods", + Kind: "no-payment-methods", + }, + expectedBuyOptions: validBuyOptions, + }, + { + // Fail with payment declined + input: validBuyInput, + err: store.ErrPaymentDeclined, + expectedStatus: http.StatusBadRequest, + expectedResponseType: ResponseTypeError, + expectedResult: &errorResult{ + Message: "payment declined", + Kind: "payment-declined", + }, + expectedBuyOptions: validBuyOptions, + }, +} + +func (s *apiSuite) TestBuySnap(c *check.C) { + for _, test := range buyTests { + s.buyResult = test.result + s.err = test.err + + buf := bytes.NewBufferString(test.input) + req, err := http.NewRequest("POST", "/v2/buy", buf) + c.Assert(err, check.IsNil) + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + rsp := postBuy(buyCmd, req, user).(*resp) + + c.Check(rsp.Status, check.Equals, test.expectedStatus) + c.Check(rsp.Type, check.Equals, test.expectedResponseType) + c.Assert(rsp.Result, check.FitsTypeOf, test.expectedResult) + c.Check(rsp.Result, check.DeepEquals, test.expectedResult) + + c.Check(s.buyOptions, check.DeepEquals, test.expectedBuyOptions) + c.Check(s.user, check.Equals, user) + } +} + +func (s *apiSuite) TestIsTrue(c *check.C) { + form := &multipart.Form{} + c.Check(isTrue(form, "foo"), check.Equals, false) + for _, f := range []string{"", "false", "0", "False", "f", "try"} { + form.Value = map[string][]string{"foo": {f}} + c.Check(isTrue(form, "foo"), check.Equals, false, check.Commentf("expected %q to be false", f)) + } + for _, t := range []string{"true", "1", "True", "t"} { + form.Value = map[string][]string{"foo": {t}} + c.Check(isTrue(form, "foo"), check.Equals, true, check.Commentf("expected %q to be true", t)) + } +} + +var readyToBuyTests = []struct { + input error + status int + respType interface{} + response interface{} +}{ + { + // Success + input: nil, + status: http.StatusOK, + respType: ResponseTypeSync, + response: true, + }, + { + // Not accepted TOS + input: store.ErrTOSNotAccepted, + status: http.StatusBadRequest, + respType: ResponseTypeError, + response: &errorResult{ + Message: "terms of service not accepted", + Kind: errorKindTermsNotAccepted, + }, + }, + { + // No payment methods + input: store.ErrNoPaymentMethods, + status: http.StatusBadRequest, + respType: ResponseTypeError, + response: &errorResult{ + Message: "no payment methods", + Kind: errorKindNoPaymentMethods, + }, + }, +} + +func (s *apiSuite) TestReadyToBuy(c *check.C) { + for _, test := range readyToBuyTests { + s.err = test.input + + req, err := http.NewRequest("GET", "/v2/buy/ready", nil) + c.Assert(err, check.IsNil) + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + rsp := readyToBuy(readyToBuyCmd, req, user).(*resp) + c.Check(rsp.Status, check.Equals, test.status) + c.Check(rsp.Type, check.Equals, test.respType) + c.Assert(rsp.Result, check.FitsTypeOf, test.response) + c.Check(rsp.Result, check.DeepEquals, test.response) + } +} + +var _ = check.Suite(&postCreateUserSuite{}) + +type postCreateUserSuite struct { + apiBaseSuite + + mockUserHome string +} + +func (s *postCreateUserSuite) SetUpTest(c *check.C) { + s.apiBaseSuite.SetUpTest(c) + + s.daemon(c) + postCreateUserUcrednetGetUID = func(string) (uint32, error) { + return 0, nil + } + s.mockUserHome = c.MkDir() + userLookup = mkUserLookup(s.mockUserHome) +} + +func (s *postCreateUserSuite) TearDownTest(c *check.C) { + s.apiBaseSuite.TearDownTest(c) + + postCreateUserUcrednetGetUID = ucrednetGetUID + userLookup = user.Lookup + osutilAddUser = osutil.AddUser + storeUserInfo = store.UserInfo +} + +func mkUserLookup(userHomeDir string) func(string) (*user.User, error) { + return func(username string) (*user.User, error) { + cur, err := user.Current() + cur.Username = username + cur.HomeDir = userHomeDir + return cur, err + } +} + +func (s *postCreateUserSuite) TestPostCreateUserNoSSHKeys(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + storeUserInfo = func(user string) (*store.User, error) { + c.Check(user, check.Equals, "popper@lse.ac.uk") + return &store.User{ + Username: "karl", + OpenIDIdentifier: "xxyyzz", + }, nil + } + + buf := bytes.NewBufferString(`{"email": "popper@lse.ac.uk"}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user for "popper@lse.ac.uk": no ssh keys found`) +} + +func (s *postCreateUserSuite) TestPostCreateUser(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + storeUserInfo = func(user string) (*store.User, error) { + c.Check(user, check.Equals, "popper@lse.ac.uk") + return &store.User{ + Username: "karl", + SSHKeys: []string{"ssh1", "ssh2"}, + OpenIDIdentifier: "xxyyzz", + }, nil + } + osutilAddUser = func(username string, opts *osutil.AddUserOptions) error { + c.Check(username, check.Equals, "karl") + c.Check(opts.SSHKeys, check.DeepEquals, []string{"ssh1", "ssh2"}) + c.Check(opts.Gecos, check.Equals, "popper@lse.ac.uk,xxyyzz") + c.Check(opts.Sudoer, check.Equals, false) + return nil + } + + buf := bytes.NewBufferString(`{"email": "popper@lse.ac.uk"}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + expected := &userResponseData{ + Username: "karl", + SSHKeys: []string{"ssh1", "ssh2"}, + } + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + // user was setup in state + state := s.d.overlord.State() + state.Lock() + user, err := auth.User(state, 1) + state.Unlock() + c.Check(err, check.IsNil) + c.Check(user.Username, check.Equals, "karl") + c.Check(user.Email, check.Equals, "popper@lse.ac.uk") + c.Check(user.Macaroon, check.NotNil) + // auth saved to user home dir + outfile := filepath.Join(s.mockUserHome, ".snap", "auth.json") + c.Check(osutil.FileExists(outfile), check.Equals, true) + content, err := ioutil.ReadFile(outfile) + c.Check(err, check.IsNil) + c.Check(string(content), check.Equals, fmt.Sprintf(`{"macaroon":"%s"}`, user.Macaroon)) +} + +func (s *postCreateUserSuite) TestGetUserDetailsFromAssertionModelNotFound(c *check.C) { + st := s.d.overlord.State() + email := "foo@example.com" + + username, opts, err := getUserDetailsFromAssertion(st, email) + c.Check(username, check.Equals, "") + c.Check(opts, check.IsNil) + c.Check(err, check.ErrorMatches, `cannot add system-user "foo@example.com": cannot get model assertion: no state entry for key`) +} + +func (s *postCreateUserSuite) setupSigner(accountID string, signerPrivKey asserts.PrivateKey) *assertstest.SigningDB { + st := s.d.overlord.State() + + // create fake brand signature + signerSigning := assertstest.NewSigningDB(accountID, signerPrivKey) + + signerAcct := assertstest.NewAccount(s.storeSigning, accountID, map[string]interface{}{ + "account-id": accountID, + "verification": "certified", + }, "") + s.storeSigning.Add(signerAcct) + assertAdd(st, signerAcct) + + signerAccKey := assertstest.NewAccountKey(s.storeSigning, signerAcct, nil, signerPrivKey.PublicKey(), "") + s.storeSigning.Add(signerAccKey) + assertAdd(st, signerAccKey) + + return signerSigning +} + +var ( + brandPrivKey, _ = assertstest.GenerateKey(752) + partnerPrivKey, _ = assertstest.GenerateKey(752) + unknownPrivKey, _ = assertstest.GenerateKey(752) +) + +func (s *postCreateUserSuite) makeSystemUsers(c *check.C, systemUsers []map[string]interface{}) { + st := s.d.overlord.State() + + assertAdd(st, s.storeSigning.StoreAccountKey("")) + + brandSigning := s.setupSigner("my-brand", brandPrivKey) + partnerSigning := s.setupSigner("partner", partnerPrivKey) + unknownSigning := s.setupSigner("unknown", unknownPrivKey) + + signers := map[string]*assertstest.SigningDB{ + "my-brand": brandSigning, + "partner": partnerSigning, + "unknown": unknownSigning, + } + + model, err := brandSigning.Sign(asserts.ModelType, map[string]interface{}{ + "series": "16", + "authority-id": "my-brand", + "brand-id": "my-brand", + "model": "my-model", + "architecture": "amd64", + "gadget": "pc", + "kernel": "pc-kernel", + "required-snaps": []interface{}{"required-snap1"}, + "system-user-authority": []interface{}{"my-brand", "partner"}, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + model = model.(*asserts.Model) + + // now add model related stuff to the system + assertAdd(st, model) + + for _, suMap := range systemUsers { + su, err := signers[suMap["authority-id"].(string)].Sign(asserts.SystemUserType, suMap, nil, "") + c.Assert(err, check.IsNil) + su = su.(*asserts.SystemUser) + // now add system-user assertion to the system + assertAdd(st, su) + } + // create fake device + st.Lock() + err = auth.SetDevice(st, &auth.DeviceState{ + Brand: "my-brand", + Model: "my-model", + }) + st.Unlock() + c.Assert(err, check.IsNil) +} + +var goodUser = map[string]interface{}{ + "authority-id": "my-brand", + "brand-id": "my-brand", + "email": "foo@bar.com", + "series": []interface{}{"16", "18"}, + "models": []interface{}{"my-model", "other-model"}, + "name": "Boring Guy", + "username": "guy", + "password": "$6$salt$hash", + "since": time.Now().Format(time.RFC3339), + "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339), +} + +var partnerUser = map[string]interface{}{ + "authority-id": "partner", + "brand-id": "my-brand", + "email": "p@partner.com", + "series": []interface{}{"16", "18"}, + "models": []interface{}{"my-model"}, + "name": "Partner Guy", + "username": "partnerguy", + "password": "$6$salt$hash", + "since": time.Now().Format(time.RFC3339), + "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339), +} + +var badUser = map[string]interface{}{ + // bad user (not valid for this model) + "authority-id": "my-brand", + "brand-id": "my-brand", + "email": "foobar@bar.com", + "series": []interface{}{"16", "18"}, + "models": []interface{}{"non-of-the-models-i-have"}, + "name": "Random Gal", + "username": "gal", + "password": "$6$salt$hash", + "since": time.Now().Format(time.RFC3339), + "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339), +} + +var unknownUser = map[string]interface{}{ + "authority-id": "unknown", + "brand-id": "my-brand", + "email": "x@partner.com", + "series": []interface{}{"16", "18"}, + "models": []interface{}{"my-model"}, + "name": "XGuy", + "username": "xguy", + "password": "$6$salt$hash", + "since": time.Now().Format(time.RFC3339), + "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339), +} + +func (s *postCreateUserSuite) TestGetUserDetailsFromAssertionHappy(c *check.C) { + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + // ensure that if we query the details from the assert DB we get + // the expected user + st := s.d.overlord.State() + username, opts, err := getUserDetailsFromAssertion(st, "foo@bar.com") + c.Check(username, check.Equals, "guy") + c.Check(opts, check.DeepEquals, &osutil.AddUserOptions{ + Gecos: "foo@bar.com,Boring Guy", + Password: "$6$salt$hash", + }) + c.Check(err, check.IsNil) +} + +// FIXME: These tests all look similar, with small deltas. Would be +// nice to transform them into a table that is just the deltas, and +// run on a loop. +func (s *postCreateUserSuite) TestPostCreateUserFromAssertion(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + // mock the calls that create the user + osutilAddUser = func(username string, opts *osutil.AddUserOptions) error { + c.Check(username, check.Equals, "guy") + c.Check(opts.Gecos, check.Equals, "foo@bar.com,Boring Guy") + c.Check(opts.Sudoer, check.Equals, false) + c.Check(opts.Password, check.Equals, "$6$salt$hash") + return nil + } + + defer func() { + osutilAddUser = osutil.AddUser + }() + + // do it! + buf := bytes.NewBufferString(`{"email": "foo@bar.com","known":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + expected := &userResponseData{ + Username: "guy", + } + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + // ensure the user was added to the state + st := s.d.overlord.State() + st.Lock() + users, err := auth.Users(st) + c.Assert(err, check.IsNil) + st.Unlock() + c.Check(users, check.HasLen, 1) +} + +func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnown(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + s.makeSystemUsers(c, []map[string]interface{}{goodUser, partnerUser, badUser, unknownUser}) + + // mock the calls that create the user + osutilAddUser = func(username string, opts *osutil.AddUserOptions) error { + switch username { + case "guy": + c.Check(opts.Gecos, check.Equals, "foo@bar.com,Boring Guy") + case "partnerguy": + c.Check(opts.Gecos, check.Equals, "p@partner.com,Partner Guy") + default: + c.Logf("unexpected username %q", username) + c.Fail() + } + c.Check(opts.Sudoer, check.Equals, false) + c.Check(opts.Password, check.Equals, "$6$salt$hash") + return nil + } + defer func() { + osutilAddUser = osutil.AddUser + }() + + // do it! + buf := bytes.NewBufferString(`{"known":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + // note that we get a list here instead of a single + // userResponseData item + c.Check(rsp.Result, check.FitsTypeOf, []userResponseData{}) + seen := map[string]bool{} + for _, u := range rsp.Result.([]userResponseData) { + seen[u.Username] = true + c.Check(u, check.DeepEquals, userResponseData{Username: u.Username}) + } + c.Check(seen, check.DeepEquals, map[string]bool{ + "guy": true, + "partnerguy": true, + }) + + // ensure the user was added to the state + st := s.d.overlord.State() + st.Lock() + users, err := auth.Users(st) + c.Assert(err, check.IsNil) + st.Unlock() + c.Check(users, check.HasLen, 2) +} + +func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnownClassicErrors(c *check.C) { + restore := release.MockOnClassic(true) + defer restore() + + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + postCreateUserUcrednetGetUID = func(string) (uint32, error) { + return 0, nil + } + defer func() { + postCreateUserUcrednetGetUID = ucrednetGetUID + }() + + // do it! + buf := bytes.NewBufferString(`{"known":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user: device is a classic system`) +} + +func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnownButOwnedErrors(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + st := s.d.overlord.State() + st.Lock() + _, err := auth.NewUser(st, "username", "email@test.com", "macaroon", []string{"discharge"}) + st.Unlock() + c.Check(err, check.IsNil) + + // do it! + buf := bytes.NewBufferString(`{"known":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user: device already managed`) +} + +func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnownButOwned(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + st := s.d.overlord.State() + st.Lock() + _, err := auth.NewUser(st, "username", "email@test.com", "macaroon", []string{"discharge"}) + st.Unlock() + c.Check(err, check.IsNil) + + // mock the calls that create the user + osutilAddUser = func(username string, opts *osutil.AddUserOptions) error { + c.Check(username, check.Equals, "guy") + c.Check(opts.Gecos, check.Equals, "foo@bar.com,Boring Guy") + c.Check(opts.Sudoer, check.Equals, false) + c.Check(opts.Password, check.Equals, "$6$salt$hash") + return nil + } + defer func() { + osutilAddUser = osutil.AddUser + }() + + // do it! + buf := bytes.NewBufferString(`{"known":true,"force-managed":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + // note that we get a list here instead of a single + // userResponseData item + expected := []userResponseData{ + {Username: "guy"}, + } + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) +} + +func (s *postCreateUserSuite) TestUsersEmpty(c *check.C) { + req, err := http.NewRequest("GET", "/v2/users", nil) + c.Assert(err, check.IsNil) + + rsp := getUsers(usersCmd, req, nil).(*resp) + + expected := []userResponseData{} + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) +} + +func (s *postCreateUserSuite) TestUsersHasUser(c *check.C) { + st := s.d.overlord.State() + st.Lock() + u, err := auth.NewUser(st, "someuser", "mymail@test.com", "macaroon", []string{"discharge"}) + st.Unlock() + c.Assert(err, check.IsNil) + + req, err := http.NewRequest("GET", "/v2/users", nil) + c.Assert(err, check.IsNil) + + rsp := getUsers(usersCmd, req, nil).(*resp) + + expected := []userResponseData{ + {ID: u.ID, Username: u.Username, Email: u.Email}, + } + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) +} + +func (s *postCreateUserSuite) TestSysinfoIsManaged(c *check.C) { + st := s.d.overlord.State() + st.Lock() + _, err := auth.NewUser(st, "someuser", "mymail@test.com", "macaroon", []string{"discharge"}) + st.Unlock() + c.Assert(err, check.IsNil) + + req, err := http.NewRequest("GET", "/v2/system-info", nil) + c.Assert(err, check.IsNil) + + rsp := sysInfo(sysInfoCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result.(map[string]interface{})["managed"], check.Equals, true) +} + +// aliases + +func (s *apiSuite) TestAliasSuccess(c *check.C) { + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, check.IsNil) + d := s.daemon(c) + + s.mockSnap(c, aliasYaml) + + oldAutoAliases := snapstate.AutoAliases + snapstate.AutoAliases = func(*state.State, *snap.Info) ([]string, error) { + return nil, nil + } + defer func() { snapstate.AutoAliases = oldAutoAliases }() + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &aliasAction{ + Action: "alias", + Snap: "alias-snap", + Aliases: []string{"alias1"}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.IsNil) + + // sanity check + c.Check(osutil.IsSymlink(filepath.Join(dirs.SnapBinariesDir, "alias1")), check.Equals, true) +} + +func (s *apiSuite) TestAliasErrors(c *check.C) { + s.daemon(c) + + errScenarios := []struct { + mangle func(*aliasAction) + err string + }{ + {func(a *aliasAction) { a.Action = "" }, `unsupported alias action: ""`}, + {func(a *aliasAction) { a.Action = "what" }, `unsupported alias action: "what"`}, + {func(a *aliasAction) { a.Aliases = nil }, `at least one alias name is required`}, + {func(a *aliasAction) { a.Snap = "lalala" }, `cannot find snap "lalala"`}, + } + + for _, scen := range errScenarios { + action := &aliasAction{ + Action: "alias", + Snap: "alias-snap", + Aliases: []string{"alias1"}, + } + scen.mangle(action) + + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + + rsp := changeAliases(aliasesCmd, req, nil).(*resp) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, http.StatusBadRequest) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, scen.err) + } +} + +func (s *apiSuite) TestUnaliasSuccess(c *check.C) { + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, check.IsNil) + d := s.daemon(c) + + s.mockSnap(c, aliasYaml) + + oldAutoAliases := snapstate.AutoAliases + snapstate.AutoAliases = func(*state.State, *snap.Info) ([]string, error) { + return nil, nil + } + defer func() { snapstate.AutoAliases = oldAutoAliases }() + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &aliasAction{ + Action: "unalias", + Snap: "alias-snap", + Aliases: []string{"alias1"}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + defer st.Unlock() + err = chg.Err() + c.Assert(err, check.IsNil) + + var allAliases map[string]map[string]string + err = st.Get("aliases", &allAliases) + c.Assert(err, check.IsNil) + c.Check(allAliases, check.DeepEquals, map[string]map[string]string{ + "alias-snap": {"alias1": "disabled"}, + }) + +} + +func (s *apiSuite) TestResetAliasSuccess(c *check.C) { + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, check.IsNil) + d := s.daemon(c) + + s.mockSnap(c, aliasYaml) + + oldAutoAliases := snapstate.AutoAliases + snapstate.AutoAliases = func(*state.State, *snap.Info) ([]string, error) { + return nil, nil + } + defer func() { snapstate.AutoAliases = oldAutoAliases }() + + d.overlord.Loop() + defer d.overlord.Stop() + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + + st.Set("aliases", map[string]map[string]string{ + "alias-snap": { + "alias1": "disabled", + }, + }) + + action := &aliasAction{ + Action: "reset", + Snap: "alias-snap", + Aliases: []string{"alias1"}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + st.Unlock() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + st.Lock() + c.Check(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + chg := st.Change(id) + c.Assert(chg, check.NotNil) + + st.Unlock() + <-chg.Ready() + st.Lock() + + err = chg.Err() + c.Assert(err, check.IsNil) + + var allAliases map[string]map[string]string + err = st.Get("aliases", &allAliases) + c.Assert(err, check.IsNil) + c.Check(allAliases, check.HasLen, 0) +} + +func (s *apiSuite) TestAliases(c *check.C) { + d := s.daemon(c) + + s.mockSnap(c, aliasYaml) + + st := d.overlord.State() + st.Lock() + st.Set("aliases", map[string]map[string]string{ + "alias-snap": { + "alias1": "enabled", + "alias3": "disabled", // gone from the current revision of the snap + }, + }) + st.Unlock() + + req, err := http.NewRequest("GET", "/v2/aliases", nil) + c.Assert(err, check.IsNil) + + rsp := getAliases(aliasesCmd, req, nil).(*resp) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, http.StatusOK) + c.Check(rsp.Result, check.DeepEquals, map[string]map[string]aliasStatus{ + "alias-snap": { + "alias1": {App: "alias-snap.app", Status: "enabled"}, + "alias2": {App: "alias-snap.app2"}, + "alias3": {Status: "disabled"}, + }, + }) + +} diff --git a/daemon/daemon.go b/daemon/daemon.go new file mode 100644 index 00000000..a2cf761f --- /dev/null +++ b/daemon/daemon.go @@ -0,0 +1,339 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package daemon + +import ( + "fmt" + "net" + "net/http" + "os" + "os/exec" + "runtime" + "strings" + unix "syscall" + "time" + + "github.com/coreos/go-systemd/activation" + "github.com/gorilla/mux" + "gopkg.in/tomb.v2" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n/dumb" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/store" +) + +// A Daemon listens for requests and routes them to the right command +type Daemon struct { + Version string + overlord *overlord.Overlord + snapdListener net.Listener + snapListener net.Listener + tomb tomb.Tomb + router *mux.Router + // enableInternalInterfaceActions controls if adding and removing slots and plugs is allowed. + enableInternalInterfaceActions bool +} + +// A ResponseFunc handles one of the individual verbs for a method +type ResponseFunc func(*Command, *http.Request, *auth.UserState) Response + +// A Command routes a request to an individual per-verb ResponseFUnc +type Command struct { + Path string + // + GET ResponseFunc + PUT ResponseFunc + POST ResponseFunc + DELETE ResponseFunc + // can guest GET? + GuestOK bool + // can non-admin GET? + UserOK bool + // is this path accessible on the snapd-snap socket? + SnapOK bool + + d *Daemon +} + +func (c *Command) canAccess(r *http.Request, user *auth.UserState) bool { + if user != nil { + // Authenticated users do anything for now. + return true + } + + isUser := false + uid, err := ucrednetGetUID(r.RemoteAddr) + if err == nil { + if uid == 0 { + // Superuser does anything. + return true + } + + isUser = true + } else if err != errNoUID { + logger.Noticef("unexpected error when attempting to get UID: %s", err) + return false + } else if c.SnapOK { + return true + } + + if r.Method != "GET" { + return false + } + + if isUser && c.UserOK { + return true + } + + if c.GuestOK { + return true + } + + return false +} + +func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) { + state := c.d.overlord.State() + state.Lock() + // TODO Look at the error and fail if there's an attempt to authenticate with invalid data. + user, _ := UserFromRequest(state, r) + state.Unlock() + + if !c.canAccess(r, user) { + Unauthorized("access denied").ServeHTTP(w, r) + return + } + + var rspf ResponseFunc + var rsp = BadMethod("method %q not allowed", r.Method) + + switch r.Method { + case "GET": + rspf = c.GET + case "PUT": + rspf = c.PUT + case "POST": + rspf = c.POST + case "DELETE": + rspf = c.DELETE + } + + if rspf != nil { + rsp = rspf(c, r, user) + } + + rsp.ServeHTTP(w, r) +} + +type wrappedWriter struct { + w http.ResponseWriter + s int +} + +func (w *wrappedWriter) Header() http.Header { + return w.w.Header() +} + +func (w *wrappedWriter) Write(bs []byte) (int, error) { + return w.w.Write(bs) +} + +func (w *wrappedWriter) WriteHeader(s int) { + w.w.WriteHeader(s) + w.s = s +} + +func logit(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ww := &wrappedWriter{w: w} + t0 := time.Now() + handler.ServeHTTP(ww, r) + t := time.Now().Sub(t0) + url := r.URL.String() + if !strings.Contains(url, "/changes/") { + logger.Debugf("%s %s %s %s %d", r.RemoteAddr, r.Method, r.URL, t, ww.s) + } + }) +} + +// getListener tries to get a listener for the given socket path from +// the listener map, and if it fails it tries to set it up directly. +func getListener(socketPath string, listenerMap map[string]net.Listener) (net.Listener, error) { + if listener, ok := listenerMap[socketPath]; ok { + return listener, nil + } + + if c, err := net.Dial("unix", socketPath); err == nil { + c.Close() + return nil, fmt.Errorf("socket %q already in use", socketPath) + } + + if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { + return nil, err + } + + address, err := net.ResolveUnixAddr("unix", socketPath) + if err != nil { + return nil, err + } + + runtime.LockOSThread() + oldmask := unix.Umask(0111) + listener, err := net.ListenUnix("unix", address) + unix.Umask(oldmask) + runtime.UnlockOSThread() + if err != nil { + return nil, err + } + + logger.Debugf("socket %q was not activated; listening", socketPath) + + return listener, nil +} + +// Init sets up the Daemon's internal workings. +// Don't call more than once. +func (d *Daemon) Init() error { + t0 := time.Now() + listeners, err := activation.Listeners(false) + if err != nil { + return err + } + + listenerMap := make(map[string]net.Listener, len(listeners)) + + for _, listener := range listeners { + listenerMap[listener.Addr().String()] = listener + } + + // The SnapdSocket is required-- without it, die. + if listener, err := getListener(dirs.SnapdSocket, listenerMap); err == nil { + d.snapdListener = &ucrednetListener{listener} + } else { + return fmt.Errorf("when trying to listen on %s: %v", dirs.SnapdSocket, err) + } + + if listener, err := getListener(dirs.SnapSocket, listenerMap); err == nil { + // Note that the SnapSocket listener does not use ucrednet. We use the lack + // of remote information as an indication that the request originated with + // this socket. This listener may also be nil if that socket wasn't among + // the listeners, so check it before using it. + d.snapListener = listener + } else { + logger.Debugf("cannot get listener for %q: %v", dirs.SnapSocket, err) + } + + d.addRoutes() + + logger.Debugf("init done in %s", time.Now().Sub(t0)) + logger.Noticef("started %v.", store.UserAgent()) + + return nil +} + +func (d *Daemon) addRoutes() { + d.router = mux.NewRouter() + + for _, c := range api { + c.d = d + d.router.Handle(c.Path, c).Name(c.Path) + } + + // also maybe add a /favicon.ico handler... + + d.router.NotFoundHandler = NotFound("not found") +} + +var shutdownMsg = i18n.G("reboot scheduled to update the system - temporarily cancel with 'sudo shutdown -c'") + +// Start the Daemon +func (d *Daemon) Start() { + // die when asked to restart (systemd should get us back up!) + d.overlord.SetRestartHandler(func(t state.RestartType) { + switch t { + case state.RestartDaemon: + d.tomb.Kill(nil) + case state.RestartSystem: + cmd := exec.Command("shutdown", "+10", "-r", shutdownMsg) + if out, err := cmd.CombinedOutput(); err != nil { + logger.Noticef("%s", osutil.OutputErr(out, err)) + } + default: + logger.Noticef("internal error: restart handler called with unknown restart type: %v", t) + d.tomb.Kill(nil) + } + }) + + // the loop runs in its own goroutine + d.overlord.Loop() + + d.tomb.Go(func() error { + if d.snapListener != nil { + d.tomb.Go(func() error { + if err := http.Serve(d.snapListener, logit(d.router)); err != nil && d.tomb.Err() == tomb.ErrStillAlive { + return err + } + + return nil + }) + } + + if err := http.Serve(d.snapdListener, logit(d.router)); err != nil && d.tomb.Err() == tomb.ErrStillAlive { + return err + } + + return nil + }) +} + +// Stop shuts down the Daemon +func (d *Daemon) Stop() error { + d.tomb.Kill(nil) + d.snapdListener.Close() + if d.snapListener != nil { + d.snapListener.Close() + } + d.overlord.Stop() + + return d.tomb.Wait() +} + +// Dying is a tomb-ish thing +func (d *Daemon) Dying() <-chan struct{} { + return d.tomb.Dying() +} + +// New Daemon +func New() (*Daemon, error) { + ovld, err := overlord.New() + if err != nil { + return nil, err + } + return &Daemon{ + overlord: ovld, + // TODO: Decide when this should be disabled by default. + enableInternalInterfaceActions: true, + }, nil +} diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go new file mode 100644 index 00000000..897ab874 --- /dev/null +++ b/daemon/daemon_test.go @@ -0,0 +1,310 @@ +// -*- 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 . + * + */ + +package daemon + +import ( + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/gorilla/mux" + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/state" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { check.TestingT(t) } + +type daemonSuite struct{} + +var _ = check.Suite(&daemonSuite{}) + +func (s *daemonSuite) SetUpTest(c *check.C) { + dirs.SetRootDir(c.MkDir()) + err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) + c.Assert(err, check.IsNil) +} + +func (s *daemonSuite) TearDownTest(c *check.C) { + dirs.SetRootDir("") +} + +// build a new daemon, with only a little of Init(), suitable for the tests +func newTestDaemon(c *check.C) *Daemon { + d, err := New() + c.Assert(err, check.IsNil) + d.addRoutes() + + return d +} + +// a Response suitable for testing +type mockHandler struct { + cmd *Command + lastMethod string +} + +func (mck *mockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + mck.lastMethod = r.Method +} + +func mkRF(c *check.C, cmd *Command, mck *mockHandler) ResponseFunc { + return func(innerCmd *Command, req *http.Request, user *auth.UserState) Response { + c.Assert(cmd, check.Equals, innerCmd) + return mck + } +} + +func (s *daemonSuite) TestCommandMethodDispatch(c *check.C) { + cmd := &Command{d: newTestDaemon(c)} + mck := &mockHandler{cmd: cmd} + rf := mkRF(c, cmd, mck) + cmd.GET = rf + cmd.PUT = rf + cmd.POST = rf + cmd.DELETE = rf + + for _, method := range []string{"GET", "POST", "PUT", "DELETE"} { + req, err := http.NewRequest(method, "", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + cmd.ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, http.StatusUnauthorized, check.Commentf(method)) + + rec = httptest.NewRecorder() + req.RemoteAddr = "uid=0;" + req.RemoteAddr + + cmd.ServeHTTP(rec, req) + c.Check(mck.lastMethod, check.Equals, method) + c.Check(rec.Code, check.Equals, http.StatusOK) + } + + req, err := http.NewRequest("POTATO", "", nil) + c.Assert(err, check.IsNil) + req.RemoteAddr = "uid=0;" + req.RemoteAddr + + rec := httptest.NewRecorder() + cmd.ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, http.StatusMethodNotAllowed) +} + +func (s *daemonSuite) TestGuestAccess(c *check.C) { + get := &http.Request{Method: "GET"} + put := &http.Request{Method: "PUT"} + pst := &http.Request{Method: "POST"} + del := &http.Request{Method: "DELETE"} + + cmd := &Command{d: newTestDaemon(c)} + c.Check(cmd.canAccess(get, nil), check.Equals, false) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + c.Check(cmd.canAccess(pst, nil), check.Equals, false) + c.Check(cmd.canAccess(del, nil), check.Equals, false) + + cmd = &Command{d: newTestDaemon(c), UserOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, false) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + c.Check(cmd.canAccess(pst, nil), check.Equals, false) + c.Check(cmd.canAccess(del, nil), check.Equals, false) + + cmd = &Command{d: newTestDaemon(c), GuestOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + c.Check(cmd.canAccess(pst, nil), check.Equals, false) + c.Check(cmd.canAccess(del, nil), check.Equals, false) + + // Since this request has no RemoteAddr, it must be coming from the snap + // socket instead of the snapd one. In that case, if SnapOK is true, this + // command should be wide open for all HTTP methods. + cmd = &Command{d: newTestDaemon(c), SnapOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, true) + c.Check(cmd.canAccess(pst, nil), check.Equals, true) + c.Check(cmd.canAccess(del, nil), check.Equals, true) +} + +func (s *daemonSuite) TestUserAccess(c *check.C) { + get := &http.Request{Method: "GET", RemoteAddr: "uid=42;"} + put := &http.Request{Method: "PUT", RemoteAddr: "uid=42;"} + + cmd := &Command{d: newTestDaemon(c)} + c.Check(cmd.canAccess(get, nil), check.Equals, false) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + + cmd = &Command{d: newTestDaemon(c), UserOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + + cmd = &Command{d: newTestDaemon(c), GuestOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + + // Since this request has a RemoteAddr, it must be coming from the snapd + // socket instead of the snap one. In that case, SnapOK should have no + // bearing on the default behavior, which is to deny access. + cmd = &Command{d: newTestDaemon(c), SnapOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, false) + c.Check(cmd.canAccess(put, nil), check.Equals, false) +} + +func (s *daemonSuite) TestSuperAccess(c *check.C) { + get := &http.Request{Method: "GET", RemoteAddr: "uid=0;"} + put := &http.Request{Method: "PUT", RemoteAddr: "uid=0;"} + + cmd := &Command{d: newTestDaemon(c)} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, true) + + cmd = &Command{d: newTestDaemon(c), UserOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, true) + + cmd = &Command{d: newTestDaemon(c), GuestOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, true) + + cmd = &Command{d: newTestDaemon(c), SnapOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, true) +} + +func (s *daemonSuite) TestAddRoutes(c *check.C) { + d := newTestDaemon(c) + + expected := make([]string, len(api)) + for i, v := range api { + expected[i] = v.Path + } + + got := make([]string, 0, len(api)) + c.Assert(d.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + got = append(got, route.GetName()) + return nil + }), check.IsNil) + + c.Check(got, check.DeepEquals, expected) // this'll stop being true if routes are added that aren't commands (e.g. for the favicon) + + // XXX: still waiting to know how to check d.router.NotFoundHandler has been set to NotFound + // the old test relied on undefined behaviour: + // c.Check(fmt.Sprintf("%p", d.router.NotFoundHandler), check.Equals, fmt.Sprintf("%p", NotFound)) +} + +type witnessAcceptListener struct { + net.Listener + accept chan struct{} +} + +func (l *witnessAcceptListener) Accept() (net.Conn, error) { + close(l.accept) + return l.Listener.Accept() +} + +func (s *daemonSuite) TestStartStop(c *check.C) { + d := newTestDaemon(c) + l, err := net.Listen("tcp", "127.0.0.1:0") + c.Assert(err, check.IsNil) + + snapdAccept := make(chan struct{}) + d.snapdListener = &witnessAcceptListener{l, snapdAccept} + + snapAccept := make(chan struct{}) + d.snapListener = &witnessAcceptListener{l, snapAccept} + + d.Start() + + snapdDone := make(chan struct{}) + go func() { + select { + case <-snapdAccept: + case <-time.After(2 * time.Second): + c.Fatal("snapd accept was not called") + } + close(snapdDone) + }() + + snapDone := make(chan struct{}) + go func() { + select { + case <-snapAccept: + case <-time.After(2 * time.Second): + c.Fatal("snapd accept was not called") + } + close(snapDone) + }() + + <-snapdDone + <-snapDone + + err = d.Stop() + c.Check(err, check.IsNil) +} + +func (s *daemonSuite) TestRestartWiring(c *check.C) { + d := newTestDaemon(c) + l, err := net.Listen("tcp", "127.0.0.1:0") + c.Assert(err, check.IsNil) + + snapdAccept := make(chan struct{}) + d.snapdListener = &witnessAcceptListener{l, snapdAccept} + + snapAccept := make(chan struct{}) + d.snapListener = &witnessAcceptListener{l, snapAccept} + + d.Start() + defer d.Stop() + + snapdDone := make(chan struct{}) + go func() { + select { + case <-snapdAccept: + case <-time.After(2 * time.Second): + c.Fatal("snapd accept was not called") + } + close(snapdDone) + }() + + snapDone := make(chan struct{}) + go func() { + select { + case <-snapAccept: + case <-time.After(2 * time.Second): + c.Fatal("snap accept was not called") + } + close(snapDone) + }() + + <-snapdDone + <-snapDone + + d.overlord.State().RequestRestart(state.RestartDaemon) + + select { + case <-d.Dying(): + case <-time.After(2 * time.Second): + c.Fatal("RequestRestart -> overlord -> Kill chain didn't work") + } +} diff --git a/daemon/response.go b/daemon/response.go new file mode 100644 index 00000000..021f7378 --- /dev/null +++ b/daemon/response.go @@ -0,0 +1,242 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package daemon + +import ( + "encoding/json" + "fmt" + "mime" + "net/http" + "path/filepath" + "strconv" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/logger" +) + +// ResponseType is the response type +type ResponseType string + +// "there are three standard return types: Standard return value, +// Background operation, Error", each returning a JSON object with the +// following "type" field: +const ( + ResponseTypeSync ResponseType = "sync" + ResponseTypeAsync ResponseType = "async" + ResponseTypeError ResponseType = "error" +) + +// Response knows how to serve itself, and how to find itself +type Response interface { + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +type resp struct { + Status int `json:"status-code"` + Type ResponseType `json:"type"` + Result interface{} `json:"result"` + *Meta +} + +// TODO This is being done in a rush to get the proper external +// JSON representation in the API in time for the release. +// The right code style takes a bit more work and unifies +// these fields inside resp. +type Meta struct { + Sources []string `json:"sources,omitempty"` + Paging *Paging `json:"paging,omitempty"` + SuggestedCurrency string `json:"suggested-currency,omitempty"` + Change string `json:"change,omitempty"` +} + +type Paging struct { + Page int `json:"page"` + Pages int `json:"pages"` +} + +type respJSON struct { + Type ResponseType `json:"type"` + Status int `json:"status-code"` + StatusText string `json:"status"` + Result interface{} `json:"result"` + *Meta +} + +func (r *resp) MarshalJSON() ([]byte, error) { + return json.Marshal(respJSON{ + Type: r.Type, + Status: r.Status, + StatusText: http.StatusText(r.Status), + Result: r.Result, + Meta: r.Meta, + }) +} + +func (r *resp) ServeHTTP(w http.ResponseWriter, _ *http.Request) { + status := r.Status + bs, err := r.MarshalJSON() + if err != nil { + logger.Noticef("cannot marshal %#v to JSON: %v", *r, err) + bs = nil + status = http.StatusInternalServerError + } + + hdr := w.Header() + if r.Status == http.StatusAccepted || r.Status == http.StatusCreated { + if m, ok := r.Result.(map[string]interface{}); ok { + if location, ok := m["resource"]; ok { + if location, ok := location.(string); ok && location != "" { + hdr.Set("Location", location) + } + } + } + } + + hdr.Set("Content-Type", "application/json") + w.WriteHeader(status) + w.Write(bs) +} + +type errorKind string + +const ( + errorKindTwoFactorRequired = errorKind("two-factor-required") + errorKindTwoFactorFailed = errorKind("two-factor-failed") + errorKindLoginRequired = errorKind("login-required") + errorKindInvalidAuthData = errorKind("invalid-auth-data") + errorKindTermsNotAccepted = errorKind("terms-not-accepted") + errorKindNoPaymentMethods = errorKind("no-payment-methods") + errorKindPaymentDeclined = errorKind("payment-declined") + + errorKindSnapAlreadyInstalled = errorKind("snap-already-installed") + errorKindSnapNotInstalled = errorKind("snap-not-installed") + errorKindSnapNoUpdateAvailable = errorKind("snap-no-update-available") +) + +type errorValue interface{} + +type errorResult struct { + Message string `json:"message"` // note no omitempty + Kind errorKind `json:"kind,omitempty"` + Value errorValue `json:"value,omitempty"` +} + +// SyncResponse builds a "sync" response from the given result. +func SyncResponse(result interface{}, meta *Meta) Response { + if err, ok := result.(error); ok { + return InternalError("internal error: %v", err) + } + + if rsp, ok := result.(Response); ok { + return rsp + } + + return &resp{ + Type: ResponseTypeSync, + Status: http.StatusOK, + Result: result, + Meta: meta, + } +} + +// AsyncResponse builds an "async" response from the given *Task +func AsyncResponse(result map[string]interface{}, meta *Meta) Response { + return &resp{ + Type: ResponseTypeAsync, + Status: http.StatusAccepted, + Result: result, + Meta: meta, + } +} + +// makeErrorResponder builds an errorResponder from the given error status. +func makeErrorResponder(status int) errorResponder { + return func(format string, v ...interface{}) Response { + res := &errorResult{ + Message: fmt.Sprintf(format, v...), + } + if status == http.StatusUnauthorized { + res.Kind = errorKindLoginRequired + } + return &resp{ + Type: ResponseTypeError, + Result: res, + Status: status, + } + } +} + +// A FileResponse 's ServeHTTP method serves the file +type FileResponse string + +// ServeHTTP from the Response interface +func (f FileResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { + filename := fmt.Sprintf("attachment; filename=%s", filepath.Base(string(f))) + w.Header().Add("Content-Disposition", filename) + http.ServeFile(w, r, string(f)) +} + +type assertResponse struct { + assertions []asserts.Assertion + bundle bool +} + +// AssertResponse builds a response whose ServerHTTP method serves one or a bundle of assertions. +func AssertResponse(asserts []asserts.Assertion, bundle bool) Response { + if len(asserts) > 1 { + bundle = true + } + return &assertResponse{assertions: asserts, bundle: bundle} +} + +func (ar assertResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { + t := asserts.MediaType + if ar.bundle { + t = mime.FormatMediaType(t, map[string]string{"bundle": "y"}) + } + w.Header().Set("Content-Type", t) + w.Header().Set("X-Ubuntu-Assertions-Count", strconv.Itoa(len(ar.assertions))) + w.WriteHeader(http.StatusOK) + enc := asserts.NewEncoder(w) + for _, a := range ar.assertions { + err := enc.Encode(a) + if err != nil { + logger.Noticef("cannot write encoded assertion into response: %v", err) + break + + } + } +} + +// errorResponder is a callable that produces an error Response. +// e.g., InternalError("something broke: %v", err), etc. +type errorResponder func(string, ...interface{}) Response + +// standard error responses +var ( + Unauthorized = makeErrorResponder(http.StatusUnauthorized) + NotFound = makeErrorResponder(http.StatusNotFound) + BadRequest = makeErrorResponder(http.StatusBadRequest) + BadMethod = makeErrorResponder(http.StatusMethodNotAllowed) + InternalError = makeErrorResponder(http.StatusInternalServerError) + NotImplemented = makeErrorResponder(http.StatusNotImplemented) + Forbidden = makeErrorResponder(http.StatusForbidden) + Conflict = makeErrorResponder(http.StatusConflict) +) diff --git a/daemon/response_test.go b/daemon/response_test.go new file mode 100644 index 00000000..e26e0d5b --- /dev/null +++ b/daemon/response_test.go @@ -0,0 +1,99 @@ +// -*- 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 . + * + */ + +package daemon + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + + "gopkg.in/check.v1" +) + +type responseSuite struct{} + +var _ = check.Suite(&responseSuite{}) + +func (s *responseSuite) TestRespSetsLocationIfAccepted(c *check.C) { + rec := httptest.NewRecorder() + + rsp := &resp{ + Status: http.StatusAccepted, + Result: map[string]interface{}{ + "resource": "foo/bar", + }, + } + + rsp.ServeHTTP(rec, nil) + hdr := rec.Header() + c.Check(hdr.Get("Location"), check.Equals, "foo/bar") +} + +func (s *responseSuite) TestRespSetsLocationIfCreated(c *check.C) { + rec := httptest.NewRecorder() + + rsp := &resp{ + Status: http.StatusCreated, + Result: map[string]interface{}{ + "resource": "foo/bar", + }, + } + + rsp.ServeHTTP(rec, nil) + hdr := rec.Header() + c.Check(hdr.Get("Location"), check.Equals, "foo/bar") +} + +func (s *responseSuite) TestRespDoesNotSetLocationIfOther(c *check.C) { + rec := httptest.NewRecorder() + + rsp := &resp{ + Status: http.StatusTeapot, + Result: map[string]interface{}{ + "resource": "foo/bar", + }, + } + + rsp.ServeHTTP(rec, nil) + hdr := rec.Header() + c.Check(hdr.Get("Location"), check.Equals, "") +} + +func (s *responseSuite) TestFileResponseSetsContentDisposition(c *check.C) { + const filename = "icon.png" + + path := filepath.Join(c.MkDir(), filename) + err := ioutil.WriteFile(path, nil, os.ModePerm) + c.Check(err, check.IsNil) + + rec := httptest.NewRecorder() + rsp := FileResponse(path) + req, err := http.NewRequest("GET", "", nil) + c.Check(err, check.IsNil) + + rsp.ServeHTTP(rec, req) + + hdr := rec.Header() + c.Check(hdr.Get("Content-Disposition"), check.Equals, + fmt.Sprintf("attachment; filename=%s", filename)) +} diff --git a/daemon/snap.go b/daemon/snap.go new file mode 100644 index 00000000..5e37d67f --- /dev/null +++ b/daemon/snap.go @@ -0,0 +1,260 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 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 . + * + */ + +package daemon + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" +) + +var errNoSnap = errors.New("no snap installed") + +// snapIcon tries to find the icon inside the snap +func snapIcon(info *snap.Info) string { + // XXX: copy of snap.Snap.Icon which will go away + found, _ := filepath.Glob(filepath.Join(info.MountDir(), "meta", "gui", "icon.*")) + if len(found) == 0 { + return info.IconURL + } + + return found[0] +} + +// snapDate returns the time of the snap mount directory. +func snapDate(info *snap.Info) time.Time { + st, err := os.Stat(info.MountDir()) + if err != nil { + return time.Time{} + } + + return st.ModTime() +} + +func publisherName(st *state.State, info *snap.Info) (string, error) { + if info.SnapID == "" { + return "", nil + } + + pubAcct, err := assertstate.Publisher(st, info.SnapID) + if err != nil { + return "", fmt.Errorf("cannot find publisher details: %v", err) + } + return pubAcct.Username(), nil +} + +type aboutSnap struct { + info *snap.Info + snapst *snapstate.SnapState + publisher string +} + +// localSnapInfo returns the information about the current snap for the given name plus the SnapState with the active flag and other snap revisions. +func localSnapInfo(st *state.State, name string) (aboutSnap, error) { + st.Lock() + defer st.Unlock() + + var snapst snapstate.SnapState + err := snapstate.Get(st, name, &snapst) + if err != nil && err != state.ErrNoState { + return aboutSnap{}, fmt.Errorf("cannot consult state: %v", err) + } + + info, err := snapst.CurrentInfo() + if err == snapstate.ErrNoCurrent { + return aboutSnap{}, errNoSnap + } + if err != nil { + return aboutSnap{}, fmt.Errorf("cannot read snap details: %v", err) + } + + publisher, err := publisherName(st, info) + if err != nil { + return aboutSnap{}, err + } + + return aboutSnap{ + info: info, + snapst: &snapst, + publisher: publisher, + }, nil +} + +// allLocalSnapInfos returns the information about the all current snaps and their SnapStates. +func allLocalSnapInfos(st *state.State, all bool) ([]aboutSnap, error) { + st.Lock() + defer st.Unlock() + + snapStates, err := snapstate.All(st) + if err != nil { + return nil, err + } + about := make([]aboutSnap, 0, len(snapStates)) + + var firstErr error + for _, snapst := range snapStates { + var aboutThis []aboutSnap + var info *snap.Info + var publisher string + var err error + if all { + for _, seq := range snapst.Sequence { + info, err = snap.ReadInfo(seq.RealName, seq) + if err != nil { + break + } + publisher, err = publisherName(st, info) + aboutThis = append(aboutThis, aboutSnap{info, snapst, publisher}) + } + } else { + info, err = snapst.CurrentInfo() + if err == nil { + var publisher string + publisher, err = publisherName(st, info) + aboutThis = append(aboutThis, aboutSnap{info, snapst, publisher}) + } + } + + if err != nil { + // XXX: aggregate instead? + if firstErr == nil { + firstErr = err + } + continue + } + about = append(about, aboutThis...) + } + + return about, firstErr +} + +// appJSON contains the json for snap.AppInfo +type appJSON struct { + Name string `json:"name"` + Daemon string `json:"daemon"` + Aliases []string `json:"aliases"` +} + +// screenshotJSON contains the json for snap.ScreenshotInfo +type screenshotJSON struct { + URL string `json:"url"` + Width int64 `json:"width,omitempty"` + Height int64 `json:"height,omitempty"` +} + +func mapLocal(about aboutSnap) map[string]interface{} { + localSnap, snapst := about.info, about.snapst + status := "installed" + if snapst.Active && localSnap.Revision == snapst.Current { + status = "active" + } + + apps := make([]appJSON, 0, len(localSnap.Apps)) + for _, app := range localSnap.Apps { + apps = append(apps, appJSON{ + Name: app.Name, + Daemon: app.Daemon, + Aliases: app.Aliases, + }) + } + + return map[string]interface{}{ + "description": localSnap.Description(), + "developer": about.publisher, + "icon": snapIcon(localSnap), + "id": localSnap.SnapID, + "install-date": snapDate(localSnap), + "installed-size": localSnap.Size, + "name": localSnap.Name(), + "revision": localSnap.Revision, + "status": status, + "summary": localSnap.Summary(), + "type": string(localSnap.Type), + "version": localSnap.Version, + "channel": localSnap.Channel, + "tracking-channel": snapst.Channel, + "confinement": localSnap.Confinement, + "devmode": snapst.DevMode, + "trymode": snapst.TryMode, + "jailmode": snapst.JailMode, + "private": localSnap.Private, + "apps": apps, + "broken": localSnap.Broken, + } +} + +func mapRemote(remoteSnap *snap.Info) map[string]interface{} { + status := "available" + if remoteSnap.MustBuy { + status = "priced" + } + + confinement := remoteSnap.Confinement + if confinement == "" { + confinement = snap.StrictConfinement + } + + screenshots := make([]screenshotJSON, len(remoteSnap.Screenshots)) + for i, screenshot := range remoteSnap.Screenshots { + screenshots[i] = screenshotJSON{ + URL: screenshot.URL, + Width: screenshot.Width, + Height: screenshot.Height, + } + } + + result := map[string]interface{}{ + "description": remoteSnap.Description(), + "developer": remoteSnap.Publisher, + "download-size": remoteSnap.Size, + "icon": snapIcon(remoteSnap), + "id": remoteSnap.SnapID, + "name": remoteSnap.Name(), + "revision": remoteSnap.Revision, + "status": status, + "summary": remoteSnap.Summary(), + "type": string(remoteSnap.Type), + "version": remoteSnap.Version, + "channel": remoteSnap.Channel, + "private": remoteSnap.Private, + "confinement": confinement, + } + + if len(screenshots) > 0 { + result["screenshots"] = screenshots + } + + if len(remoteSnap.Prices) > 0 { + result["prices"] = remoteSnap.Prices + } + + if len(remoteSnap.Channels) > 0 { + result["channels"] = remoteSnap.Channels + } + + return result +} diff --git a/daemon/ucrednet.go b/daemon/ucrednet.go new file mode 100644 index 00000000..20853fa9 --- /dev/null +++ b/daemon/ucrednet.go @@ -0,0 +1,95 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package daemon + +import ( + "errors" + "fmt" + "net" + "strconv" + "strings" + sys "syscall" +) + +var errNoUID = errors.New("no uid found") + +const ucrednetNobody = uint32((1 << 32) - 1) + +func ucrednetGetUID(remoteAddr string) (uint32, error) { + idx := strings.IndexByte(remoteAddr, ';') + if !strings.HasPrefix(remoteAddr, "uid=") || idx < 5 { + return ucrednetNobody, errNoUID + } + + uid, err := strconv.ParseUint(remoteAddr[4:idx], 10, 32) + if err != nil { + return ucrednetNobody, err + } + + return uint32(uid), nil +} + +type ucrednetAddr struct { + net.Addr + uid string +} + +func (wa *ucrednetAddr) String() string { + return fmt.Sprintf("uid=%s;%s", wa.uid, wa.Addr) +} + +type ucrednetConn struct { + net.Conn + uid string +} + +func (wc *ucrednetConn) RemoteAddr() net.Addr { + return &ucrednetAddr{wc.Conn.RemoteAddr(), wc.uid} +} + +type ucrednetListener struct{ net.Listener } + +var getUcred = sys.GetsockoptUcred + +func (wl *ucrednetListener) Accept() (net.Conn, error) { + con, err := wl.Listener.Accept() + if err != nil { + return nil, err + } + + uid := "" + if ucon, ok := con.(*net.UnixConn); ok { + f, err := ucon.File() + if err != nil { + return nil, err + } + // File() is a dup(); needs closing + defer f.Close() + + ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED) + if err != nil { + return nil, err + } + + uid = strconv.FormatUint(uint64(ucred.Uid), 10) + } + + return &ucrednetConn{con, uid}, err +} diff --git a/daemon/ucrednet_test.go b/daemon/ucrednet_test.go new file mode 100644 index 00000000..e8bb5973 --- /dev/null +++ b/daemon/ucrednet_test.go @@ -0,0 +1,172 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package daemon + +import ( + "errors" + "net" + "path/filepath" + sys "syscall" + + "gopkg.in/check.v1" +) + +type ucrednetSuite struct { + ucred *sys.Ucred + err error +} + +var _ = check.Suite(&ucrednetSuite{}) + +func (s *ucrednetSuite) getUcred(fd, level, opt int) (*sys.Ucred, error) { + return s.ucred, s.err +} + +func (s *ucrednetSuite) SetUpSuite(c *check.C) { + getUcred = s.getUcred +} + +func (s *ucrednetSuite) TearDownTest(c *check.C) { + s.ucred = nil + s.err = nil +} +func (s *ucrednetSuite) TearDownSuite(c *check.C) { + getUcred = sys.GetsockoptUcred +} + +func (s *ucrednetSuite) TestAcceptConnRemoteAddrString(c *check.C) { + s.ucred = &sys.Ucred{Uid: 42} + d := c.MkDir() + sock := filepath.Join(d, "sock") + + l, err := net.Listen("unix", sock) + c.Assert(err, check.IsNil) + defer l.Close() + + go func() { + cli, err := net.Dial("unix", sock) + c.Assert(err, check.IsNil) + cli.Close() + }() + + wl := &ucrednetListener{l} + + conn, err := wl.Accept() + c.Assert(err, check.IsNil) + defer conn.Close() + + remoteAddr := conn.RemoteAddr().String() + c.Check(remoteAddr, check.Matches, "uid=42;.*") + uid, err := ucrednetGetUID(remoteAddr) + c.Check(uid, check.Equals, uint32(42)) + c.Check(err, check.IsNil) +} + +func (s *ucrednetSuite) TestNonUnix(c *check.C) { + l, err := net.Listen("tcp", "localhost:0") + c.Assert(err, check.IsNil) + defer l.Close() + + addr := l.Addr().String() + + go func() { + cli, err := net.Dial("tcp", addr) + c.Assert(err, check.IsNil) + cli.Close() + }() + + wl := &ucrednetListener{l} + + conn, err := wl.Accept() + c.Assert(err, check.IsNil) + defer conn.Close() + + remoteAddr := conn.RemoteAddr().String() + c.Check(remoteAddr, check.Matches, "uid=;.*") + uid, err := ucrednetGetUID(remoteAddr) + c.Check(uid, check.Equals, ucrednetNobody) + c.Check(err, check.Equals, errNoUID) +} + +func (s *ucrednetSuite) TestAcceptErrors(c *check.C) { + s.ucred = &sys.Ucred{Uid: 42} + d := c.MkDir() + sock := filepath.Join(d, "sock") + + l, err := net.Listen("unix", sock) + c.Assert(err, check.IsNil) + c.Assert(l.Close(), check.IsNil) + + wl := &ucrednetListener{l} + + _, err = wl.Accept() + c.Assert(err, check.NotNil) +} + +func (s *ucrednetSuite) TestUcredErrors(c *check.C) { + s.err = errors.New("oopsie") + d := c.MkDir() + sock := filepath.Join(d, "sock") + + l, err := net.Listen("unix", sock) + c.Assert(err, check.IsNil) + defer l.Close() + + go func() { + cli, err := net.Dial("unix", sock) + c.Assert(err, check.IsNil) + cli.Close() + }() + + wl := &ucrednetListener{l} + + _, err = wl.Accept() + c.Assert(err, check.Equals, s.err) +} + +func (s *ucrednetSuite) TestGetNoUid(c *check.C) { + uid, err := ucrednetGetUID("uid=;") + c.Check(err, check.Equals, errNoUID) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGetBadUid(c *check.C) { + uid, err := ucrednetGetUID("uid=hello;") + c.Check(err, check.NotNil) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGetNonUcrednet(c *check.C) { + uid, err := ucrednetGetUID("hello") + c.Check(err, check.Equals, errNoUID) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGetNothing(c *check.C) { + uid, err := ucrednetGetUID("") + c.Check(err, check.Equals, errNoUID) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGet(c *check.C) { + uid, err := ucrednetGetUID("uid=42;") + c.Check(err, check.IsNil) + c.Check(uid, check.Equals, uint32(42)) +} diff --git a/data/completion/snap b/data/completion/snap new file mode 100644 index 00000000..22f8652b --- /dev/null +++ b/data/completion/snap @@ -0,0 +1,40 @@ +# -*- sh -*- +_complete() { + local cur prev words cword + _init_completion || return + + local command + if [[ ${#words[@]} -gt 2 ]]; then + if [[ ${words[1]} =~ ^-- ]]; then + # global options take no args + return 0 + fi + if [[ ${words[-2]} = "--help" ]]; then + # help takes no args + return 0 + fi + + command=${words[-2]} + fi + + # Only split on newlines + local IFS=$'\n' + + COMPREPLY=($(GO_FLAGS_COMPLETION=1 "${words[@]}")) + + case $command in + install|info|sign-build) + _filedir "snap" + ;; + ack) + _filedir + ;; + try) + _filedir -d + ;; + esac + + return 0 +} + +complete -F _complete snap diff --git a/data/failure.txt b/data/failure.txt new file mode 100644 index 00000000..2c5e1620 --- /dev/null +++ b/data/failure.txt @@ -0,0 +1,8 @@ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░█▀▀░█▀▄░█░█░█▀▀░█░█░▀█▀░█▀█░█▀▀░░░█▀▀░█▀█░▀█▀░█░░░█░█░█▀▄░█▀▀░░░ +░░░█░░░█▀▄░█░█░▀▀█░█▀█░░█░░█░█░█░█░░░█▀▀░█▀█░░█░░█░░░█░█░█▀▄░█▀▀░░░ +░░░▀▀▀░▀░▀░▀▀▀░▀▀▀░▀░▀░▀▀▀░▀░▀░▀▀▀░░░▀░░░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀▀▀░░░ +░░░░░░░░░░░░░█▀█░█▀█░█▀▄░░░█▀▄░█▀▀░█▀▀░█▀█░█▀█░▀█▀░█▀▄░░░░░░░░░░░░░ +░░░░░░░░░░░░░█▀█░█░█░█░█░░░█░█░█▀▀░▀▀█░█▀▀░█▀█░░█░░█▀▄░░░░░░░░░░░░░ +░░░░░░░░░░░░░▀░▀░▀░▀░▀▀░░░░▀▀░░▀▀▀░▀▀▀░▀░░░▀░▀░▀▀▀░▀░▀░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ diff --git a/data/info b/data/info new file mode 100644 index 00000000..9fbbbda2 --- /dev/null +++ b/data/info @@ -0,0 +1 @@ +VERSION=unknown diff --git a/data/success.txt b/data/success.txt new file mode 100644 index 00000000..ccb9d409 --- /dev/null +++ b/data/success.txt @@ -0,0 +1,20 @@ + ▒██▒ ████ ████ ██ + ▓██▓ ████ ████ ██ + ████ ██ ██ ██ + ████ ██ ██ ▒███▒██ ░████░ ░████░ ▒███░██ + ▒█▓▓█▒ ██ ██ ░███████ ░██████░ ░██████░ ▒███████ + ▓█▒▒█▓ ██ ██ ███ ███ ███ ███ ███ ███ ███ ███ + ██ ██ ██ ██ ██░ ░██ ██░ ░██ ██░ ░██ ██░ ░██ + ██████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ░██████░ ██ ██ ██░ ░██ ██░ ░██ ██░ ░██ ██░ ░██ + ▒██ ██▒ ██▒ ██▒ ███ ███ ███ ███ ███ ███ ███ ███ + ███ ███ █████ █████ ░███████ ░██████░ ░██████░ ▒███████ + ██▒ ▒██ ░████ ░████ ▒███▒██ ░████░ ░████░ ▒███░██ + █░ ▒██ + ██████▓ + ▒████▒ + + ▌ ▐ ▜ ▌ ▗ ▌ ▜ + ▌ ▌▛▀▖▝▀▖▜▀ ▞▀▖▞▀▖▌ ▌▐ ▞▀▌ ▛▀▖▞▀▖▞▀▘▞▀▘▄ ▛▀▖▐ ▌ ▌ ▞▀▌▞▀▖ ▌ ▌▙▀▖▞▀▖▛▀▖▞▀▌ + ▐▐▐ ▌ ▌▞▀▌▐ ▖ ▌ ▖▌ ▌▌ ▌▐ ▌ ▌ ▙▄▘▌ ▌▝▀▖▝▀▖▐ ▌ ▌▐ ▚▄▌ ▚▄▌▌ ▌ ▐▐▐ ▌ ▌ ▌▌ ▌▚▄▌ + ▘▘ ▘ ▘▝▀▘ ▀ ▝▀ ▝▀ ▝▀▘ ▘▝▀▘ ▌ ▝▀ ▀▀ ▀▀ ▀▘▀▀ ▘▗▄▘ ▗▄▘▝▀ ▘▘ ▘ ▝▀ ▘ ▘▗▄▘ diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000..7246db28 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,2235 @@ +snapd (2.21) xenial; urgency=medium + + * New upstream release, LP: #1656382 + - daemon: re-enable reexec + - interfaces: allow reading installed files from previous revisions + by default + - daemon: make activation optional + - tests: run all snap-confine tests in c-unit-tests task + - many: fix abbreviated forms of disconnect + - tests: switch more tests to MATCH + - store: export userAgent. daemon: print store.UserAgent() on + startup. + - tests: test classic confinement `snap list` and `snap info` + output + - debian: skip snap-confine unit tests on nocheck + - overlord/snapstate: share code between Update and UpdateMany, so + that it deals with auto-aliases correctly + - interfaces: upower-observe: refactor to allow snaps to provide a + slot + - tests: add end-to-end store test for classic confinement + - overlord,overlord/snapstate: have UpdateMany retire/enable auto- + aliases even without new revision + - interfaces/browser-support: add @{PROC}/@{pid}/fd/[0-9] w and misc + /run/udev + - interfaces/builtin: add physical-memory-* and io-ports-control + - interfaces: allow getsockopt by default since it is so commonly + used + - cmd/snap, daemon, overlord/snapstate: tests and fixes for "snap + refresh" of a classic snap + - interfaces: allow read/write access of real-time clock with time- + control interface + - store: request no CDN via a header using SNAPPY_STORE_NO_CDN + envvar + - snap: add information about tracking channel (not just actual + channel) + - interfaces: use fewer dot imports + - overlord/snapstate: remove restrictions on ResetAliases + - overlord, store: move confinement filtering to the overlord (from + The Store) + - many: move interface test helpers to ifacetest package + - many: implement 'snap aliases' + - vet: fix for unkeyed fields error on aliases_test.go + - interfaces: miscellaneous policy updates for network-control, + unity7, pulseaudio, default and home + - tests: test for auto-aliases + - interface hooks: connect plug slot hooks (step 2) + - cmd/snap: fix internal naming in snap connect + - snap: use "size" as the json tag in snap.ChannelSnapInfo + - tests: restore the missing initialization of iface manager causing + race + - snap: fix missing sizes in `snap info ` + - tests: improve cleanup for c-unit-tests + - cmd/snap-confine: build non-installed libsnap-confine-private.a + - cmd/snap-confine: small tweaks to seccomp support code + - interfaces/docker-support: allow /run/shm/aufs.xeno for 14.04 + - many: obtain installed snaps developer/publisher username through + assertions + - store: setting of fields for details endpoint + - cmd/snap-confine: check for rst2man on configure + - snap: show `snap --help` output when just running `snap` + - interface/builtin: drop the obsolete checks in udisks2 + SanitizeSlot + - cmd/snap: remove currency switch following UX review + - spread: find top-level directory before running generate- + packaging-dir + - interface hooks: prepare plug slot hooks (step 1) + - i18n: use github.com/mvo5/gettext.go (pure go) for i18n to avoid + cgo + - many: put a marker in the User-Agent sent by snapd/snap when under + testingThe User-Agent will look like: + - tests: fix -reuse and -resend when govendor is missing + - snap: provide friendlier `snap find` message when no snaps are + found + - tests: fix mkversions.sh failure on zesty + - spread: install build-essentail unconditionally + - spread: improve qemu ubuntu-14.04-{32,64} support + - overlord/snapstate,daemon: implement GET /v2/aliases handling + - store: retry user info request + - tests: port more snap-confine regression tests + - tests: cancel the scheduled reboot on ubuntu-core-upgrade-no-gc + and restore state + - tests: debug zesty autopkgtest failures + - overlord/snapstate: use keyed fields on literals + - tests: use MATCH in install-remove-multi + - tests: increase wait time for service to be up + - tests: make debug-each succeed if DENIED doesn't match + - tests: skip packaging dir generation for non-git based autopkgtest + runs + - tests: port refresh-all-undo to MATCH + - tests: improve snap connect test + - tests: port additional snap-confine regression tests + - tests: show --version when it matches unknown + - tests: optionally use apt proxy for qemu + - tests: add hello-classic test + - many: behave more consistently when pointed to staging and + possibly the fake store + - overlord/ifacestate: remove stale comments + - interfaces/apparmor: ignore snippets in classic confinement + - tests: port first regression test from snap-confine + - cmd/snap-confine: disable old tests + + -- Michael Vogt Fri, 13 Jan 2017 19:39:51 +0100 + +snapd (2.20.1) xenial; urgency=medium + + * New upstream release, LP: #1648520 + - tests: enable the ppc64el tests again + - tests: add classic confinement test + - tests: run snap confine tests in debian/rules already + + -- Michael Vogt Mon, 19 Dec 2016 11:53:29 +0100 + +snapd (2.20) xenial; urgency=medium + + * New upstream release, LP: #1648520 + - many: implement "snap alias --reset" using snapstate.ResetAliases + - debian: use a packaging branch for 14.04 + - store: retry downloads on io.Copy errors and sha3 checksum errors + - snap: show apps in `snap info` + - store: send an explicit X-Ubuntu-Classic header to the store + - overlord/snapstate: implement snapstate.ResetAliases + - interfaces/builtin: add dbus interface + - tests: fix tests on 17.04 + - store: use mocked retry strategy to make store tests faster + - overlord: apply auto-aliases information from the snap-declaration + on install or refresh + - many: prepare landing on trusty + - many: implement snap unalias using snapstate.Unalias + - overlord/snapstate: fixing the placement/grouping of some + functions + - interfaces: support network namespaces via 'ip netns' in network- + control + - interfaces/builtin: fix pulseaudio apparmor rules + - interfaces/builtin: add iio interface + - tests: update custom core snap with the freshly build snap-confine + - interfaces: use sysd.{Disable,Stop} instead of sysd.DisableNow() + - overlord,overlord/snapstate: implement snapstate.Unalias by + generalizing the "alias" task + - interfaces: misc openstack snap enablement + - cmd/snap: mock terminal.ReadPassword instead of using /dev/ptmx + - notifications, daemon: kill the unsupported events endpoint + - client: only allow Dangerous option in InstallPath + - overlord/ifacestate: no interface checks if no snap id + - many: implement alias command + - snap: tweak snap install output as designed by Mark + - debian: fix Pre-Depends on dpkg + - tests: check if snap-confine --version is unknown + - cmd/snap-confine: allow content interface mounts + - tests: remove ppa:snappy-dev/image again + - interfaces/apparmor: allow access to core snap + - tests: remove snap-confine/ubuntu-core-launcher after the tests + - overlord,overlord/snapstate: implement snapstate.Alias + - cmd/snap: reject "snap disconnect foo" + - debian: add split ubuntu-core-launcher and snap-confine packages + - cmd: fix mkversion.sh and add regression test + - overlord/snapstate: setup/remove aliases as we link/unlink snaps + - cmd/snap,tests: alias support in snap run + - snap/snapenv: don't obscure HOME if snap uses classic confinement + - store: decode response.Body json inside retry loops + - cmd/snap-confine: fix compilation on platforms with gcc < 4.9.0 + - vendor: update tomb package fixing context support + + -- Michael Vogt Thu, 15 Dec 2016 22:07:08 +0100 + +snapd (2.19) xenial; urgency=medium + + * New upstream release, LP: #1648520 + - cmd/snap-confine: disable support for XDG_RUNTIME_DIR + - cmd/snap-confine/tests: fix stale path after move to snapd + - cmd/snap-confine: don't use __attribute__((nonull)) + - snap: add description to `snap info` + - snap: show last refresh time + - store: switch default delta format from xdelta to xdelta3 + - interfaces: fix system-observe interface to work with ps_mem + - debian: add missing ca-certificates dependency + - cmd/snap-confine: add support for classic confinement + - snapstate/backend: add backend methods to manage aliases + - tests: re-enable snap-confine unit tests via spread + - many: merge snap-confine into snapd + - many: add support for classic confinement + - snap: abort install with ctrl+c + - cmd/snap: change terms accept URL following UX review + - interfaces/apparmor: use distinct apparmor template for classic + - snap: add snap size to `snap info` + - interfaces: add unconfined access to modem-manager + - snap: support for parsing and exposing on snap.Info aliases + - debian: disable autopkgtests on ppc64el + - snap: disable support for socket activation + - tests: fix incorrect restore of the current symlink + - asserts: introduce auto-aliases header in snap-declaration + - interfaces/seccomp: add support for classic confinement + - tests: do not use external snaps + - daemon: close the dup()ed file descriptor to not leak it + - overlord, daemon, progress: enable building snapd without CGO + - daemon, store: let snap info find things in any channel + - store: retry tweaks and logging + - snap: Improve `snap --help` output as designed by Mark + - interfaces/builtin: fix incorrect udev rule in i2c + - overlord: increase test timeout and improve failure message + - snap: remove unused experimental command + - debian: remove unneeded conflict against the "snappy" package + - daemon, strutil: move daemon.quotedNames to strutil.Quoted + - docs: document SNAP_DEBUG_HTTP in HACKING.md + - cmd/snap: have some completers + - snap: support "daemon: notify" in snap.yaml + - snap: fix try command when daemon linie is added + - interfaces: apparmor support for classic confinement + - debian/rules: build with -buildoptions=pie + - tests: include /boot in saved state (including bootenv and any + kernels) + - daemon: ensure `snap try` installs core if it's missing + - tests: save/restore /snap/core/current symlink + - tests: decrease the number of expected featured apps + - tests: add set -e to the prepare ssh script + - cmd/snap: add tests for section completion; fix bugs. + - cmd/snap: document 'snap list --all' + + -- Michael Vogt Thu, 08 Dec 2016 16:16:04 +0100 + +snapd (2.18.1) xenial; urgency=medium + + * New upstream release, LP: #1644625 + - daemon: fix crash when `snap refresh` contains a single update + - fix unhandled error from io.Copy() in download() + - interfaces/builtin: fix incorrect udev rule in i2c + + -- Michael Vogt Mon, 05 Dec 2016 15:04:13 +0100 + +snapd (2.18) xenial; urgency=medium + + * New upstream release, LP: #1644625 + - store: retry on io.EOF + - tests: skip pty tests on ppc64el and powerpc + - client, cmd/snap: introducing "snap info" + - snap: do exit 0 on install/remove if that snap is already + installed or already removed + - snap: add `snap watch ` to attach to a running change + - store: retry downloads using retry loop + - snap: try doesn't require snap-dir when run in snap's directory + - daemon: show what will change in the "refresh-all" changes + - tests: disable autorefresh for the external backend + - snap: add `snap list -a` to show all snaps (even inactive ones) + - many: unify boolean env var handling + - overlord/ifacestate: don't setup jailmode snaps with devmode + confinement + - snapstate: do not garbage collect the snaps used by the bootenv + - debian: drop hard xdelta dependency for now + - snap: make `snap login` ask for email if not given as argument + - osutil: fix build on armhf (arm in go-arch) and powerpc (ppc in + go-arch) + - many: rename DevmodeConfinement to DevModeConfinement + - store: resp.Body.Close() missing in ReadyToBuy + - many: use ConfinementOptions instead of ConfinementType + - snap, daemon, store: fake the channel map in the REST API + - misc: run github.com/gordonklaus/ineffassign as part of the static + checks + - docs: add goreportcard badge and remove coveralls badge + - tests: force gofmt -s in static checks + - many: run gofmt -s -w on all the code + - store: DRY actual retry code + - many: fix various errors uncovered by goreportcard.com + - interfaces/builtin: allow additional shared memory for webkit + - many: some more missing snapState->snapst + - asserts: introduce an optional freeform display-name for model + - interfaces/builtin: rename usb-raw to raw-usb + - progress: init pbar with correct total value + - daemon/api.go: add quotedNames() helper + - interfaces: add ConfinementOptions type + - tests: add a test about the extra bits that prepare-device can + specify for device registration + - tests: check that gpio device nodes are exported after reboot + - tests: parameterize core channel with env var for classic too + - many: rename variable "ss" to "snapsup" or "snapst" or "st" + (depending on context) + - tests: do not use external snaps in spread + - store: retry buy request + - store: retry store.Find + - store: retry assertion store call + - store: retry call for snap details + - many: use snap.ConfinementType rather than bool devmode + - daemon: if a bad snap is posted it is not an internal error but a + bad request + - client: add "Snap.Screenshots" to the client API + - interfaces: update base declaration documentation and policy for + on-classic and snap-type + - store: check payment method before TOS for a better UX + - interfaces: allow sched_setaffinity in process-control + - tests: parameterize core channel with env var + - tests: ensure that the XDG_ env contains at least XDG_RUNTIME_DIR + - interfaces: fcitx also listens on the session bus for Qt apps + - store: retry ListRefresh + - snap: use "Password of :" in the `snap login` + - many: reshuffle how we load/inject tests keys so image doesn't + need assertstate anymore + - store: use range requests if we have a local file already + - dirs,interfaces,overlord,snap,snapenv,test: export per-snap + XDG_RUNTIME_DIR per user + - osutil: make RealUser only look at SUDO_USER when uid==0 + - tests: do not use the ppa:snappy-dev/image in the tests + - store: retry readyToBuy request + - tests: increase `expect` timeouts + - static tests: add spell check + - tests: add debug to all flaky expect tests + - systemd: correct the mount arguments when mounting with squashfuse + - interfaces: add avahi-observe + - store: bring delta downloads back + - interfaces: add alsa + - interfaces/builtin: fix a broken test that snuck into master + - osutil: add chattr funcs + - image: init "snap_mode" on image creation time to avoid ugly + messages + - tests: test-snapd-fuse-consumer needs python-fuse as a build- + package + - interfaces/builtin: add i2c interface + - interfaces: add ofono interface + - tests: do not use hello-world in our tests + - snap: add support for classic confinement + - interfaces: remove LegacyAutoConnect() from the interfaces + - interfaces: miscellaneous policy updates + - tests: run autopkgtests in the autopkgtest.ubuntu.com + infrastructure + - Implement lxd-client interface exposing the lxd snap + - asserts: validate optional account username + - many: remove unnecessary snap name parameter from buying endpoint + - tests: do not hardcode the size of /dev/ram0 + - tests: add test that ensures the right content for /etc/os-release + - spread tests: fix snap mode check + - docs: fix path for source files location in HACKING.md + - interfaces/builtin/mir: allow slot to make recvfrom syscalls + - store: sections/featured snaps store support + + -- Michael Vogt Thu, 24 Nov 2016 19:43:08 +0100 + +snapd (2.17.1) xenial; urgency=medium + + * New upstream release, LP: #1637215: + - release: os-release on core has changed + - tests: /dev/ptmx does not work on powerpc, skip here + - docs: moved to github.com/snapcore/snapd/wiki (#2258) + - debian: golang is not installable on powerpc, use golang-any + + -- Michael Vogt Fri, 04 Nov 2016 18:13:10 +0200 + +snapd (2.17) xenial; urgency=medium + + * New upstream release, LP: #1637215: + - overlord/ifacestate: add unit tests for undo of setup-snap- + security (#2243) + - daemon,overlord,snap,tests: download to .partial in final dir + (#2237) + - overlord/state: marshaling tests for lanes (#2245) + - overlord/state: introduce state lanes (#2241) + - overlord/snapstate: fix revert+refresh (#2224) + - interfaces/sytemd: enable/disable generated service units (#2229) + - many: fix incorrect security files generation on undo + - overlord/snapstate: add dynamic snapdX.Y assumes (#2227) + - interfaces: network-manager: give slot full read-write access to + /run/NetworkManager + - docs: update the name of the command for the cross-build + - overlord/snapstate: fix missing argument to Noticef + - snapstate: ensure gadget/core/kernel can not be disabled (#2218) + - asserts: limit to 1y only if len(models) == 0 (#2219) + - debian: only install share/locale if available (missing on + powerpc) + - overlrod/snapstate: fix revert followed by refresh to old-current + (#2214) + - interfaces/builtin: network-manager and bluez can change hostname + (#2204) + - snap: switch the auto-import dir to /run/snapd/auto-import + - docs: less details about cloud.cfg as requested in trello (#2206) + - spread.yaml: Ensure ubuntu user has passwordless sudo for + autopkgtests (#2201) + - interfaces/builtin: add dcdbas-control interface + - boot: do not set boot to try mode if the revision is unchanged + - interfaces: add shutdown interface (#2162) + - interfaces: add system-power-control interface + - many: use the new systemd backend for configuring GPIOs + - overlord/ifacestate: setup security for slots before plugs + - snap: spool assertion candidates if snapd is not up yet + - store,daemon,overlord: download things to a partials dir + - asserts,daemon: implement system-user-authority header/concept + - interfaces/builtin: home base declaration rule using on-classic + for its policy + - interfaces/builtin: finish decl based checks + - asserts: bump snap-declaration to allow signing with new-style + plugs and slots + - overlord: checks for kernel installation/refresh based on model + assertion and previous kernel + - tests/lib/fakestore: fix logic to distinguish assertion not found + errors + - client: add a few explicit error types (around the request cycle) + - tests/lib/fakestore/cmd/fakestore: make it log, and fix a typo + - overlord/snapstate: two bugs for one + - snappy: disable auto-import of assertions on classic (#2122) + - overlord/snapstate: move trash cleanup to a cleanup handler + (#2173) + - daemon: make create-user --known fail on classic without --force- + managed (#2123) + - asserts,interfaces/policy: implement on-classic plug/slot + constraints + - overlord: check that the first installed gadget matches the model + assertion + - tests: use the snapd-control-consumer snap from the store + - cmd/snap: make snap run not talk to snapd for finding the revision + - snap/squashfs: try to hard link instead of copying. Also, switch + to osutil.CopyFile for cp invocation. + - store: send supported max-format when retrieving assertions + - snapstate, devicestate: do not remove seed + - boot,image,overlord,partition: read/write boot variables in single + operation + - tests: reenable ubuntu-core tests on qemu + - asserts,interfaces/policy: allow OR-ing of subrule constraints in + plug/slot rules + - many: move from flags as ints to flags as structs-of-bools (#2156) + - many: add supports for keeping and finding assertions with + different format iterations + - snap: stop using ubuntu-core-launcher, use snap-confine + - many: introduce an assertion format iteration concept, refuse to + add unsupported assertion + - interfaces: tweak wording and comment + - spread.yaml: dump apparmor denials on spread failure + - tests: unflake ubuntu-core-reboot (#2150) + - cmd/snap: tweak unknown command error message (#2139) + - client,daemon,cmd: add payment-declined error kind (#2107) + - cmd/snap: update remove command help (#2145) + - many: removed frameworks target and fixed service files (#2138) + - asserts,snap: validate attributes to a JSON-compatible type subset + (#2140) + - asserts: remove unused serial-proof type + - tests: skip auto-import tests on systems without test keys (#2142) + - overlord/devicestate: don't spam the debug log on classic (#2141) + - cmd/snap: simplify auto-import mountinfo parsing (#2135) + - tests: run ubuntu-core upgrades on isolated machine (#2137) + - overlord/devicestate: recover seeding from old external approach + (#2134) + - overlord: merge overlord/boot pkg into overlord/devicestate + (#2118) + - daemon: add postCreateUserSuite test suite (#2124) + - tests: abort tests if an update process is scheduled (#2119) + - snapstate: avoid reboots if nothing in the boot setup has changed + (#2117) + - cmd/snap: do not auto-import from loop or non-dev devices (#2121) + - tests: add spread test for `snap auto-import` (#2126) + - tests: add test for auto-mount assertion import (#2127) + - osutil: add missing unit tests for IsMounted (#2133) + - tests: check for failure creating user on managed ubuntu-core + systems (#2096) + - snap: ignore /dev/loop addings from udev (#2111) + - tests: remove snapd.boot-ok reference (#2109) + - tests: enable tests related to the home interface in all-snaps + (#2106) + - snapstate: only import defaults from gadget on install (#2105) + - many: move firstboot code into the snapd daemon (#2033) + - store: send correct JSON type of string for expected payment + amount (#2103) + - cmd/snap: rename is-managed to managed and tune (#2102) + - interfaces,overlord/ifacestate: initial cleaning up of no arg + AutoConnect related bits (#2090) + - client, cmd: prompt for password when buying (#2086) + - snapstate: fix hanging `snap remove` if snap is no longer mounted + - image: support gadget specific cloud.conf file (#2101) + - cmd/snap,ctlcmd: fix behavior of snap(ctl) get (#2093) + - store: local users download from the anonymous url (#2100) + - docs/hooks.md: fix typos (#2099) + - many: check installation of slots and plugs against declarations + - docs: fix missing "=" in the systemd-active docs + - store: do not set store auth for local users (#2092) + - interfaces,overlord/ifacestate: use declaration-based checking for + auto-connect (#2071) + - overlord, daemon, snap: support gadget config defaults (#2082)The + main semantic changes are: + - tests: fix snap-disconnect tests after core rename (#2088) + - client,daemon,overlord,cmd: add /v2/users and create-user on auto- + import (#2074) + - many: abbreviated forms of disconnect (#2066) + - asserts: require lowercase model until insensitive matching is + ready (#2076) + - cmd/snap: add version command, same as --version (#2075) + - all: use "core" by default but allow "ubuntu-core" still (#2070) + - overlord/devicestate, docs/hooks.md: nest prepare-device + configuration options + - daemon: fix login API to return local macaroons (#2078) + - daemon: do not hardcode UID in userLookup (#2080) + - client, cmd: connect fixes (#2026) + - many: preparations for switching most of autoconnect to use the + declarationsfor now: + - overlord/auth: update CheckMacaroon to verify local snapd + macaroons (#2069) + - cmd/snap: trivial auto-import and download tweaks (#2067) + - interfaces: add repo.ResolveConnect that handles name resolution + - interfaces/policy: introduce InstallCandidate and its checks + - interfaces/policy,overlord: check connection requests against the + declarations in ifacestate + - many: setup snapd macaroon for local users (#2051)Next step: do + snapd macaroons verification. + - interfaces/policy: implement snap-id/publisher-id checks + - many: change Connect to take ConnRef instead of strings (#2060) + - snap: auto mount block devices and import assertions (#2047) + - daemon: add `snap create-user --force-managed` support (#2041) + - docs: remove references to removed buying features (#2057) + - interfaces,docs: allow sharing SNAP{,_DATA,_COMMON} via content + iface (#2063) + - interfaces: add Plug/Slot/Connection reference helpers (#2056) + - client,daemon,cmd/snap: improve create-user APIs (#2054) + - many: introduce snap refresh --ignore-validation to + override refresh validation (#2052) + - daemon: add support for `snap create-user --known` (#2040) + - interfaces/policy: start of interface policy checking code based + on declarations (#2050) + - overlord/configstate: support nested configuration (#2039) + - asserts,interfaces/builtin,overlord/assertstate: introduce base- + declaration (#2037) + - interfaces: builtin: Allow writing DHCP lease files to + /run/NetworkManager/dhcp (#2049) + - many: remove all traces of the /v2/buy/methods endpoint (#2045) + - tests: add external spread backend (#1918) + - asserts: parse the slot rules in snap-declarations (#2035) + - interfaces: allow read of /etc/ld.so.preload by default for armhf + on series 16 (#2048) + - store: change purchase to order and store clean up first pass + (#2043) + - daemon, store: switch to new store APIs in snapd (#2036) + - many: add email to UserState (#2038) + - asserts: support parsing the plugs stanza i.e. plug rules in snap- + declarations (#2027) + - store: apply deltas if explicitly enabled (#2031) + - tests: fix create-key/snap-sign test isolation (#2032) + - snap/implicit: don't restrict the camera iface to classic (#2025) + - client, cmd: change buy command to match UX document (#2011) + - coreconfig: nuke it. Also, ignore po/snappy.pot. (#2030) + - store: download deltas if explicitly enabled (#2017) + - many: allow use of the system user assertion with create-user + (#1990) + - asserts,overlord,snap: add prepare-device hook for device + registration (#2005) + - debian: adjust packaging for trusty/deputy systemd (#2003) + - asserts: introduce AttributeConstraints (#2015) + - interface/builtin: access system bus on screen-inhibit-control + - tests: add firewall-control interface test (#2009) + - snapstate: pass errors from ListRefresh in updateInfo (#2018) + - README: add links to IRC, mailing list and social media (#2022) + - docs: add `configure` hook to hooks list (#2024)LP: #1596629 + - cmd/snap,configstate: rename apply-config variables to configure. + (#2023) + - store: retry download on 500 (#2019) + - interfaces/builtin: support time and date settings via + 'org.freedesktop.timedate1 (#1832) + + -- Michael Vogt Wed, 02 Nov 2016 01:17:36 +0200 + +snapd (2.16) xenial; urgency=medium + + * New upstream release, LP: #1628425 + - overlord/state: prune old empty changes + - interfaces: ppp: load needed kernel module (#2007) + - interfaces/builtin: add missing rule to allow run-parts to + execute all resolvconf scripts + - many: rename apply-config hook to configure + - tests: use new spread `debug` feature + - many: finish `snap set` API. + - overlord: fix and simplify configstate.Transaction + - assertions: add system-user assertion + - snap: add `snap known --remote` + - tests: replace systemd-run with on-the-fly generation of units. + - overlord/boot: switch to using assertstate.Batch + - snap, daemon, store: pass through screenshots from store + - image: add meta/gadget.yaml infrastructure + - tests: add test benchmark script + - daemon: add the actual ssh keys that got added to the create-user + response + - daemon: add REST API behind `snap get` + - debian: re-add golang-github-gosexy-gettext-dev + - tests: added install_local function + - interfaces/builtin: fix resolvconf permissions for network-manager + interface + - tests: use apt as compatible with trusty + - many: discard preserved namespace after removing snap + - daemon, overlord, store: add ReadyToBuy API to snapd + - many: add support for installing/removing multiple snaps + - progress: use New64 and fix output newline + - interfaces/builtin: allow network-manager to access netplan conf + files + - tests: build once and install test snap from cache + - overlord/state: introduce cleanup support + - snap: move/clarify Info.Broken + - ctlcmd: add snapctl get. + - overlord,store: clean up serial-proof plumbing code + - interfaces/builtin: add network-setup-observe interface + - daemon,overlord/assertstate: support streams of assertions with + snap ack + - snapd: kmod backend + - tests: ensure HOME is also set correctly + - configstate,hookstate: add snapctl set + - tests: disable broken create-key test + - interfaces: adjust bluetooth-control to allow getsockopt (LP: + #1613572) + - tests: add a test for core about device initialization and device + registration and auth + - many: show snap name before the download progress bar + - interfaces/builtin: add rcvfrom for client connected plugs to mir + interface + - asserts: support for maps in assertions + - tests: increase timeout for key generation in create-key test + - many: validate refreshes against validation assertions by gating + snaps + - interfaces/apparmor: allow 'm' in default policy for snap-exec + - many: avoid snap.InfoFromSnapYaml in tests + - interfaces/builtin: allow /dev/net/tun with network-control + - tests: add spread test for snap create-key/snap sign + - tests: add missing quotes in security-device-cgroups/task.yaml + - interfaces: drop ErrUnknownSecurity + - store: add "ready to buy" method + - snap/snapenv, tests: use root's data dirs when running via sudo + - interfaces/builtin: add initial docker interface + - snap: remove extra newline after progress is done + - docs: fix formating of HACKING.md "Testing snapd" + - store : add requestOptions.ExtraHeaders so that individual + requests can customise headers. + - many: use unique plug/slot names in tests + - tests: add tests for the classic dimension + - many: add vendoring of dependencies by default + - tests: use in-tree snap{ctl,-exec} for all tests + - many: support snapctl -h + - tests: adjust regex after changes in stat output + - store,snap: initial support for delta downloads + - interfaces/builtin: add run/udev/data paths to mir interface + - snap: lessen annoyance of implicit interface tests + - tests: ensure http{,s}_proxy is defined inside the fake-store + - interfaces: allow xdg-open in unity7, unity7 cleanups + - daemon,store: move store login user logic to store + - tests: replace realpath with readlink -f for trusty support. + - tests: add https_proxy into environment as well + - interfaces/builtin: allow mmaping pulseaudio buffers + + -- Michael Vogt Wed, 28 Sep 2016 11:09:27 +0200 + +snapd (2.15.2ubuntu1) xenial; urgency=medium + + * New upstream release, LP: #1623579 + - snap/snapenv, tests: use root's data dirs when running via sudo + (cherry pick PR: #1857) + - tests: add https_proxy into environment + (cherry pick PR: #1926) + - interfaces: allow xdg-open in unity7, unity7 cleanups + (cherry pick PR: #1946) + - tests: ensure http{,s}_proxy is defined inside the fake-store + (cherry pick PR: #1949) + + -- Michael Vogt Wed, 21 Sep 2016 17:21:12 +0200 + +snapd (2.15.2) xenial; urgency=medium + + * New upstream release, LP: #1623579 + - asserts: define a bit less terse Ref.String + - interfaces: disable auto-connect in libvirt interface + - asserts: check that validation assertions are signed by the + publisher of the gating snap + + -- Michael Vogt Mon, 19 Sep 2016 10:42:29 +0200 + +snapd (2.15.1) xenial; urgency=medium + + * New upstream release, LP: #1623579 + - image: ensure local snaps are put last in seed.yaml + - asserts: revert change that made the account-key's name mandatory. + - many: refresh all snap decls + - interfaces/apparmor: allow reading /etc/environment + + -- Michael Vogt Mon, 19 Sep 2016 09:19:44 +0200 + +snapd (2.15) xenial; urgency=medium + + * New upstream release, LP: #1623579 + - tests: disable prepare-image-grub test in autopkgtest + - interfaces: allow special casing for auto-connect until we have + assertions + - docs: add a little documentation on hooks. + - hookstate,daemon: don't mock HookRunner, mock command. + - tests: add http_proxy to /etc/environment in the autopkgtest + environment + - backends: first bits of kernel-module security backend + - tests: ensure openssh-server is installed in autopkgtest + - tests: make ubuntu-core tests more robust + - many: mostly work to support ABA upgrades + - cmd/snap: do runtime linting of descriptions + - spread.yaml: don't assume LANG is set + - snap: fix SNAP* environment merging in `snap run` + - CONTRIBUTING.md: remove integration-tests, include spread + - store: don't discard error body from request device session call + - docs: add create-user documentation + - cmd/snap: match UX document for message when buying without login + - firstboot: do not overwrite any existing netplan config + - tests: add debug output to ubuntu-core-update-rollback- + stresstest: + - tests/lib/prepare.sh: test that classic does not setting bootvars + - snap: run all tests with gpg2 + - asserts: basic support for validation assertion and refresh- + control + - interfaces: miscellaneous policy updates for default, browser- + support and camera + - snap: (re)add --force-dangerous compat option + - tests: ensure SUDO_{USER,GID} is unset in the spread tests + - many: clean out left over references to integration tests + - overlord/auth,store: fix raciness in updating device/user in state + through authcontext and other issuesbonus fixes: + - tests: fix spread tests on yakkety + - store: refactor auth/refresh tests + - asserts: use gpg --fixed-list-mode to be compatible with both gpg1 + and gpg2 + - cmd/snap: i18n option descriptions + - asserts: required account key name header + - tests: add yakkety test host + - packaging: make sure debhelper-generated snippet is invoked on + postrm + - snap,store: capture newest digest from the store, make it + DownloadInfo only + - tests: add upower-observe spread test + - Merge github.com:snapcore/snapd + - tests: fixes to actually run the spread tests inside autopkgtest + - cmd/snap: make "snap find" error nicer. + - tests: get the gadget name from snap list + - cmd/snap: tweak help of 'snap download' + - cmd/snap,image: teach snap download to download also assertions + - interfaces/builtin: tweak opengl interface + - interfaces: serial-port use udevUsbDeviceSnippet + - store: ensure the payment methods method handles auth failure + - overlord/snapstate: support revert flags + - many: add snap configuration to REST API + - tests: use ubuntu-image for the ubuntu-core-16 image creation + - cmd/snap: serialise empty keys list as [] rather than null + - cmd/snap,client: add snap set and snap get commands + - asserts: update trusted account-key asserts with names + - overlord/snapstate: misc fixes/tweaks/cleanups + - image: have prepare-image set devmode correctly + - overlord/boot: have firstboot support assertion files with + multiple assertions + - daemon: bail from enable and disable if revision given, and from + multi-op if unsupported optons given + - osutil: call sync after cp if + requested.overlord/snapstate/backend: switch to use osutil instead + of another buggy call to cp + - cmd/snap: generate account-key-request "since" header in UTC + - many: use symlinks instead of wrappers + - tests: remove silly [Service] entry from snapd.socket.d/local.conf + - store: switch device session to use device-session-request + assertion + - snap: ensure that plug and slot names are unique + - cmd/snap: fix test suite (no Exit(0) on tests!) + - interfaces: add interface for hidraw devices + - tests: use the real model assertion when creating the core test + image + - interfaces/builtin: add udisks2 and removable-media interfaces + - interface: network_manager: enable resolvconf + - interfaces/builtin: usb serial-port support via udev + - interfaces/udev: support noneSecurityTag keyed snippets + - snap: switch to the new agreed regexp for snap names + - tests: adjust test setup after ubuntu user removal + - many: start services only after the snap is fully ready (link-snap + was run) + - asserts: don't have Add/Check panic in the face of unsupported no- + authority assertions + - asserts: initial support to generate/sign snap-build assertions + - asserts: support checking account-key-request assertions + - overlord: introduce AuthContext.DeviceSessionRequest with support + in devicestate + - overlord/state: fix for reloaded task/change crashing on Set if + checkpointed w. no custom data yet + - snapd.refresh.service: require snap.socket and /snap/*/current. + - many: spell --force-dangerous as just --dangerous, devmode should + imply it + - overlord/devicestate: try to fetch/refresh the signing key of + serial (also in case is not there yet) + - image,overlord/boot,snap: metadata from asserts for image snaps + - many: automatically restart all-snap devices after os/kernel + updates + - interfaces: modem-manager: ignore camera + - firstboot: only configure en* and eth* interfaces by default + - interfaces: fix interface handling on no-app snaps + - snap: set user variables even if HOME is unset (like with systemd + services) + + -- Michael Vogt Fri, 16 Sep 2016 07:46:22 +0200 + +snapd (2.14.2~16.04) xenial; urgency=medium + + * New upstream release: LP: #1618095 + - tests: use the spread tests with the adhoc interface inside + autopkgtest + - interfaces: add fwupd interface + - asserts,cmd/snap: add "name" header to account-key(-request) + - client,cmd/snap: display os-release data only on classic + - asserts/tool,cmd/snap: introduce hidden "snap sign" + - many: when installing snap file derive metadata from assertions + unless --force-dangerous + - osutil: tweak the createUserTests a bit and extract common code + - debian: umount --lazy before rm on snapd.postrm + - interfaces: updates to default policy, browser-support, and x11 + - store: set initial device session + - interfaces: add upower-observe interface (LP: #1595813) + - tests: use beta u-d-f in test by default + - interfaces/builtin: allow writing on /dev/vhci in bluetooth- + control + - interfaces/builtin: allow /dev/vhci on bluetooth-control + - tests: port integration tests to spread + - snapstate: use umount --lazy when removing the mount units + - spread: enable halt-timeout, tweak image selection + - tests: fix firstboot-assertions to actually be runnable on classic + again + - asserts: introduce device-session-request + - interfaces: add screen-inhibit-control interface (LP: #1604880) + - firstboot: change location of netplan config + - overlord/devicestate: some cleanups and solving a couple todos + - daemon,overlord: add subcommand handling to snapctl + + -- Michael Vogt Thu, 01 Sep 2016 18:52:05 +0200 + +snapd (2.14.1) xenial; urgency=medium + + * New upstream release: LP: #1618095 + - snap-exec: add support for commands with internal args in snap- + exec + - store: refresh expired device sessions + - debian: re-add ubuntu-core-snapd-units as a transitional package + - image: snap assertions into image + - overlord/assertstate,asserts/snapasserts: give snap assertions + helpers a package, introduce ReconstructSideInfo + - docs/interfaces: Add empty line after lxd-support title + - README: cover the new /run/snapd-snap.socket + - daemon: make socket split backward-compatible. + + -- Michael Vogt Tue, 30 Aug 2016 16:43:29 +0200 + +snapd (2.14) xenial; urgency=medium + + * New upstream release: LP: #1618095 + - cmd: enable SNAP_REEXEC only if it is set to SNAP_REEXEC=1 + - osutil: fix create-user on classic + - firstboot: disable firstboot on classic for now + - cmd/snap: add export-key --account= option + - many: split public snapd REST API into separate socket. + - many: drop ubuntu-core-snapd-units package, use release.OnClassic + instead + - tests: add content-shareing binary test that excersises snap- + confine + - snap: use "up to date" instead of "up-to-date" + - asserts: add an account-key-request assertion + - asserts: fix GPG key generation parameters + - tests, integration-tests: implement the cups-control manual test + as a spread test + - many: clarify/tie down model assertion + - cmd/snap: add "snap download" command + - integration-tests: remove them in favour of the spread tests + - tests: test all snap ubuntu core upgrade + - many: support install and remove by revision + - overlord/state: prevent change ready => unready + - tests: fixes to make the ubuntu-core-16 image usable with + -keep/-reuse + - asserts: authority-id and brand-id of serial must match + - firstboot: generate netplan config rather than ifupdown + - store: request device session macaroon from store + - tests: add workaround for u-d-f to unblock all-snap image tests + - tests: the stable ubuntu-core snap has snap run support now + - many: use make StripGlobalRootDir public + - asserts: add some stricter checks around format + - many: have AuthContext expose device store-id, serial and serial- + proof signing to the store + - tests: fix "tests/main/ack" to not break if asserts are alreay + there + - tests/main/ack: fix test/style + - snap: add key management commands + - firstboot: add firstboot assertions importing + + -- Michael Vogt Mon, 29 Aug 2016 17:07:20 +0200 + +snapd (2.13) xenial; urgency=medium + + * New upstream release: LP: #1616157 + - many: respect dirs.SnapSnapsDir in tests + - tests: update listing test for latest stable image + - many: hook in start of code to fetch/check assertions when + installing snap from store + - boot: add missing udevadm mock to fix FTBFS + - interfaces: add lxd-support interface + - dirs,snap: handle empty root directory in SetRootDir + - dirs,snap: define methods for SNAP_USER_DATA and SNAP_USER_COMMON + - tests: spread all-snap test cleanup + - tests: add all-snap spread image tests + - store,tests: have just one envvar SNAPPY_USE_STAGING_STORE to + control talking to staging + - overlord/hookstate: use snap run posix parameters. + - interfaces/builtin: allow bind in the network interface + - asserts,overlord/devicestate: simplify private key/key pairs APIs, + they take just key ids + - dependencies: update godeps + - boot: add support for "devmode: {true,false}" in seed.yaml + - many: teach prepare-image to copy the model assertion (and + prereqs) into the seed area of the image + - tests: start teaching the fakestore about assertions + - asserts/sysdb: embed the new format official root/trusted + assertions + - overlord/devicestate: first pass at device registration logic + - tests: add process-control interface spread test + - tests: disable unity test + - tests: adapt to new spread version + - asserts: add serial-proof device assertion + - client, cmd/snap: use the new multi-refresh endpoint + - many: preparations for image code to fetch model prereqs + - debian: add extra checks when debian/snapd.postrm purge is run + - overlord/snapstate, daemon: support for multi-snap refresh + - tests: do not leave "squashfs-root" around + - snap-exec: Fix broken `snap run --shell` and add test + - overlord/snapstate: check changes to SnapState for conflicts also. + - docs/interfaces: change snappy command to snap + - tests: test `snap run --hook` using in-tree snap-exec. + - partition: ensure that snap_{kernel,core} is not overridden with an + empty value + - asserts,overlord/assertstate: introduce an assertstate task + handler to fetch snap assertions + - spread: disable re-exec to always test development tree. + - interfaces: implement a fuse interface + - interfaces/hardware-observe.go: re-add /run/udev/data + - overlord/assertstate,daemon: reorg how the assert manager exposes + the assertion db and adding to it + - release: Remove "UBUNTU_CODENAME" from the test data + - many: implement snapctl command. + - interfaces: mpris updates (fix unconfined introspection, add name + attribute) + - asserts: export DecodePublicKey + - asserts: introduce support for assertions with no authority, + implement serial-request + - interfaces: bluez: add a few more tests to verify interface + connection works + - interfaces: bluez: add missing mount security snippet case + - interfaces: add kernel-module interface for module insertion. + - integration-tests: look for ubuntu-device-flash on PATH before + calling sudo + - client, cmd, daemon, osutil: support --yaml and --sudoer flags for + create-user + - spread: use snap-confine from ppa:snappy-dev/image for the tests + - many: move to purely hash based key lookup and to new + key/signature format (v1) + - spread: Use /home/gopath in spread.yaml + - tests: base security spread tests + + -- Michael Vogt Wed, 24 Aug 2016 14:48:28 +0200 + +snapd (2.12) xenial; urgency=medium + + * New upstream release: LP: #1612362 + - many: do not require root for `snap prepare-image` + - tests: prevent restore error on test failure + - osutil: change escaping for create-user's sudoers + - docs: private flag doesn't exist on /v2/find (it's select) + - snap: do not sort the result of `snap find` + - interfaces/builtin: add gpio interface + - partition: fix cleaning of the boot variables on the second good + boot + - tests: add udev rules spread test + - docs: fix references to refresh action + - interfaces/udev,osutil: avoid doubled rules and put all in a per + snap file + - store: minor store improvements from previous reviews + - many: support interactive payments in snapd, filter from command + line + - docs/interfaces.md: improve interfaces documentation + - overlord,store: set store device authorization header + - store: add device nonce API support + - many: various fixes around the `create-user` command + - client, osutil: chown the auth file + - interfaces/builtin: add transitional browser-support interface + - snap: don't load unsupported implicit hooks. + - cmd/snap,cmd/snap-exec: support hooks again. + - interfaces/builtin: improve pulseaudio interface + - asserts: make account-key's `until` optional to represent a never- + expiring key + - store: refactor newRequest/doRequest to take requestOptions + - tests: allow-downgrades on upgrade test to prevent version errors + - daemon: stop using group membership as succedaneous of running + things with sudo + - interfaces: add bluetooth-control interfaces + - many: remove integration-test coverage metrics + - daemon,docs: drop license docs and error kind + - tests: add network-control interface spread test + - tests: add hardware-observe spread test + - interfaces: add system-trace interface LP: #1600085 + - boot: use `cp -aLv` instead of `cp -a` (no symlinks on vfat) + - store: soft-refresh discharge macaroon from store when required + - partition: clear snap_try_{kernel,core} on success + - tests: add snapd-control interface spread test + - tests: add locale-control write spread test + - store: fix buy method after some refactoring broke it + - interfaces/builtin: read perms for network devices in network- + observe + - interfaces: also allow rfkill in network_control + - snapstate: remove artifacts from a snap try dir that vanished + - client, cmd/snap: better errors for empty snap list result + - wrappers: set BAMF_DESKTOP_FILE_HINT for unity + - many: cleanup/update rest.md; improve auth errors + - interfaces: miscelleneous policy updates for default, log-observe, + mount-observe, opengl, pulseaudio, system-observe and unity7 + - interfaces: add process-control interface (LP: #1598225) + - osutil: support both "nobody" and "nogroup" for grpnam tests + - cmd: support defaulting to the user's preferred payment method + - overlord: actually run hooks. + - overlord/state,overlord/ifacestate: define basic infrastructure + for and then setting up serialising of interface mgr tasks + - asserts: add Assertion.Prerequisites and SigningKey, Ref and + FindTrusted + - overlord/snapstate: ensure calls to store are done without the + state lock held + - asserts,client: switch snap-build and snap-revision to be indexed + by snap-sha3-384 + - many: make seed.yaml on firstboot mandatory and include sideInfo + - asserts,many: start supporting structured headers using the new + parseHeaders + - many: update code for the new snap_mode + - tests: added spread find private test + - store: deal with 404 froms the SSO store properly + - snap: remove meta/kernel.yaml again + - daemon: always mock release info in tests + - snapstate: drop revisions after "current" on refresh + - asserts: introduce new parseHeadersThis introduces the new + parseHeaders returning map[string]interface{} and capable of + accepting: + - asserts: remove/disable comma separated lists and their uses + + -- Michael Vogt Thu, 11 Aug 2016 19:30:36 +0200 + +snapd (2.11) xenial; urgency=medium + + * New upstream release: LP: #1605303 + - increase version number to reflect the nature of the update + better + - store, daemon, client, cmd/snap, docs/rest.md: adieu search + grammar + - debian: move snapd.refresh.timer into timers.target + - snapstate: add daemon-reload to fix autopkgtest on yakkety + - Interfaces: hardware-observe + - snap: rework the output after a snap operation + - daemon, cmd/snap: refresh --devmode + - store, daemon, client, cmd/snap: implement `snap find --private` + - tests: add network-observe interface spread test + - interfaces/builtin: allow getsockopt for connected x11 plugs + - osutil: check for nogrup instead of adm + - store: small cleanups (more needed) + - snap/squashfs: fix test not to hardcode snap size + - client,cmd/snap: cleanup cmd/snap test suite, add extra args + testThis cleans up the cmd/snap test suite: + - wrappers: map "never" restart condition to "no." + - wrappers: run update-desktop-database after add/remove of desktop + files + - release: work around elementary mistake + - many: remove all traces of channel from the buying codepath + - store: kill setUbuntuStoreHeaders + - docs: add payment methods documentation + - many: present user with a choice of payment backends + - asserts: add cross checks for snap asserts + - cmd/snap,cmd/snap-exec: support running hooks via snap-exec. + - tests: improve snap run symlink tests + - tests: add content sharing interface spread test + - store & many: a mechanical branch shortening store names + - snappy: remove old snappy pkg + - overlord/snapstate: kill flagscompat + - overlord/snapstate, daemon, client, cmd/snap: devmode override + (aka confined) + - tests: extend refresh test to talk to the staging and production + stores + - asserts,daemon: cross checks for account and account-key + assertions + - client: existing JSON fixtures uses tabs for indentation + - snap-exec: add proper integration test for snap-exec + - spread.yaml, tests: replace hello-world with test-snapd-tools + - tests: add locale-control interface spread test + - tests: add mount-observe interface spread test + - tests: add system-observe interface spread test + - many: add AuthContext to mediate user updates to the state + - store/auth: add helper for the macaroon refresh endpoint + - cmd: add buy command + - overlord: switch snapstate.Update to use ListRefresh (aka + /snaps/metadata) + - snap-exec: fix silly off-by-one error + - tests: stop using hello-world.echo in the tests + - tests: add env command to test-snapd-tools + - classic: remove (most of) "classic" mode, this is implemented as a + snap now + - many: remove snapstate.Candidate and other cleanups + - many: removed authenticator, store gets a user instead + - asserts: fix minor doc comment typo + - snap: ensure unknown arguments to `snap run` are ignored + - overlord/auth: add Device/SetDevice to persist device identity in + state + - overlord: make SyncBoot work again + - tests: add -y flag to apt autoremove command in unity task restore + - many: migrate SnapSetup and SideInfo to use RealName + - daemon: drop auther() + - client: improve error from client.do() on json decode failures + - tests: readd the fake store tests + - many: allow removal of broken snaps, add spread test + - overlord: implement &Retry{After: duration} support for handlers + - interface: add new interfaces.all.SecurityBackends + - integration-tests: remove login tests + - cmd,interfaces,snap: implement hook whitelist. + - daemon,overlord/auth,store: update macaroon authentication to use + the new endpoints + - daemon, overlord: add buy endpoint to REST API + - tests: use systemd-run for starting and stopping the unity app + - tests, integration-tests: port systemd service check test to + spread + - store: switch search to new snap-specific endpoint + - store, many: start using the new details endpoint + - tests, integration-tests: port unity test to spread + - tests: add spread test for tried snaps removal + - tests, integration-tests: port auth errors test to spread + - snapstate: rename OfficialName to RealName in the new tests + - many: rename SideInfo.OfficialName to SideInfo.RealName + - snapstate: use snapstate.Type in backend.RemoveSnapFiles + - many: add `snap enable/disable` commands + - tests, integration-tests: port refresh all test to spread + - snap: add `snap run --shell` + - tests: set yaml indentation to 4 spaces + - snapstate: cleanup downloaded temp snap files + - overlord: make patch1_test more robust + - debian: add snapd.postrm that purges + - integration-tests: drop already covered refresh app test + - many: add concept of "broken" snaps + - tests, integration-tests: port remove errors tests to spread + - tests, integration-tests: port revert test to spread + - debian: fix snapbuild path + - overlord: fix access to the state without lock in firstboot.go and + add test + - snapstate: add very simple garbage collection on upgrade + - asserts: introduce assertstest with helpers to test code involving + assertions + - tests, integration tests: port undone failed install test to + spread + - snap,store: switch to the new snaps/metadata endpoint, introduce + and start capturing DeveloperID + - tests, integration-tests: port the op remove retry test to spread + - po: remove snappy.pot from git, it will be generated at build time + - many: add some missing tests, clarify some things and nitpicks as + follow up to `snap revert` + - snapstate: when doing snapsate.Update|Install, talk to the store + early + - tests, integration-tests: port the op remove test to spread + - interfaces: allow /usr/bin/locale in default policy + - many: add `snap revert` + - overlord/auth,store: add macaroon serialization/deserialization + helpers + - many: embed main store trusted assertions in snapd, way to have + test ones, spread tests for ack and known + - overlord/snapstate,daemon: clarify active vs current, add + SnapState.HasCurrent,CurrentInfo + - tests: do not search for a specific snap (we hit 100 items) and + pagination kicks in + - tests: use printf instead of echo where we need portability + - tests: rename and generalize basic-binaries to test-snapd-tools + + -- Michael Vogt Tue, 26 Jul 2016 15:49:04 +0200 + +snapd (2.0.10) xenial; urgency=medium + + * New upstream release: LP: #1597329 + - interfaces: also allow @{PROC}/@{pid}/mountinfo and + @{PROC}/@{pid}/mountstats + - interfaces: allow read access to /etc/machine-id and + @{PROC}/@{pid}/smaps + - interfaces: miscelleneous policy updates for default, log-observe + and system-observe + - snapstate: add logging after a successful doLinkSnap + - tests, integration-tests: port try tests to spread + - store, cmd/snapd: send a basic user-agent to the store + - store: add buy method + - client: retry on failed GETs + - tests: actual refresh test + - docs: REST API update + - interfaces: add mount support for hooks. + - interfaces: add udev support for hooks. + - interfaces: add dbus support for hooks. + - tests, integration-tests: port refresh test to spread + - tests, integration-tests: port change errors test to spread + - overlord/ifacestate: don't retry snap security setup + - integration-tests: remove unused file + - tests: manage the socket unit when reseting state + - overlord: improve organization of state patches + - tests: wait for snapd listening after reset + - interfaces/builtin: allow other sr*/scd* optical devices + - systemd: add support for squashfuse + - snap: make snaps vanishing less fatal for the system + - snap-exec: os.Exec() needs argv0 in the args[] slice too + - many: add new `create-user` command + - interfaces: auto-connect content interfaces with the same content + and developer + - snapstate: add Current revision to SnapState + - readme: tweak readme blurb + - integration-tests: wait for listening port instead of active + service reported by systemd + - many: rename Current -> {CurrentSideInfo,CurrentInfo} + - spread: fix home interface test after suite move + - many: name unversioned data. + - interfaces: add "content" interface + - overlord/snapstate: defaultBackend can go away now + - debian: comment to remember why the timer is setup like it is + - tests,spread.yaml: introduce an upgrade test, support/split into + two suites for this + - overlord,overlord/snapstate: ensure we keep snap type in snapstate + of each snap + - many: rework the firstboot support + - integration-tests: fix test failure + - spread: keep core on suite restore + - tests: temporary fix for state reset + - overlord: add infrastructure for simple state format/content + migrations + - interfaces: add seccomp support for hooks. + - interfaces: allow gvfs shares in home and temporarily allow + socketcall by default (LP: #1592901, LP: #1594675) + - tests, integration-tests: port network-bind interface tests to + spread + - snap,snap/snaptest: use PopulateDir/MakeTestSnapWithFiles directly + and remove MockSnapWithHooks + - interfaces: add mpris interface + - tests: enable `snap run` on i386 + - tests, integration-tests: port network interface test to spread + - tests, integration-tests: port interfaces cli to spread + - tests, integration-tests: port leftover install tests to spread + - interfaces: add apparmor support for hooks. + - tests, integration-tests: port log-observe interface tests to + spread + - asserts: improve Decode doc comment about assertion format + - tests: moved snaps to lib + - many: add the camera interface + - many: add optical-drive interface + - interfaces: auto-connect home if running on classic + - spread: bump gccgo test timeout + - interfaces: use security tags to index security snippets. + - daemon, overlord/snapstate, store: send confinement header to the + store for install + - spread: run tests on 16.04 i386 concurrently + - tests,integration-tests: port install error tests to spread + - interfaces: add a serial-port interface + - tests, integration-tests, debian: port sideload install tests to + spread + - interfaces: add new bind security backend and refactor + backendtests + - snap: load and validate implicit hooks. + - tests: add a build/run test for gccgo in spread + - cmd/snap/cmd_login: Adjust message after adding support for wheel + group + - tests, integration-tests: ported install from store tests to + spread + - snap: make `snap change ` show task progress + - tests, integration-tests: port search tests to spread + - overlord/state,daemon: make abort proceed immediately, fix doc + comment, improve tests + - daemon: extend privileged access to users in "wheel" group + - snap: tweak `snap refresh` and `snap refresh --list` outputTiny + branch that does three things: + - interfaces: refactor auto-connection candidate check + - snap: add support for snap {install,refresh} + --{edge,beta,candidate,stable} + - release: don't force KDE Neon into devmode. + + -- Michael Vogt Wed, 29 Jun 2016 21:02:39 +0200 + +snapd (2.0.9) xenial; urgency=medium + + * New upstream release: LP: #1593201 + - snap: add the magic redirect part of `snap run` + - tests, integration-tests: port server related tests to spread + - overlord/snapstate: log restarting in the task + - daemon: test restart wiring, fix setup/teardown + - cmd: don't show the price if a snap has already been purchased + - tests, integration-tests: port listing tests to spread + - integration-tests: do not try to kill ubuntu-clock-app.clock (no + longer a process) + - several: tie up overlord's restart handler into daemon; adjust + snap to cope + - tests, integration-tests: port abort tests to spread + - integration-tests: fix flaky TestRemoveBusyRetries + - testutils: refactor/mock exec + - snap,cmd: add hook support to snap run. + - overlord/snapstate: remove Download from backend + - store: use a custom logging transport + - overlord/hookstate: implement basic HookManager. + - spread: move the suite restore to restore-each + - asserts: turn model os into model core field, making it also more + like the kernel and gadget fields + - asserts: / is not allowed in primary key headers, follow the store + in this + - release: enable full confinement on Elementary 0.4 + - integration-tests: fix another i386 autopkgtest failure. + - cmd/snap: create SNAP_USER_DATA and common dirs in `snap run` + - many: have the installation of the core snap request a restart (on + classic) + - asserts: allow to load also account assertions into the trusted + set + - many: install snaps in devmode on distributions without complete + apparmor and seccomp support + - spread: run on travis + - snapenv: do not hardcode amd64 in tests + - spread: initial harness and first test + - interfaces: miscelleneous policy updates for chromium, x86, + opengl, etc + - integration-tests: remove daemon to use the log-observe interface + - client: remove client.Revision and import snap.Revision instead + - integration-tests: wait for network-bind service in try test + - many: move over from snappy to snapstate/backend SetupSnap and + related code + - integration-tests: add interfaces cli tests + - snapenv: cleanup snapenv.{Basic,User} + - cmd/snap: also print slots that connect to the wanted snap (LP: + #1590704) + - asserts: error style, use "cannot" instead of "failed to" + following the main decided style + - integration-tests: wait until the network-bind service is up + before testing + - many: add new `snap run` command + - snappy: unexport snappy.Install and snappy.Overlord.{Un,}Install + - many: add some shared testing helpers to snap/snaptest and to + boot/boottest + - rest-api: support to send apps per snap (LP: #1564076) + + -- Michael Vogt Thu, 16 Jun 2016 13:56:12 +0200 + +snapd (2.0.8.1) UNRELEASED; urgency=medium + + * New upstream release + - Cherry pick four commits that show snaps as installed in devmode on + distributions without full confinement dependencies available: + + 25634d3364a46b5e9147e4466932c59b1b572d35 + 53f2e8d5f1b2d7ce13f5b50be4c09fa1de8cf1e0 + 38771f4cc324ad9dd4aa48b03108d13a2c361aad + c46e069351c61e45c338c98ab12689a319790bd5 + + -- Zygmunt Krynicki Tue, 14 Jun 2016 15:55:30 +0200 + +snapd (2.0.8) xenial; urgency=medium + + * New upstream release: LP: #1589534 + - debian: make `snap refresh` times more random (LP: #1537793) + - cmd: ExecInCoreSnap looks in "core" snap first, and only in + "ubuntu-core" snap if rev>125. + - cmd/snap: have 'snap list' display helper message on stderr + (LP: #1587445) + - snap: make app names more restrictive. + + -- Michael Vogt Wed, 08 Jun 2016 07:56:58 +0200 + +snapd (2.0.7) xenial; urgency=medium + + * New upstream release: LP: #1589534 + - debian: do not ship /etc/ld.so.conf.d/snappy.conf (LP: #1589006) + - debian: fix snapd.refresh.service install and usage (LP: #1588977) + - ovlerlord/state: actually support task setting themself as + done/undone + - snap: do not use "." import in revision_test.go, as this breaks + gccgo-6 (fix build failure on powerpc) + - interfaces: add fcitx and mozc input methods to unity7 + - interfaces: add global gsettings interfaces + - interfaces: autoconnect home and doc updates (LP: #1588886) + - integration-tests: remove + abortSuite.TestAbortWithValidIdInDoingStatus + - many: adding backward compatible code to upgrade SnapSetup.Flags + - overlord/snapstate: handle sideloading over an old sideloaded snap + without panicing + - interfaces: add socketcall() to the network/network-bind + interfaces (LP: #1588100) + - overlord/snapstate,snappy: move over CanRemoveThis moves over the + CanRemove check to snapstate itself.overlord/snapstate + - snappy: move over CanRemove + - overlord/snapstate,snappy: move over CopyData and Remove*Data code + + -- Michael Vogt Mon, 06 Jun 2016 16:35:50 +0200 + +snapd (2.0.6) xenial; urgency=medium + + * New upstream release: LP: #1588052: + - many: repository moved to snapcore/snapd + - debian: add transitional pkg for the github location change + - snap: ensure `snap try` work with relative paths + - debian: drop run/build dependency on lsb-release + - asserts/tool: gpg key pair manager + - many: add new snap-exec + - many: implement `snap refresh --list` and `snap refresh` + - snap: add parsing support for hooks. + - many: add the cups interface + - interfaces: misc policy fixes (LP: #1583794) + - many: add `snap try` + - interfaces: allow using sysctl and scmp_sys_resolver for parsing + kernel logs + - debian: make snapd get its environ from /etc/environment + - daemon,client,snap: revisions are now strings + - interfaces: allow access to new ibus abstract socket path + LP: #1580463 + - integration-tests: add remove tests + - asserts: stronger crypto choices and follow better latest designs + - snappy,daemon: hollow out more of snappy (either removing or not + exporting stuff on its way out), snappy/gadget.go is gone + - asserts: rename device-serial to serial + - asserts: rename identity to account (and username access) + - integration-tests: add changes tests + - backend: add tests for environment wrapper generation + - interfaces/builtin: add location-control interface + - overlord/snapstate: move over check snap logic from snappy + - release: use os-release instead of lsb-release for cross-distro + use + - asserts: allow empty snap-name for snap-declaration + - interfaces/builtin,docs,snap: add the pulseaudio interface + - many: add support for an environment map inside snap.yaml + - overlord/snapstate: increase robustness of doLinkSnap/undoLinkSnap + with sanity unit tests + - snap: parse epoch property + - snappy: do nothing in SetNextBoot when running on classic + - snap: validate snap type + - integration-tests: extend find command tests + - asserts: extend tests to cover mandatory and empty headers + - tests: stop the update-pot check in run-checks + - snap: parse confinement property. + - store: change applyUbuntuStoreHeaders to not take accept, and to + take a channel + - many: struct-based revisions, new representation + - interfaces: remove 'audit deny' rules from network_control.go + - interfaces: add com.canonical.UrlLauncher.XdgOpen to unity7 + interface + - interfaces: firewall-control can access xtables lock file + - interfaces: allow unity7 AppMenu + - interfaces: allow unity7 launcher API + - interfaces/builtin: add location-observe interface + - snap: fixed snap empty list text LP: #1587445 + + -- Michael Vogt Thu, 02 Jun 2016 08:23:50 +0200 + +snapd (2.0.5) xenial; urgency=medium + + * New upstream release: LP: #1583085 + - interfaces: add dbusmenu, freedesktop and kde notifications to + unity7 (LP: #1573188) + - daemon: make localSnapInfo return SnapState + - cmd: make snap list with no snaps not special + - debian: workaround for XDG_DATA_DIRS issues + - cmd,po: fix conflicts, apply review from #1154 + - snap,store: load and store the private flag sent by the store in + SideInfo + - interfaces/apparmor/template.go: adjust /dev/shm to be more usable + - store: use purchase decorator in Snap and FindSnaps + - interfaces: first version of the networkmanager interface + - snap, snappy: implement the new (minmimal) kernel spec + - cmd/snap, debian: move manpage generation to depend on an environ + key; also, fix completion + + -- Michael Vogt Thu, 19 May 2016 15:29:16 +0200 + +snapd (2.0.4) xenial; urgency=medium + + * New upstream release: + - interfaces: cleanup explicit denies + - integration-tests: remove the ancient integration daemon tests + - integration-tests: add network-bind interface test + - integration-tests: add actual checks for undoing install + - integration-tests: add store login test + - snap: add certain implicit slots only on classic + - integration-tests: add coverage flags to snapd.service ExecStart + setting when building from branch + - integration-tests: remove the tests for features removed in 16.04. + - daemon, overlord/snapstate: "(de)activate" is no longer a thing + - docs: update meta.md and security.md for current snappy + - debian: always start snapd + - integration-tests: add test for undoing failed install + - overlord: handle ensureNext being in the past + - overlord/snapstate,overlord/snapstate/backend,snappy: start + backend porting LinkSnap and UnlinkSnap + - debian/tests: add reboot capability to autopkgtest and execute + snapPersistsSuite + - daemon,snappy,progress: drop license agreement broken logic + - daemon,client,cmd/snap: nice access denied message + (LP: #1574829) + - daemon: add user parameter to all commands + - snap, store: rework purchase methods into decorators + - many: simplify release package and add OnClassic + - interfaces: miscellaneous policy updates + - snappy,wrappers: move desktop files handling to wrappers + - snappy: remove some obviously dead code + - interfaces/builtin: quote apparmor label + - many: remove the gadget yaml support from snappy + - snappy,systemd,wrappers: move service units generation to wrappers + - store: add method to determine if a snap must be bought + - store: add methods to read purchases from the store + - wrappers,snappy: move binary wrapper generation to new package + wrappers + - snap: add `snap help` command + - integration-tests: remove framework-test data and avoid using + config-snap for now + - add integration test to verify fix for LP: #1571721 + + -- Michael Vogt Fri, 13 May 2016 17:19:37 -0700 + +snapd (2.0.3) xenial; urgency=medium + + * New upstream micro release: + - integration-tests, debian/tests: add unity snap autopkg test + - snappy: introduce first feature flag for assumes: common-data-dir + - timeout,snap: add YAML unmarshal function for timeout.Timeout + - many: go into state.Retry state when unmounting a snap fails. + (LP: #1571721, #1575399) + - daemon,client,cmd/snap: improve output after snap + install/refresh/remove (LP: #1574830) + - integration-tests, debian/tests: add test for home interface + - interfaces,overlord: support unversioned data + - interfaces/builtin: improve the bluez interface + - cmd: don't include the unit tests when building with go test -c + for integration tests + - integration-tests: teach some new trick to the fake store, + reenable the app refresh test + - many: move with some simplifications test snap building to + snap/snaptest + - asserts: define type for revision related errors + - snap/snaptest,daemon,overlord/ifacestate,overlord/snapstate: unify + mocking snaps behind MockSnap + - snappy: fix openSnapFile's handling of sideInfo + - daemon: improve snap sideload form handling + - snap: add short and long description to the man-page + (LP: #1570280) + - snappy: remove unused SetProperty + - snappy: use more accurate test data + - integration-tests: add a integration test about remove removing + all revisions + - overlord/snapstate: make "snap remove" remove all revisions of a + snap (LP: #1571710) + - integration-tests: re-enable a bunch of integration tests + - snappy: remove unused dbus code + - overlord/ifacestate: fix setup-profiles to use new snap revision + for setup (LP: #1572463) + - integration-tests: add regression test for auth bug LP:#1571491 + - client, snap: remove obsolete TypeCore which was used in the old + SystemImage days + - integration-tests: add apparmor test + - cmd: don't perform type assertion when we know error to be nil + - client: list correct snap types + - intefaces/builtin: allow getsockname on connected x11 plugs + (LP: #1574526) + - daemon,overlord/snapstate: read name out of sideloaded snap early, + improved change summary + - overlord: keep tasks unlinked from a change hidden, prune them + - integration-tests: snap list on fresh boot is good again + - integration-tests: add partial term to the find test + - integration-tests: changed default release to 16 + - integration-tests: add regression test for snaps not present after + reboot + - integration-tests: network interface + - integration-tests: add proxy related environment variables to + snapd env file + - README.md: snappy => snap + - etc: trivial typo fix (LP:#1569892) + - debian: remove unneeded /var/lib/snapd/apparmor/additional + directory (LP: #1569577) + - builtin/unity7.go: allow using gmenu. LP: #1576287 + + -- Michael Vogt Tue, 03 May 2016 07:51:57 +0200 + +snapd (2.0.2) xenial; urgency=medium + + * New upstream release: + - systemd: add multi-user.target (LP: #1572125) + - release: our series is 16 + - integration-tests: fix snapd binary path for mounting the daemon + built from branch + - overlord,snap: add firstboot state sync + + -- Michael Vogt Tue, 19 Apr 2016 16:02:44 +0200 + +snapd (2.0.1) xenial; urgency=medium + + * client,daemon,overlord: fix authentication: + - fix incorrect authenication check (LP: #1571491) + + -- Michael Vogt Mon, 18 Apr 2016 07:24:33 +0200 + +snapd (2.0) xenial; urgency=medium + + * New upstream release: + - debian: put snapd in /usr/lib/snapd/ + - cmd/snap: minor polishing + - cmd,client,daemon: add snap abort command + - overlord: don't hold locks when callling backends + - release,store,daemon: no more default-channel, release=>series + - many: drop support for deprecated environment variables + (SNAP_APP_*) + - many: support individual ids in changes cmd + - overlord/state: use numeric change and task ids + - overlord/auth,daemon,client,cmd/snap: logout + - daemon: don't install ubuntu-core twice + - daemon,client,overlord/state,cmd: add changes command + - interfaces/dbus: drop superfluous backslash from template + - daemon, overlord/snapstate: updates are users too! + - cmd/snap,daemon,overlord/ifacestate: add support for developer + mode + - daemon,overlord/snapstate: on refresh use the remembered channel, + default to stable channel otherwise + - cmd/snap: improve UX of snap interfaces when there are no results + - overlord/state: include time in task log messages + - overlord: prune and abort old changes and tasks + - overlord/ifacestate: add implicit slots in setup-profiles + - daemon,overlord: setup authentication for store downloads + - daemon: macaroon-authed users are like root, and sudoers can login + - daemon,client,docs: send install options to daemon + + -- Michael Vogt Sat, 16 Apr 2016 22:15:40 +0200 + +snapd (1.9.4) xenial; urgency=medium + + * New upstream release: + - etc: fix desktop file location + - overlord/snapstate: stop an update once download sees the revision + is already installed + - overlord: make SnapState.DevMode a method, store flags + - snappy: no more snapYaml in snappy.Snap + - daemon,cmd,dirs,lockfile: drop all lockfiles + - debian: use sudo in setup of the proxy environment + - snap/snapenv,snappy,systemd: expose SNAP_REVISION to app + environment + - snap: validate similarly to what we did with old snapYaml info + from squashfs snaps + - daemon,store: plug in authentication for store search/details + - overlord/snapstate: fix JSON name of SnapState.Candidate + - overlord/snapstate: start using revisions higher than 100000 for + local installs (sideloads) + - interfaces,overlorf/ifacestate: honor user choice and don't auto- + connect disconnected plugs + - overlord/auth,daemon,client: hide user ids again + - daemon,overlord/snapstate: back /snaps (and so snap list) using + state + - daemon,client,overlord/auth: rework state auth data + - overlord/snapstate: disable Activate and Deactivate + - debian: fix silly typo in autopkgtest setup + - overlord/ifacestate: remove connection state with discard-conns + task, on the removal of last snap + - daemon,client: rename API update action to refresh + - cmd/snap: rework login to be more resilient + - overlord/snapstate: deny two changes on one snap + - snappy: fix crash on certain snap.yaml + - systemd: use native systemctl enable instead of our own + implementation + - store: add workaround for misbehaving store + - debian: make autopkgtest use the right env vars + - state: log do/undo status too when a task is run + - docs: update rest.md with price information + - daemon: only include price property if the snap is non-free + - daemon, client, cmd/snap: connect/disconnect now async + - snap,snappy: allow snaps to require system features + - integration-tests: fix report of skips in SetUpTest method + - snappy: clean out major bits (still using Installed) now + unreferenced as cmd/snappy is gone + - daemon/api,overlord/auth: add helper to get UserState from a + client request + + -- Michael Vogt Fri, 15 Apr 2016 23:30:00 +0200 + +snapd (1.9.3) xenial; urgency=medium + + * New upstream release: + - many: prepare for opengl support on classic + - interfaces/apparmor: load all apparmor profiles on snap setup + - daemon,client: move async resource to change in meta + - debian: disable autopilot + - snap: add basic progress reporting + - client,cmd,daemon,snap,store: show the price of snaps in the cli + - state: add minimal taskrunner logging + - daemon,snap,overlord/snapstate: in the API get the snap icon using + state + - client,daemon,overlord: don't guess snap file vs. name + - overlord/ifacestate: reload snap connections when setting up + security for a given snap + - snappy: remove cmd/snappy (superseded in favour of cmd/snap) + - interfaecs/apparmor: remove all traces of old-security from + apparmor backend + - interfaces/builtin: add bluez interface + - overlord/ifacestate: don't crash if connection cannot be reloaded + - debian: add searchSuite to autopkgtest + - client, daemon, cmd/snap: no more tasks; everything is changes + - client: send authorization header in client requests + - client, daemon: marshal suggested currency over REST + - docs, snap: enumerate snap types correctly in docs and comments + - many: add store authenticator parameter + - overlord/ifacestate,daemon: setup security on conect and + disconnect + - interfaces/apparmor: remove unused apparmor variables + - snapstate: add missing "TaskProgressAdapter.Write()" for working + progress reporting + - many: clean out snap config related code not for OS + - daemon,client,cmd: return snap list from /v2/snaps + - docs: update `/v2/snaps` endpoint documentation + - interfaces: rename developerMode to devMode + - daemon,client,overlord: progress current => done + - daemon,client,cmd/snap: move query metadata to top-level doc + - interfaces: add TestSecurityBackend + - many: replace typographic quotes with ASCII + - client, daemon: rework rest changes to export "ready" and "err" + - overlord/snapstate,snap,store: track snap-id in side-info and + therefore in state + - daemon: improve mocking of interfaces API tests + - integration-tests: remove origins in default snap names for udf + call + - integration-test: use "snap list" in GetCurrentVersion + - many: almost no more NewInstalledSnap reading manifest from + snapstate and backend + - daemon: auto install ubuntu-core if missing + - oauth,store: remove OAuth authentication logic + - overlord/ifacestate: simplify some tests with implicit manager + initialization + - store, snappy: move away from hitting details directly + - overlord/ifacestate: reload connections when restarting the + manager + - overlord/ifacestate: increase flexibility of unit tests + - overlord: use state to discover all installed snaps + - overlord/ifacestate: track connections in the state + - many: separate copy-data from unlinking of current snap + - overlord/auth,store/auth: add macaroon authenticator to UserState + - client: support for /v2/changes and /v2/changes/{id} + - daemon/api,overlord/auth: rework authenticated users information + in state + + -- Michael Vogt Thu, 14 Apr 2016 23:29:43 +0200 + +snapd (1.9.2) xenial; urgency=medium + + * New upstream release: + - cmd/snap,daemon,store: rework login command to use daemon login + API + - store: cache suggested currency from the store + - overlord/ifacestate: modularize and extend tests + - integration-tests: reenable failure tests + - daemon: include progress in rest changes + - daemon, overlord/state: expose individual changes + - overlord/ifacestate: drop duplicate package comment + - overlord/ifacestate: allow tests to override security backends + - cmd/snap: install *.snap and *.snap.* as files too + - interfaces/apparmor: replace /var/lib/snap with /var/snap + - daemon,overlord/ifacestate: connect REST API to interfaces in the + overlord + - debian: remove unneeded dependencies from snapd + - overlord/state: checkpoint on final progress only + - osutil: introduce IsUIDInAny + - overlord/snapstate: rename GetSnapState to Get, SetSnapState to + Set + - daemon: add id to changes json + - overlord/snapstate: SetSnapState() needs locks + - overlord: fix broken tests + - overlord/snapstate,overlord/ifacestate: reimplement SnapInfo (as + Info) actually using the state + + -- Michael Vogt Wed, 13 Apr 2016 17:27:00 +0200 + +snapd (1.9.1.1) xenial; urgency=medium + + * debian/tests/control: + - add git to make autopkgtest work + + -- Michael Vogt Tue, 12 Apr 2016 17:19:19 +0200 + +snapd (1.9.1) xenial; urgency=medium + + * Add warning about installing ubuntu-core-snapd-units on Desktop systems. + * Add ${misc:Depends} to ubuntu-core-snapd-units. + * interfaces,overlord: add support for auto-connecting plugs on + install + * fix sideloading snaps and (re)add tests for this + * add `ca-certificates` to the test-dependencies to fix autopkgtest + failure on armhf + + -- Michael Vogt Tue, 12 Apr 2016 14:39:57 +0200 + +snapd (1.9) xenial; urgency=medium + + * rename source and binary package to "snapd" + * update directory layout to final 16.04 layout + * use `snap` command instead of the previous `snappy` + * use `interface` based security + * use new state engine for install/update/remove + + -- Michael Vogt Tue, 12 Apr 2016 01:05:09 +0200 + +ubuntu-snappy (1.7.3+20160310ubuntu1) xenial; urgency=medium + + - debian: update versionized ubuntu-core-launcher dependency + - debian: tweak desktop file dir, ship Xsession.d snip for seamless + integration + - snappy: fix hw-assign to work with per-app udev tags + - snappy: use $snap.$app as per-app udev tag + - snap,snappy,systemd: %s/\/SNAP_DEVELOPER/g + - snappy: add mksquashfs --no-xattrs parameter + - snap,snappy,systemd: kill SNAP_FULLNAME + + -- Michael Vogt Thu, 10 Mar 2016 09:26:20 +0100 + +ubuntu-snappy (1.7.3+20160308ubuntu1) xenial; urgency=medium + + - snappy,snap: move icon under meta/gui/ + - debian: add snap.8 manpage + - debian: move snapd to /usr/lib/snappy/snapd + - snap,snappy,systemd: remove TMPDIR, TEMPDIR, SNAP_APP_TMPDIR + - snappy,dirs: add support to use desktop files from inside snaps + - daemon: snapd API events endpoint redux + - interfaces/builtin: add "network" interface + - overlord/state: do small fixes (typo, id clashes paranoia) + - overlord: add first pass of the logic in StateEngine itself + - overlord/state: introduce Status/SetStatus on Change + - interfaces: support permanent security snippets + - overlord/state: introduce Status/SetStatus and + Progress/SetProgress on Task + - overlord/state: introduce Task and Change.NewTask + - many: selectively swap semantics of plugs and slots + - client,cmd/snap: remove useless indirection in Interfaces + - interfaces: maintain Plug and Slot connection details + - client,daemon,cmd/snap: change POST /2.0/interfaces to work with + lists + - overlord/state: introduce Change and NewChange on state to create + them + - snappy: bugfix for snap.yaml parsing to be more consistent with + the spec + - snappy,systemd: remove "ports" from snap.yaml + + -- Michael Vogt Tue, 08 Mar 2016 11:24:09 +0100 + +ubuntu-snappy (1.7.3+20160303ubuntu4) xenial; urgency=medium + + * rename: + debian/golang-snappy-dev.install -> + debian/golang-github-ubuntu-core-snappy-dev.install: + + -- Michael Vogt Thu, 03 Mar 2016 12:29:16 +0100 + +ubuntu-snappy (1.7.3+20160303ubuntu3) xenial; urgency=medium + + * really fix typo in dependency name + + -- Michael Vogt Thu, 03 Mar 2016 12:21:39 +0100 + +ubuntu-snappy (1.7.3+20160303ubuntu2) xenial; urgency=medium + + * fix typo in dependency name + + -- Michael Vogt Thu, 03 Mar 2016 12:05:36 +0100 + +ubuntu-snappy (1.7.3+20160303ubuntu1) xenial; urgency=medium + + - debian: update build-depends for MIR + - many: implement new REST API: GET /2.0/interfaces + - integration-tests: properly stop snapd from branch + - cmd/snap: update tests for go-flags changes + - overlord/state: implement Lock/Unlock with implicit checkpointing + - overlord: split out the managers and State to their own + subpackages of overlord + - snappy: rename "migration-skill" to "old-security" and use new + interface names instead of skills + - client,cmd/snap: clarify name ambiguity in Plug or Slot + - overlord: start working on state engine along spec v2, have the + main skeleton follow that + - classic, oauth: update tests for change in MakeRandomString() + - client,cmd/snap: s/add/install/:-( + - interfaces,daemon: specialize Name to either Plug or Slot + - interfaces,interfaces/types: unify security snippet functions + - snapd: close the listener on Stop, to force the http.Serve loop to + exit + - snappy,daemon,snap/lightweight,cmd/snappy,docs/rest.md: expose + explicit channel selection to rest api + - interfaces,daemon: rename package holding built-in interfaces + - integration-tests: add the first classic dimension tests + - client,deaemon,docs: rename skills to interfaces on the wire + - asserts: add identity assertion type + - integration-tests: add the no_proxy env var + - debian: update build-depends for new package names + - oauth: fix oauth & quoting in the oauth_signature + - integration-tests: remove unused field + - integration-tests: add the http proxy argument + - interfaces,interfaces/types,deamon: mass internal rename to + interfaces + - client,cmd/snap: rename skills to interfaces (part 2) + - arch: fix missing mapping for powerpc + + -- Michael Vogt Thu, 03 Mar 2016 11:00:19 +0100 + +ubuntu-snappy (1.7.3+20160225ubuntu1) xenial; urgency=medium + + - integration-tests: always use the built snapd when compiling + binaries from branch + - cmd/snap: rename skills to interfaces + - testutil,skills/types,skills,daemon: tweak discovery of know skill + types + - docs: add docs for arm64 cross building + - overlord: implement basic ReadState/WriteState + - overlord: implement Get/Set/Copy on State + - integration-tests: fix dd output check + - integration-tests: add fromBranch config field + - integration-tests: use cli pkg methods in hwAssignSuite + - debian: do not create the snappypkg user, we don't need it anymore + - arch: fix build failure on s390x + - classic: cleanup downloaded lxd tarball + - cmd/snap,client,integration-tests: rename snap subcmds + 'assert'=>'ack', 'asserts'=>'known' + - skills: fix broken tests builds + - skills,skills/types: pass slot to SlotSecuritySnippet() + - skills/types: teach bool-file about udev security + + -- Michael Vogt Thu, 25 Feb 2016 16:17:19 +0100 + +ubuntu-snappy (1.7.2+20160223ubuntu1) xenial; urgency=medium + + * New git snapshot: + - asserts: introduce snap-declaration + - cmd/snap: fix integration tests for the "cmd_asserts" + - integration-tests: fix fanctl output check + - cmd/snap: fix test failure after merging 23a64e6 + - cmd/snap: replace skip-help with empty description + - docs: update security.md to match current migration-skill + semantics + - snappy: treat commands with 'daemon' field as services + - asserts: use more consistent names for receivers in + snap_asserts*.go + - debian: add missing golang-websocket-dev build-dependency + - classic: if classic fails to get created, undo the bind mounts + - snappy: never return nil in NewLocalSnapRepository() + - notifications: A simple notification system + - snappy: when using staging, authenticate there instead + - integration-tests/snapd: fix the start of the test snapd socket + - skills/types: use CamelCase for security names + - skills: add support for implicit revoke + - skills: add security layer + - integration-tests: use exec.Command wrapper for updates + - cmd/snap: add 'snap skills' + - cms/snap: add 'snap revoke' + - docs: add docs for skills API + - cmd/snap: add 'snap grant' + - cmd/snappy, coreconfig, daemon, snappy: move config to always be + bytes (in and out) + - overlord: start with a skeleton and stubs for Overlord, + StateEngine, StateJournal and managers + - integration-tests: skip tests affected by LP: #1544507 + - skills/types: add bool-file + - po: refresh translation templates + - cmd/snap: add 'snap experimental remove-skill-slot' + - asserts: introduce device assertion + - cmd/snap: implemented add, remove, purge, refresh, rollback, + activate, deactivate + - cmd/snap: add 'snap experimental add-skill-slot' + - cmd/snap: add 'snap experimental remove-skill' + - cmd/snap: add tests for common skills code + - cmd/snap: add 'snap experimental add-skill' + - asserts: make assertion checkers used by db.Check modular and + pluggable + - cmd,client,daemon,caps,docs,po: remove capabilities + - scripts: move the script to get dependencies to a separate file + - asserts: make the disk layout compatible for storing more than one + revision + - cmd/snap: make the assert command options exported + - integration-tests: Remove the target release and channel + - asserts: introduce model assertion + - integration-tests: add exec.Cmd wrapper + - cmd/snap: add client test support methods + - cmd/snap: move key=value attribute parsing to commmon + - cmd/snap: apply new style consistency to "snap" commands. + - cmd/snap: support redirecting the client for testing + - cmd/snap: support testing command output + - snappy,daemon: remove the meta repositories abstractions + - cmd: add support for experimental commands + - cmd/snappy,daemon,snap,snappy: remove SetActive from parts + - cmd/snappy,daemon,snappy,snap: remove config from parts interface + - client: improve test data + - cmd: allow to construct a fresh parser + - cmd: don't treat help as an error + - cmd/snappy,snappy: remove "Details" from the repository interface + - asserts: check that primary keys are set when + Decode()ing/assembling assertions + - snap,snappy: refactor to remove "Install" from the Part interface + - client,cmd: make client.New() configurable + - client: enable retrieving asynchronous operation information with + `Client.Operation`. + + -- Michael Vogt Tue, 23 Feb 2016 11:28:18 +0100 + +ubuntu-snappy (1.7.2+20160204ubuntu1) xenial; urgency=medium + + * New git snapshot: + - integration-tests: fix the rollback error messages + - integration-test: use the common cli method when trying to install + an unexisting snap + - integration-tests: rename snap find test + - daemon: refactor makeErrorResponder() + - integration: add regression test for LP: #1541317 + - integration-tests: reenable TestRollbackMustRebootToOtherVersion + - asserts: introduce "snap asserts" subcmd to show assertions in the + system db + - docs: fix parameter style + - daemon: use underscore in JSON interface + - client: add skills API + - asserts,docs/rest.md: change Encoder not to add extra newlines at + the end of the stream + - integration-tests: "snappy search" is no more, its "snap search" + now + - README, integration-tests/tests: chmod snapd.socket after manual + start. + - snappy: add default security profile if none is specified + - skills,daemon: add REST APIs for skills + - cmd/snap, cmd/snappy: move from `snappy search` to `snap find`. + - The first step towards REST world domination: search is now done + via + - debian: remove obsolete /etc/grub.d/09_snappy on upgrade + - skills: provide different security snippets for skill and slot + side + - osutil: make go vet happy again + - snappy,systemd: use Type field in systemd.ServiceDescription + - skills: add basic grant-revoke methods + - client,daemon,asserts: expose the ability to query assertions in + the system db + - skills: add basic methods for slot handling + - snappy,daemon,snap: move "Uninstall" into overlord + - snappy: move SnapFile.Install() into Overlord.Install() + - integration-tests: re-enable some failover tests + - client: remove snaps + - asserts: uniform searching across trusted (account keys) and main + backstore + - asserts: introduce Decoder to parse streams of assertions and + Encoder to build them + - client: filter snaps with a search query + - client: pass query as well as path in client internals + - skills: provide different security snippets for skill and slot + side + - snappy: refactor snapYaml to remove methods on snapYaml type + - snappy: remove unused variable from test + - skills: add basic methods for skill handing + - snappy: remove support for meta/package.yaml and implement new + meta/snap.yaml + - snappy: add new overlord type responsible for + Installed/Install/Uninstall/SetActive and stub it out + - skills: add basic methods for type handling + - daemon, snappy: add find (aka search) + - client: filter snaps by type + - skills: tweak valid names and error messages + - skills: add special skill type for testing + - cmd/snapd,daemon: filter snaps by type + - partition: remove obsolete uEnv.txt + - skills: add Type interface + - integration-tests: fix the bootloader path + - asserts: introduce a memory backed assertion backstore + - integration-tests: get name of OS snap from bootloader + - cmd/snapd,daemon: filter snaps by source + - asserts,daemon: bump some copyright years for things that have + been touched in the new year + - skills: add the initial Repository type + - skills: add a name validation function + - client: filter snaps by source + - snappy: unmount the squashfs snap again if it fails to install + - snap: make a copy of the search uri before mutating it + Closes: LP#1537005 + - cmd/snap,client,daemon,asserts: introduce "assert " snap + subcommand + - cmd/snappy, snappy: fix failover handling of the "active" + kernel/os snap + - daemon, client, docs/rest.md, snapd integration tests: move to the + new error response + - asserts: change Backstore interface, backstores can now access + primary key names from types + - asserts: make AssertionType into a real struct exposing the + metadata Name and PrimaryKey + - caps: improve bool-file sanitization + - asserts: fixup toolbelt to use exposed key ID. + - client: return by reference rather than by value + - asserts: exported filesystem backstores + explicit backstores + + -- Michael Vogt Thu, 04 Feb 2016 16:35:31 +0100 + +ubuntu-snappy (1.7.2+20160113ubuntu1) xenial; urgency=medium + + * New git snapshot + + -- Michael Vogt Wed, 13 Jan 2016 11:25:40 +0100 + +ubuntu-snappy (1.7.2ubuntu1) xenial; urgency=medium + + * New upstream release: + - bin-path integration + - assertions/capability work + - fix squashfs based snap building + + -- Michael Vogt Fri, 04 Dec 2015 08:46:35 +0100 + +ubuntu-snappy (1.7.1ubuntu1) xenial; urgency=medium + + * New upstream release: + - fix dependencies + - fix armhf builds + + -- Michael Vogt Wed, 02 Dec 2015 07:46:07 +0100 + +ubuntu-snappy (1.7ubuntu1) xenial; urgency=medium + + * New upstream release: + - kernel/os snap support + - squashfs snap support + - initial capabilities work + - initial assertitions work + - rest API support + + -- Michael Vogt Wed, 18 Nov 2015 19:59:51 +0100 + +ubuntu-snappy (1.6ubuntu1) wily; urgency=medium + + * New upstream release, including the following changes: + - Fix hwaccess for gpio (LP: #1493389, LP: #1488618) + - Fix handleAssets name normalization + - Run boot-ok job late (LP: #1476129) + - Add support for systemd socket files + - Add "snappy service" command + - Documentation improvements + - Many test improvements (unit and integration) + - Override sideload versions + - Go1.5 fixes + - Add i18n + - Add man-page + - Add .snapignore + - Run services that uses external ports only after the network is up + - Bufix in Synbootloader (LP: 1474125) + - Use uboot.env for boot state tracking + + -- Michael Vogt Wed, 09 Sep 2015 14:20:22 +0200 + +ubuntu-snappy (1.5ubuntu1) wily; urgency=medium + + * New upstream release, including the following changes: + - Use O_TRUNC when copying files + - Added path redefinition to include test's binaries location + - Don't run update-grub, instead use grub.cfg from the oem + package + - Do network configuration from first boot + - zero size systemd of new partition made executable to + prevent unrecoverable boot failure + - Close downloaded files + + -- Ricardo Salveti de Araujo Mon, 06 Jul 2015 15:14:37 -0300 + +ubuntu-snappy (1.4ubuntu1) wily; urgency=medium + + * New upstream release, including the following changes: + - Allow to run the integration tests using snappy from branch + - Add CopyFileOverwrite flag and behaviour to helpers.CopyFile + - add a bunch of missing i18n.G() now that we have gettext + - Generate only the translators comments that start with + TRANSLATORS + - Try both clickpkg and snappypkg when dropping privs + + -- Ricardo Salveti de Araujo Thu, 02 Jul 2015 16:21:53 -0300 + +ubuntu-snappy (1.3ubuntu1) wily; urgency=medium + + * New upstream release, including the following changes: + - gettext support + - use snappypkg user for the installed snaps + - switch to system-image-3.x as the system-image backend + - more reliable developer mode detection + + -- Michael Vogt Wed, 01 Jul 2015 10:37:05 +0200 + +ubuntu-snappy (1.2-0ubuntu1) wily; urgency=medium + + * New upstream release, including the following changes: + - Consider the root directory when installing and removing policies + - In the uboot TestHandleAssetsNoHardwareYaml, patch the cache dir + before creating the partition type + - In the PartitionTestSuite, remove the unnecessary patches for + defaultCacheDir + - Fix the help output of "snappy install -h" + + -- Ricardo Salveti de Araujo Wed, 17 Jun 2015 11:42:47 -0300 + +ubuntu-snappy (1.1.2-0ubuntu1) wily; urgency=medium + + * New upstream release, including the following changes: + - Remove compatibility for click-bin-path in generated exec-wrappers + - Release the readme.md after parsing it + + -- Ricardo Salveti de Araujo Thu, 11 Jun 2015 23:42:49 -0300 + +ubuntu-snappy (1.1.1-0ubuntu1) wily; urgency=medium + + * New upstream release, including the following changes: + - Set all app services to restart on failure + - Fixes the missing oauth quoting and makes the code a bit nicer + - Added integrate() to set Integration to default values needed for + integration + - Moved setActivateClick to be a method of SnapPart + - Make unsetActiveClick a method of SnapPart + - Check the package.yaml for the required fields + - Integrate lp:snappy/selftest branch into snappy itself + - API to record information about the image and to check if the kernel was + sideloaded. + - Factor out update from cmd + - Continue updating when a sideload error is returned + + -- Ricardo Salveti de Araujo Wed, 10 Jun 2015 15:54:12 -0300 + +ubuntu-snappy (1.1-0ubuntu1) wily; urgency=low + + * New wily upload with fix for go 1.4 syscall.Setgid() breakage + + -- Michael Vogt Tue, 09 Jun 2015 10:02:04 +0200 + +ubuntu-snappy (1.0.1-0ubuntu1) vivid; urgency=low + + * fix symlink unpacking + * fix typo in apparmor rules generation + + -- Michael Vogt Thu, 23 Apr 2015 16:09:56 +0200 + +ubuntu-snappy (1.0-0ubuntu1) vivid; urgency=low + + * 15.04 archive upload + + -- Michael Vogt Thu, 23 Apr 2015 11:08:22 +0200 + +ubuntu-snappy (0.1.2-0ubuntu1) vivid; urgency=medium + + * initial ubuntu archive upload + + -- Michael Vogt Mon, 13 Apr 2015 22:48:13 -0500 + +ubuntu-snappy (0.1.1-0ubuntu1) vivid; urgency=low + + * new snapshot + + -- Michael Vogt Thu, 12 Feb 2015 13:51:22 +0100 + +ubuntu-snappy (0.1-0ubuntu1) vivid; urgency=medium + + * Initial packaging + + -- Sergio Schvezov Fri, 06 Feb 2015 02:25:43 -0200 diff --git a/debian/compat b/debian/compat new file mode 100644 index 00000000..ec635144 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 00000000..aa03af3c --- /dev/null +++ b/debian/control @@ -0,0 +1,114 @@ +Source: snapd +Section: devel +Priority: optional +Maintainer: Ubuntu Developers +Build-Depends: autoconf, + automake, + autotools-dev, + bash-completion, + debhelper (>= 9), + dh-apparmor, + dh-autoreconf, + dh-golang (>=1.7), + dh-systemd, + fakeroot, + gettext, + gnupg2, + golang-any (>=2:1.6) | golang-1.6, + indent, + init-system-helpers, + libapparmor-dev, + libglib2.0-dev, + libseccomp-dev, + libudev-dev, + pkg-config, + python3, + python3-docutils, + python3-markdown, + squashfs-tools, + udev +Standards-Version: 3.9.7 +Homepage: https://github.com/snapcore/snapd +Vcs-Browser: https://github.com/snapcore/snapd +Vcs-Git: https://github.com/snapcore/snapd.git + +Package: golang-github-ubuntu-core-snappy-dev +Architecture: all +Depends: golang-github-snapcore-snapd-dev, ${misc:Depends} +Section: oldlibs +Description: transitional dummy package + This is a transitional dummy package. It can safely be removed. + +Package: golang-github-snapcore-snapd-dev +Architecture: all +Breaks: golang-github-ubuntu-core-snappy-dev (<< 2.0.6), + golang-snappy-dev (<< 1.7.3+20160303ubuntu4) +Replaces: golang-github-ubuntu-core-snappy-dev (<< 2.0.6), + golang-snappy-dev (<< 1.7.3+20160303ubuntu4) +Depends: ${misc:Depends} +Description: snappy development go packages. + Use these to use the snappy API. + +Package: snapd +Architecture: any +Depends: adduser, + apparmor (>= 2.10.95-0ubuntu2.2), + ca-certificates, + gnupg1 | gnupg, + snap-confine (= ${binary:Version}), + squashfs-tools, + systemd, + ubuntu-core-launcher (= ${binary:Version}), + ${misc:Depends}, + ${shlibs:Depends} +Replaces: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9) +Breaks: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9) +Conflicts: snap (<< 2013-11-29-1ubuntu1) +Built-Using: ${misc:Built-Using} +Description: Tool to interact with Ubuntu Core Snappy. + Install, configure, refresh and remove snap packages. Snaps are + 'universal' packages that work across many different Linux systems, + enabling secure distribution of the latest apps and utilities for + cloud, servers, desktops and the internet of things. + . + This is the CLI for snapd, a background service that takes care of + snaps on the system. Start with 'snap list' to see installed snaps. + +Package: ubuntu-snappy +Architecture: all +Depends: snapd, ${misc:Depends} +Section: oldlibs +Description: transitional dummy package + This is a transitional dummy package. It can safely be removed. + +Package: ubuntu-snappy-cli +Architecture: all +Depends: snapd, ${misc:Depends} +Section: oldlibs +Description: transitional dummy package + This is a transitional dummy package. It can safely be removed. + +Package: ubuntu-core-snapd-units +Architecture: all +Depends: snapd, ${misc:Depends} +Section: oldlibs +Description: transitional dummy package + This is a transitional dummy package. It can safely be removed. + +Package: snap-confine +Architecture: any +Breaks: ubuntu-core-launcher (<< 1.0.32) +Replaces: ubuntu-core-launcher (<< 1.0.32) +Depends: apparmor (>= 2.10.95-0ubuntu2.2), ${misc:Depends}, ${shlibs:Depends} +Description: Support executable to apply confinement for snappy apps + This package contains an internal tool for applying confinement to snappy app. + The executable (snap-confine) is ran internally by snapd to apply confinement + to the started application process. The tool is written in C and carefully + reviewed to limit the attack surface on the security model of snapd. + +Package: ubuntu-core-launcher +Architecture: any +Depends: snap-confine (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends} +Pre-Depends: dpkg (>= 1.15.7.2) +Description: Launcher for ubuntu-core (snappy) apps + This package contains the launcher for launching snappy applications diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 00000000..1fa64b84 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,22 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: snappy +Source: https://github.com/snapcore/snapd + +Files: * +Copyright: Copyright (C) 2014,2015 Canonical, Ltd. +License: GPL-3 + This program is free software: you can redistribute it and/or modify it + under the terms of the 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 warranties of + MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR + PURPOSE. See the applicable version of the GNU Lesser 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 . + . + On Debian systems, the complete text of the GNU General Public License + can be found in `/usr/share/common-licenses/GPL-3' diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 00000000..70910254 --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,4 @@ +[DEFAULT] +debian-branch = master +export-dir = ../build-area +postexport = govendor sync diff --git a/debian/golang-github-snapcore-snapd-dev.install b/debian/golang-github-snapcore-snapd-dev.install new file mode 100644 index 00000000..1dfbabc8 --- /dev/null +++ b/debian/golang-github-snapcore-snapd-dev.install @@ -0,0 +1 @@ +debian/tmp/usr/share/gocode/src/* diff --git a/debian/not-installed b/debian/not-installed new file mode 100644 index 00000000..708d871f --- /dev/null +++ b/debian/not-installed @@ -0,0 +1 @@ +debian/tmp/usr/bin/uboot-go diff --git a/debian/rules b/debian/rules new file mode 100755 index 00000000..590c638c --- /dev/null +++ b/debian/rules @@ -0,0 +1,213 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +#export DH_VERBOSE=1 +export DH_OPTIONS +export DH_GOPKG := github.com/snapcore/snapd +#export DEB_BUILD_OPTIONS=nocheck +export DH_GOLANG_EXCLUDES=tests +export DH_GOLANG_GO_GENERATE=1 + +export PATH:=${PATH}:${CURDIR} +# make sure that correct go version is found on trusty +export PATH:=/usr/lib/go-1.6/bin:${PATH} + +include /etc/os-release + +SYSTEMD_UNITS_DESTDIR= +ifeq (${VERSION_ID},"14.04") + # We are relying on a deputy systemd setup for trusty, + # in which systemd does not run as PID 1. To solve the + # problem of services shipping systemd units and upstart jobs + # being started twice, we altered systemd on trusty to ignore + # /lib/systemd/system and instead consider only selected units from + # /lib/systemd/upstart. + SYSTEMD_UNITS_DESTDIR="lib/systemd/upstart/" + # make sure that trusty's golang-1.6 is picked up correctly. + export PATH:=/usr/lib/go-1.6/bin:${PATH} +else + SYSTEMD_UNITS_DESTDIR="lib/systemd/system/" +endif + +# The go tool does not fully support vendoring with gccgo, but we can +# work around that by constructing the appropriate -I flag by hand. +GCCGO := $(shell go tool dist env > /dev/null 2>&1 && echo no || echo yes) + +BUILDFLAGS:=-buildmode=pie -pkgdir=$(CURDIR)/_build/std +GCCGOFLAGS= +ifeq ($(GCCGO),yes) +GOARCH := $(shell go env GOARCH) +GOOS := $(shell go env GOOS) +BUILDFLAGS:= +GCCGOFLAGS=-gccgoflags="-I $(CURDIR)/_build/pkg/gccgo_$(GOOS)_$(GOARCH)/$(DH_GOPKG)/vendor" +export DH_GOLANG_GO_GENERATE=0 +endif + +# check if we need to include the testkeys in the binary +TAGS= +ifneq (,$(filter testkeys,$(DEB_BUILD_OPTIONS))) + TAGS=-tags withtestkeys +endif + +# export DEB_BUILD_MAINT_OPTIONS = hardening=+all +# DPKG_EXPORT_BUILDFLAGS = 1 +# include /usr/share/dpkg/buildflags.mk + +# Currently, we enable confinement for Ubuntu only, not for derivatives, +# because derivatives may have different kernels that don't support all the +# required confinement features and we don't to mislead anyone about the +# security of the system. Discuss a proper approach to this for downstreams +# if and when they approach us +ifeq ($(shell dpkg-vendor --query Vendor),Ubuntu) + VENDOR_ARGS=--enable-nvidia-ubuntu +else + VENDOR_ARGS=--disable-apparmor +endif + +%: + dh $@ --buildsystem=golang --with=golang --fail-missing --with systemd --builddirectory=_build + +override_dh_fixperms: + dh_fixperms -Xusr/lib/snapd/snap-confine + +override_dh_installdeb: + dh_apparmor --profile-name=usr.lib.snapd.snap-confine -psnap-confine + dh_installdeb + +override_dh_clean: +ifneq (,$(TEST_GITHUB_AUTOPKGTEST)) + # this will be set by the GITHUB webhook to trigger a autopkgtest + # we only need to run "govendor sync" here and then its ready + (export GOPATH="/tmp/go"; \ + mkdir -p $$GOPATH/src/github.com/snapcore/; \ + cp -ar . $$GOPATH/src/github.com/snapcore/snapd; \ + go get -u github.com/kardianos/govendor; \ + (cd $$GOPATH/src/github.com/snapcore/snapd ; $$GOPATH/bin/govendor sync); \ + cp -ar $$GOPATH/src/github.com/snapcore/snapd/vendor/ .; \ + ) +endif + dh_clean + # XXX: hacky + $(MAKE) -C cmd distclean || true + +override_dh_auto_build: + # usually done via `go generate` but that is not supported on powerpc + ./mkversion.sh + # Build golang bits + mkdir -p _build/src/$(DH_GOPKG)/cmd/snap/test-data + cp -a cmd/snap/test-data/*.gpg _build/src/$(DH_GOPKG)/cmd/snap/test-data/ + dh_auto_build -- $(BUILDFLAGS) $(TAGS) $(GCCGOFLAGS) + # Build C bits, sadly manually + cd cmd && ( autoreconf -i -f ) + cd cmd && ( ./configure --prefix=/usr --libexecdir=/usr/lib/snapd $(VENDOR_ARGS)) + $(MAKE) -C cmd all + +override_dh_auto_test: + dh_auto_test -- $(GCCGOFLAGS) +# a tested default (production) build should have no test keys +ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) + # check that only the main trusted account-key is included + [ $$(strings _build/bin/snapd|grep -c -E "public-key-sha3-384: [a-zA-Z0-9_-]{64}") -eq 1 ] + strings _build/bin/snapd|grep -c "^public-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk$$" +endif +ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) + # run the snap-confine tests + $(MAKE) -C cmd check +endif + +override_dh_systemd_enable: + # enable auto-import + dh_systemd_enable \ + -psnapd \ + snapd.autoimport.service + # we want the auto-update timer enabled by default + dh_systemd_enable \ + -psnapd \ + snapd.refresh.timer + # but the auto-update service disabled + dh_systemd_enable \ + --no-enable \ + -psnapd \ + snapd.refresh.service + # enable snapd + dh_systemd_enable \ + -psnapd \ + snapd.socket + dh_systemd_enable \ + -psnapd \ + snapd.service + +override_dh_systemd_start: + # we want to start the auto-update timer + dh_systemd_start \ + -psnapd \ + snapd.refresh.timer + # but not start the service + dh_systemd_start \ + --no-start \ + -psnapd \ + snapd.refresh.service + # start snapd + dh_systemd_start \ + -psnapd \ + snapd.socket + dh_systemd_start \ + -psnapd \ + snapd.service + # start autoimport + dh_systemd_start \ + -psnapd \ + snapd.autoimport.service + +override_dh_install: + # we do not need this in the package, its just needed during build + rm -rf ${CURDIR}/debian/tmp/usr/bin/xgettext-go + # uboot-go is not shippable + rm -f ${CURDIR}/debian/tmp/usr/bin/uboot-go + # toolbelt is not shippable + rm -f ${CURDIR}/debian/tmp/usr/bin/toolbelt + # we do not like /usr/bin/snappy anymore + rm -f ${CURDIR}/debian/tmp/usr/bin/snappy + # install dev package files + mkdir -p debian/golang-github-snapcore-snapd-dev/usr/share + rm -rf debian/tmp/usr/share/gocode/src/github.com/snapcore/snapd/cmd/snap-confine + cp -R debian/tmp/usr/share/gocode debian/golang-github-snapcore-snapd-dev/usr/share + # install udev stuff, must be installed before 80-udisks + install debian/snapd.autoimport.udev -D debian/snapd/lib/udev/rules.d/66-snapd-autoimport.rules + + # install bash completion files + install --mode=0644 data/completion/snap -D debian/snapd/usr/share/bash-completion/completions/snap + # i18n stuff + mkdir -p debian/snapd/usr/share + if [ -d share/locale ]; then \ + cp -R share/locale debian/snapd/usr/share; \ + fi + # etc/profile.d contains the PATH extension for snap packages + mkdir -p debian/snapd/etc + cp -R etc/profile.d debian/snapd/etc + # etc/X11/Xsession.d will add to XDG_DATA_DIRS so that we have .desktop support + mkdir -p debian/snapd/etc + cp -R etc/X11 debian/snapd/etc + # we conditionally install snapd's systemd units + mkdir -p debian/snapd/$(SYSTEMD_UNITS_DESTDIR) + install --mode=0644 debian/snapd.refresh.timer debian/snapd/$(SYSTEMD_UNITS_DESTDIR) + install --mode=0644 debian/snapd.refresh.service debian/snapd/$(SYSTEMD_UNITS_DESTDIR) + install --mode=0644 debian/snapd.autoimport.service debian/snapd/$(SYSTEMD_UNITS_DESTDIR) + install --mode=0644 debian/*.socket debian/snapd/$(SYSTEMD_UNITS_DESTDIR) + install --mode=0644 debian/snapd.service debian/snapd/$(SYSTEMD_UNITS_DESTDIR) +ifeq ($(RELEASE),trusty) + dh_link debian/snapd/$(SYSTEMD_UNITS_DESTDIR)/snapd.service debian/snapd/$(SYSTEMD_UNITS_DESTDIR)/multi-user.target.wants/snapd.service + dh_link debian/snapd/$(SYSTEMD_UNITS_DESTDIR)/snapd.autoimport.service debian/snapd/$(SYSTEMD_UNITS_DESTDIR)/multi-user.target.wants/snapd.autoimport.service +endif + $(MAKE) -C cmd install DESTDIR=$(CURDIR)/debian/tmp + dh_install + +override_dh_auto_install: snap.8 + dh_auto_install -O--buildsystem=golang + +snap.8: + $(CURDIR)/_build/bin/snap help --man > $@ + +override_dh_auto_clean: + dh_auto_clean -O--buildsystem=golang + rm -vf snap.8 diff --git a/debian/snap-confine.install b/debian/snap-confine.install new file mode 100644 index 00000000..36b6d498 --- /dev/null +++ b/debian/snap-confine.install @@ -0,0 +1,7 @@ +etc/apparmor.d/usr.lib.snapd.snap-confine +lib/udev/rules.d/80-snappy-assign.rules +lib/udev/snappy-app-dev +usr/lib/snapd/snap-confine +usr/lib/snapd/snap-discard-ns +usr/share/man/man5/snap-confine.5 +usr/share/man/man5/snap-discard-ns.5 diff --git a/debian/snapd.autoimport.service b/debian/snapd.autoimport.service new file mode 100644 index 00000000..2c75f154 --- /dev/null +++ b/debian/snapd.autoimport.service @@ -0,0 +1,10 @@ +[Unit] +Description=Auto import assertions from block devices +After=snapd.service snapd.socket + +[Service] +Type=oneshot +ExecStart=/usr/bin/snap auto-import + +[Install] +WantedBy=multi-user.target diff --git a/debian/snapd.autoimport.udev b/debian/snapd.autoimport.udev new file mode 100644 index 00000000..f06a9f3e --- /dev/null +++ b/debian/snapd.autoimport.udev @@ -0,0 +1,3 @@ +# probe for assertions, must run before udisks2 +ACTION=="add", SUBSYSTEM=="block" \ + RUN+="/usr/bin/unshare -m /usr/bin/snap auto-import --mount=/dev/%k" diff --git a/debian/snapd.dirs b/debian/snapd.dirs new file mode 100644 index 00000000..9383d810 --- /dev/null +++ b/debian/snapd.dirs @@ -0,0 +1,10 @@ +snap +usr/lib/snapd +var/lib/snapd/auto-import +var/lib/snapd/desktop +var/lib/snapd/environment +var/lib/snapd/firstboot +var/lib/snapd/lib/gl +var/lib/snapd/snaps/partial +var/lib/snapd/void +var/snap diff --git a/debian/snapd.install b/debian/snapd.install new file mode 100644 index 00000000..dc5ee731 --- /dev/null +++ b/debian/snapd.install @@ -0,0 +1,6 @@ +usr/bin/snap +usr/bin/snap-exec /usr/lib/snapd/ +usr/bin/snapctl +usr/bin/snapd /usr/lib/snapd/ + +data/info /usr/lib/snapd/ diff --git a/debian/snapd.maintscript b/debian/snapd.maintscript new file mode 100644 index 00000000..66826a72 --- /dev/null +++ b/debian/snapd.maintscript @@ -0,0 +1,4 @@ +# keep mount point busy +# we used to ship a custom grub config that is no longer needed +rm_conffile /etc/grub.d/09_snappy 1.7.3ubuntu1 +rm_conffile /etc/ld.so.conf.d/snappy.conf 2.0.7~ diff --git a/debian/snapd.manpages b/debian/snapd.manpages new file mode 100644 index 00000000..b383785e --- /dev/null +++ b/debian/snapd.manpages @@ -0,0 +1 @@ +snap.8 diff --git a/debian/snapd.postinst b/debian/snapd.postinst new file mode 100644 index 00000000..9aefd971 --- /dev/null +++ b/debian/snapd.postinst @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e + +#DEBHELPER# + + +case "$1" in + configure) + # ensure /var/lib/snapd/lib/gl is cleared + if dpkg --compare-versions "$2" lt-nl "2.0.7"; then + ldconfig + fi +esac diff --git a/debian/snapd.postrm b/debian/snapd.postrm new file mode 100644 index 00000000..a59aff65 --- /dev/null +++ b/debian/snapd.postrm @@ -0,0 +1,67 @@ +#!/bin/sh + +set -e + +systemctl_stop() { + unit="$1" + if systemctl is-active -q "$unit"; then + echo "Stoping $unit" + systemctl stop -q "$unit" || true + fi +} + +if [ "$1" = "purge" ]; then + mounts=$(systemctl list-unit-files | grep '^snap[-.].*\.mount' | cut -f1 -d ' ') + services=$(systemctl list-unit-files | grep '^snap[-.].*\.service' | cut -f1 -d ' ') + for unit in $services $mounts; do + # ensure its really a snapp mount unit or systemd unit + if ! grep -q 'What=/var/lib/snapd/snaps/' "/etc/systemd/system/$unit" && ! grep -q 'X-Snappy=yes' "/etc/systemd/system/$unit"; then + echo "Skipping non-snapd systemd unit $unit" + continue + fi + + echo "Stopping $unit" + systemctl_stop "$unit" + + # if it is a mount unit, we can find the snap name in the mount + # unit (we just ignore unit files) + snap=$(grep 'Where=/snap/' "/etc/systemd/system/$unit"|cut -f3 -d/) + rev=$(grep 'Where=/snap/' "/etc/systemd/system/$unit"|cut -f4 -d/) + if [ -n "$snap" ]; then + echo "Removing snap $snap" + # generated binaries + rm -f "/snap/bin/$snap" + rm -f "/snap/bin/$snap".* + # snap mount dir + umount -l "/snap/$snap/$rev" 2> /dev/null || true + rm -rf "/snap/$snap/$rev" + rm -f "/snap/$snap/current" + # snap data dir + rm -rf "/var/snap/$snap/$rev" + rm -rf "/var/snap/$snap/common" + rm -f "/var/snap/$snap/current" + # opportunistic remove (may fail if there are still revisions left + for d in "/snap/bin" "/snap/$snap" "/var/snap/$snap" "/snap" "/var/snap"; do + if [ -d "$d" ]; then + rmdir --ignore-fail-on-non-empty $d + fi + done + fi + + echo "Removing $unit" + rm -f "/etc/systemd/system/$unit" + rm -f "/etc/systemd/system/multi-user.target.wants/$unit" + done + + echo "Discarding preserved snap namespaces" + # opportunistic as those might not be actually mounted + for mnt in /run/snapd/ns/*.mnt; do + umount -l "$mnt" || true + done + umount -l /run/snapd/ns/ || true + + echo "Removing snapd state" + rm -rf /var/lib/snapd +fi + +#DEBHELPER# diff --git a/debian/snapd.refresh.service b/debian/snapd.refresh.service new file mode 100644 index 00000000..4c337578 --- /dev/null +++ b/debian/snapd.refresh.service @@ -0,0 +1,11 @@ +[Unit] +Description=Automatically refresh installed snaps +After=network-online.target snapd.socket +Requires=snapd.socket +ConditionPathExistsGlob=/snap/*/current +Documentation=man:snap(1) + +[Service] +Type=oneshot +ExecStart=/usr/bin/snap refresh +Environment=SNAP_REFRESH_FROM_TIMER=1 diff --git a/debian/snapd.refresh.timer b/debian/snapd.refresh.timer new file mode 100644 index 00000000..fbc62a93 --- /dev/null +++ b/debian/snapd.refresh.timer @@ -0,0 +1,14 @@ +[Unit] +Description=Timer to automatically refresh installed snaps + +[Timer] +# spread the requests gently +# https://bugs.launchpad.net/snappy/+bug/1537793 +OnCalendar=23,05,11,17:00 +RandomizedDelaySec=6h +AccuracySec=10min +Persistent=true +OnStartupSec=15m + +[Install] +WantedBy=timers.target diff --git a/debian/snapd.service b/debian/snapd.service new file mode 100644 index 00000000..08632259 --- /dev/null +++ b/debian/snapd.service @@ -0,0 +1,11 @@ +[Unit] +Description=Snappy daemon +Requires=snapd.socket + +[Service] +ExecStart=/usr/lib/snapd/snapd +EnvironmentFile=/etc/environment +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/debian/snapd.socket b/debian/snapd.socket new file mode 100644 index 00000000..64605992 --- /dev/null +++ b/debian/snapd.socket @@ -0,0 +1,13 @@ +[Unit] +Description=Socket activation for snappy daemon + +[Socket] +ListenStream=/run/snapd.socket +ListenStream=/run/snapd-snap.socket +SocketMode=0666 +# these are the defaults, but can't hurt to specify them anyway: +SocketUser=root +SocketGroup=root + +[Install] +WantedBy=sockets.target diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 00000000..89ae9db8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/debian/tests/README.md b/debian/tests/README.md new file mode 100644 index 00000000..c32e6db3 --- /dev/null +++ b/debian/tests/README.md @@ -0,0 +1,10 @@ +## Autopkgtest + +In order to run the autopkgtest suite locally you need first to generate an image: + + $ adt-buildvm-ubuntu-cloud -a amd64 -r xenial -v + +This will create a `adt-xenial-amd64-cloud.img` file, then you can run the tests from +the project's root with: + + $ adt-run --unbuilt-tree . --- qemu ./adt-xenial-amd64-cloud.img diff --git a/debian/tests/control b/debian/tests/control new file mode 100644 index 00000000..eb3540b2 --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,12 @@ +Tests: integrationtests +Restrictions: allow-stderr, isolation-container, rw-build-tree, needs-root, breaks-testbed +Depends: @builddeps@, + bzr, + ca-certificates, + git, + golang-golang-x-net-dev, + openssh-server, + snapd, + unity, + x11-utils, + xvfb diff --git a/debian/tests/integrationtests b/debian/tests/integrationtests new file mode 100644 index 00000000..635f5e8a --- /dev/null +++ b/debian/tests/integrationtests @@ -0,0 +1,29 @@ +#!/bin/sh + +set -ex + +# required for the debian adt host +mkdir -p /etc/systemd/system/snapd.service.d/ +if [ "$http_proxy" != "" ]; then + cat <> /etc/environment + echo "https_proxy=$http_proxy" >> /etc/environment +fi +systemctl daemon-reload + +# ensure we can do a connect to localhost +echo ubuntu:ubuntu|chpasswd +sed -i 's/\(PermitRootLogin\|PasswordAuthentication\)\>.*/\1 yes/' /etc/ssh/sshd_config +systemctl reload sshd.service + +# and now run spread against localhost +. /etc/os-release +export GOPATH=/tmp/go +go get -u github.com/snapcore/spread/cmd/spread +/tmp/go/bin/spread -v autopkgtest:${ID}-${VERSION_ID}-$(dpkg --print-architecture) diff --git a/debian/tests/testconfig.json b/debian/tests/testconfig.json new file mode 100644 index 00000000..c1669d09 --- /dev/null +++ b/debian/tests/testconfig.json @@ -0,0 +1,3 @@ +{ + "FromBranch": false +} diff --git a/debian/ubuntu-core-launcher.install b/debian/ubuntu-core-launcher.install new file mode 100644 index 00000000..3bf65c59 --- /dev/null +++ b/debian/ubuntu-core-launcher.install @@ -0,0 +1,2 @@ +usr/bin/ubuntu-core-launcher +usr/share/man/man1/ubuntu-core-launcher.1 diff --git a/debian/ubuntu-snappy-cli.dirs b/debian/ubuntu-snappy-cli.dirs new file mode 100644 index 00000000..e1bbe5bc --- /dev/null +++ b/debian/ubuntu-snappy-cli.dirs @@ -0,0 +1,2 @@ +usr/lib/snapd +var/lib/snapd/snaps diff --git a/dirs/dirs.go b/dirs/dirs.go new file mode 100644 index 00000000..b3a8efa5 --- /dev/null +++ b/dirs/dirs.go @@ -0,0 +1,149 @@ +// -*- 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 . + * + */ + +package dirs + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// the various file paths +var ( + GlobalRootDir string + + SnapMountDir string + SnapBlobDir string + SnapDataDir string + SnapDataHomeGlob string + SnapAppArmorDir string + AppArmorCacheDir string + SnapAppArmorAdditionalDir string + SnapSeccompDir string + SnapMountPolicyDir string + SnapUdevRulesDir string + SnapKModModulesDir string + LocaleDir string + SnapMetaDir string + SnapdSocket string + SnapSocket string + SnapRunNsDir string + + SnapSeedDir string + SnapDeviceDir string + + SnapAssertsDBDir string + SnapTrustedAccountKey string + SnapAssertsSpoolDir string + + SnapStateFile string + + SnapBinariesDir string + SnapServicesDir string + SnapDesktopFilesDir string + SnapBusPolicyDir string + + CloudMetaDataFile string + + ClassicDir string + + LibExecDir string + + XdgRuntimeDirGlob string +) + +var ( + // not exported because it does not honor the global rootdir + snappyDir = filepath.Join("var", "lib", "snapd") +) + +func init() { + // init the global directories at startup + root := os.Getenv("SNAPPY_GLOBAL_ROOT") + + SetRootDir(root) +} + +// StripRootDir strips the custom global root directory from the specified argument. +func StripRootDir(dir string) string { + if !filepath.IsAbs(dir) { + panic(fmt.Sprintf("supplied path is not absolute %q", dir)) + } + if !strings.HasPrefix(dir, GlobalRootDir) { + panic(fmt.Sprintf("supplied path is not related to global root %q", dir)) + } + result, err := filepath.Rel(GlobalRootDir, dir) + if err != nil { + panic(err) + } + return "/" + result +} + +// SetRootDir allows settings a new global root directory, this is useful +// for e.g. chroot operations +func SetRootDir(rootdir string) { + if rootdir == "" { + rootdir = "/" + } + GlobalRootDir = rootdir + + SnapMountDir = filepath.Join(rootdir, "/snap") + SnapDataDir = filepath.Join(rootdir, "/var/snap") + SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/snap/") + SnapAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "profiles") + AppArmorCacheDir = filepath.Join(rootdir, "/var/cache/apparmor") + SnapAppArmorAdditionalDir = filepath.Join(rootdir, snappyDir, "apparmor", "additional") + SnapSeccompDir = filepath.Join(rootdir, snappyDir, "seccomp", "profiles") + SnapMountPolicyDir = filepath.Join(rootdir, snappyDir, "mount") + SnapMetaDir = filepath.Join(rootdir, snappyDir, "meta") + SnapBlobDir = filepath.Join(rootdir, snappyDir, "snaps") + SnapDesktopFilesDir = filepath.Join(rootdir, snappyDir, "desktop", "applications") + SnapRunNsDir = filepath.Join(rootdir, "/run/snapd/ns") + + // keep in sync with the debian/snapd.socket file: + SnapdSocket = filepath.Join(rootdir, "/run/snapd.socket") + SnapSocket = filepath.Join(rootdir, "/run/snapd-snap.socket") + + SnapAssertsDBDir = filepath.Join(rootdir, snappyDir, "assertions") + SnapAssertsSpoolDir = filepath.Join(rootdir, "run/snapd/auto-import") + + SnapStateFile = filepath.Join(rootdir, snappyDir, "state.json") + + SnapSeedDir = filepath.Join(rootdir, snappyDir, "seed") + SnapDeviceDir = filepath.Join(rootdir, snappyDir, "device") + + SnapBinariesDir = filepath.Join(SnapMountDir, "bin") + SnapServicesDir = filepath.Join(rootdir, "/etc/systemd/system") + SnapBusPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/system.d") + + CloudMetaDataFile = filepath.Join(rootdir, "/var/lib/cloud/seed/nocloud-net/meta-data") + + SnapUdevRulesDir = filepath.Join(rootdir, "/etc/udev/rules.d") + + SnapKModModulesDir = filepath.Join(rootdir, "/etc/modules-load.d/") + + LocaleDir = filepath.Join(rootdir, "/usr/share/locale") + ClassicDir = filepath.Join(rootdir, "/writable/classic") + + LibExecDir = filepath.Join(rootdir, "/usr/lib/snapd") + + XdgRuntimeDirGlob = filepath.Join(rootdir, "/run/user/*/") +} diff --git a/dirs/dirs_test.go b/dirs/dirs_test.go new file mode 100644 index 00000000..8e8f39aa --- /dev/null +++ b/dirs/dirs_test.go @@ -0,0 +1,49 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package dirs_test + +import ( + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +var _ = Suite(&DirsTestSuite{}) + +type DirsTestSuite struct{} + +func (s *DirsTestSuite) TestStripRootDir(c *C) { + // strip does nothing if the default (empty) root directory is used + c.Check(dirs.StripRootDir("/foo/bar"), Equals, "/foo/bar") + // strip only works on absolute paths + c.Check(func() { dirs.StripRootDir("relative") }, Panics, `supplied path is not absolute "relative"`) + // with an alternate root + dirs.SetRootDir("/alt/") + defer dirs.SetRootDir("") + // strip behaves as expected, returning absolute paths without the prefix + c.Check(dirs.StripRootDir("/alt/foo/bar"), Equals, "/foo/bar") + // strip only works on paths that begin with the global root directory + c.Check(func() { dirs.StripRootDir("/other/foo/bar") }, Panics, `supplied path is not related to global root "/other/foo/bar"`) +} diff --git a/docs/MOVED.md b/docs/MOVED.md new file mode 100644 index 00000000..24305ce2 --- /dev/null +++ b/docs/MOVED.md @@ -0,0 +1 @@ +### Moved to https://github.com/snapcore/snapd/wiki diff --git a/etc/X11/Xsession.d/65snappy b/etc/X11/Xsession.d/65snappy new file mode 100644 index 00000000..17e92118 --- /dev/null +++ b/etc/X11/Xsession.d/65snappy @@ -0,0 +1,12 @@ +# This file is sourced by Xsession(5), not executed. +# Add the additional snappy desktop path + +if [ -z "$XDG_DATA_DIRS" ]; then + # 60x11-common_xdg_path does not always set XDG_DATA_DIRS + # so we ensure we have sensible defaults here (LP: #1575014) + # as a workaround + XDG_DATA_DIRS=/usr/local/share/:/usr/share/:/var/lib/snapd/desktop +else + XDG_DATA_DIRS="$XDG_DATA_DIRS":/var/lib/snapd/desktop +fi +export XDG_DATA_DIRS diff --git a/etc/profile.d/apps-bin-path.sh b/etc/profile.d/apps-bin-path.sh new file mode 100644 index 00000000..1c3743b7 --- /dev/null +++ b/etc/profile.d/apps-bin-path.sh @@ -0,0 +1,3 @@ +# Expand the $PATH to include /snap/bin which is what snappy applications +# use +PATH=$PATH:/snap/bin diff --git a/gen-coverage.sh b/gen-coverage.sh new file mode 100755 index 00000000..df71e662 --- /dev/null +++ b/gen-coverage.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +set -e + +./run-checks --unit + +go tool cover -html=.coverage/coverage.out -o .coverage/coverage.html + +echo "Coverage html reports are available in .coverage/coverage.html" diff --git a/generate-packaging-dir b/generate-packaging-dir new file mode 100755 index 00000000..d420f1f3 --- /dev/null +++ b/generate-packaging-dir @@ -0,0 +1,30 @@ +#!/bin/sh + +set -e + +if [ ! -d .git ]; then + echo "not running in a git checkout, skipping" + exit 0 +fi + +ID="$1" +VERSION_ID="$2" + +DST="debian-$ID-$VERSION_ID" +rm -rf $DST +mkdir -p "$DST" + +if ! git remote | grep upstream; then + git remote add upstream https://github.com/snapcore/snapd +fi +git fetch upstream + +git ls-tree upstream/"$ID"/"$VERSION_ID" debian/ | while read line ; do + file=$(basename $(echo $line | cut -d " " -f4)) + hash=$(echo $line | cut -d " " -f3) + type=$(echo $line | cut -d " " -f2) + # FIXME: deal with subdirs + if [ "$type" = "blob" ]; then + git cat-file -p $hash > "$DST/$file" + fi +done diff --git a/get-deps.sh b/get-deps.sh new file mode 100755 index 00000000..d6995916 --- /dev/null +++ b/get-deps.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +set -eu + +if [ -z "$(which govendor)" ];then + echo Installing govendor + go get -u github.com/kardianos/govendor +fi +export PATH=$PATH:$GOPATH/bin + +echo Obtaining dependencies +govendor sync diff --git a/i18n/dumb/dumb.go b/i18n/dumb/dumb.go new file mode 100644 index 00000000..11b6b999 --- /dev/null +++ b/i18n/dumb/dumb.go @@ -0,0 +1,24 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package i18n // import "github.com/snapcore/snapd/i18n/dumb" + +func G(s string) string { + return s +} diff --git a/i18n/i18n.go b/i18n/i18n.go new file mode 100644 index 00000000..f5769871 --- /dev/null +++ b/i18n/i18n.go @@ -0,0 +1,102 @@ +// -*- 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 . + * + */ + +package i18n + +//go:generate update-pot + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + /* this is actually "github.com/ojii/gettext.go", however because + https://github.com/ojii/gettext.go/pull/4 + is not merged as of this writtting we want to use this fork + */ + "github.com/mvo5/gettext.go" + + "github.com/snapcore/snapd/osutil" +) + +// TEXTDOMAIN is the message domain used by snappy; see dgettext(3) +// for more information. +var ( + TEXTDOMAIN = "snappy" + locale gettext.Catalog + translations gettext.Translations +) + +func init() { + bindTextDomain(TEXTDOMAIN, "/usr/share/locale") + setLocale("") +} + +func langpackResolver(root string, locale string, domain string) string { + + // first check for the real locale (e.g. de_DE) + // then try to simplify the locale (e.g. de_DE -> de) + locales := []string{locale, strings.SplitN(locale, "_", 2)[0]} + for _, locale := range locales { + r := filepath.Join(locale, "LC_MESSAGES", fmt.Sprintf("%s.mo", domain)) + + // ubuntu uses /usr/lib/locale-langpack and patches the glibc gettext + // implementation + langpack := filepath.Join(root, "..", "locale-langpack", r) + if osutil.FileExists(langpack) { + return langpack + } + + regular := filepath.Join(root, r) + if osutil.FileExists(regular) { + return regular + } + } + + return "" +} + +func bindTextDomain(domain, dir string) { + translations = gettext.NewTranslations(dir, domain, langpackResolver) +} + +func setLocale(loc string) { + if loc == "" { + loc = os.Getenv("LC_MESSAGES") + if loc == "" { + loc = os.Getenv("LANG") + } + } + // de_DE.UTF-8, de_DE@euro all need to get simplified + loc = strings.Split(loc, "@")[0] + loc = strings.Split(loc, ".")[0] + + locale = translations.Locale(loc) +} + +// G is the shorthand for Gettext +func G(msgid string) string { + return locale.Gettext(msgid) +} + +// NG is the shorthand for NGettext +func NG(msgid string, msgidPlural string, n uint32) string { + return locale.NGettext(msgid, msgidPlural, n) +} diff --git a/i18n/i18n_test.go b/i18n/i18n_test.go new file mode 100644 index 00000000..0caa2f66 --- /dev/null +++ b/i18n/i18n_test.go @@ -0,0 +1,129 @@ +// -*- 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 . + * + */ + +package i18n + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +var mockLocalePo = []byte(` +msgid "" +msgstr "" +"Project-Id-Version: snappy\n" +"Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n" +"POT-Creation-Date: 2015-06-16 09:08+0200\n" +"Language: en_DK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;>\n" + +msgid "plural_1" +msgid_plural "plural_2" +msgstr[0] "translated plural_1" +msgstr[1] "translated plural_2" + +msgid "singular" +msgstr "translated singular" +`) + +func makeMockTranslations(c *C, localeDir string) { + fullLocaleDir := filepath.Join(localeDir, "en_DK", "LC_MESSAGES") + err := os.MkdirAll(fullLocaleDir, 0755) + c.Assert(err, IsNil) + + po := filepath.Join(fullLocaleDir, "snappy-test.po") + mo := filepath.Join(fullLocaleDir, "snappy-test.mo") + err = ioutil.WriteFile(po, mockLocalePo, 0644) + c.Assert(err, IsNil) + + cmd := exec.Command("msgfmt", po, "--output-file", mo) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + c.Assert(err, IsNil) +} + +type i18nTestSuite struct { + origLang string + origLcMessages string +} + +var _ = Suite(&i18nTestSuite{}) + +func (s *i18nTestSuite) SetUpTest(c *C) { + // this dir contains a special hand-crafted en_DK/snappy-test.mo + // file + localeDir := c.MkDir() + makeMockTranslations(c, localeDir) + + // we use a custom test mo file + TEXTDOMAIN = "snappy-test" + + s.origLang = os.Getenv("LANG") + s.origLcMessages = os.Getenv("LC_MESSAGES") + + bindTextDomain("snappy-test", localeDir) + os.Setenv("LANG", "en_DK.UTF-8") + setLocale("") +} + +func (s *i18nTestSuite) TearDownTest(c *C) { + os.Setenv("LANG", s.origLang) + os.Setenv("LC_MESSAGES", s.origLcMessages) +} + +func (s *i18nTestSuite) TestTranslatedSingular(c *C) { + // no G() to avoid adding the test string to snappy-pot + var Gtest = G + c.Assert(Gtest("singular"), Equals, "translated singular") +} + +func (s *i18nTestSuite) TestTranslatesPlural(c *C) { + // no NG() to avoid adding the test string to snappy-pot + var NGtest = NG + c.Assert(NGtest("plural_1", "plural_2", 1), Equals, "translated plural_1") +} + +func (s *i18nTestSuite) TestTranslatedMissingLangNoCrash(c *C) { + setLocale("invalid") + + // no G() to avoid adding the test string to snappy-pot + var Gtest = G + c.Assert(Gtest("singular"), Equals, "singular") +} + +func (s *i18nTestSuite) TestInvalidTextDomainDir(c *C) { + bindTextDomain("snappy-test", "/random/not/existing/dir") + setLocale("invalid") + + // no G() to avoid adding the test string to snappy-pot + var Gtest = G + c.Assert(Gtest("singular"), Equals, "singular") +} diff --git a/i18n/xgettext-go/main.go b/i18n/xgettext-go/main.go new file mode 100644 index 00000000..b7cc234b --- /dev/null +++ b/i18n/xgettext-go/main.go @@ -0,0 +1,306 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "io/ioutil" + "log" + "os" + "sort" + "strings" + "time" + + "github.com/jessevdk/go-flags" +) + +type msgID struct { + msgidPlural string + comment string + fname string + line int + formatHint string +} + +var msgIDs map[string][]msgID + +func formatComment(com string) string { + out := "" + for _, rawline := range strings.Split(com, "\n") { + line := rawline + line = strings.TrimPrefix(line, "//") + line = strings.TrimPrefix(line, "/*") + line = strings.TrimSuffix(line, "*/") + line = strings.TrimSpace(line) + if line != "" { + out += fmt.Sprintf("#. %s\n", line) + } + } + + return out +} + +func findCommentsForTranslation(fset *token.FileSet, f *ast.File, posCall token.Position) string { + com := "" + for _, cg := range f.Comments { + // search for all comments in the previous line + for i := len(cg.List) - 1; i >= 0; i-- { + c := cg.List[i] + + posComment := fset.Position(c.End()) + //println(posCall.Line, posComment.Line, c.Text) + if posCall.Line == posComment.Line+1 { + posCall = posComment + com = fmt.Sprintf("%s\n%s", c.Text, com) + } + } + } + + // only return if we have a matching prefix + formatedComment := formatComment(com) + needle := fmt.Sprintf("#. %s", opts.AddCommentsTag) + if !strings.HasPrefix(formatedComment, needle) { + formatedComment = "" + } + + return formatedComment +} + +func constructValue(val interface{}) string { + switch val.(type) { + case *ast.BasicLit: + return val.(*ast.BasicLit).Value + // this happens for constructs like: + // gettext.Gettext("foo" + "bar") + case *ast.BinaryExpr: + // we only support string concat + if val.(*ast.BinaryExpr).Op != token.ADD { + return "" + } + left := constructValue(val.(*ast.BinaryExpr).X) + // strip right " (or `) + left = left[0 : len(left)-1] + right := constructValue(val.(*ast.BinaryExpr).Y) + // strip left " (or `) + right = right[1:] + return left + right + default: + panic(fmt.Sprintf("unknown type: %v", val)) + } +} + +func inspectNodeForTranslations(fset *token.FileSet, f *ast.File, n ast.Node) bool { + // FIXME: this assume we always have a "gettext.Gettext" style keyword + l := strings.Split(opts.Keyword, ".") + gettextSelector := l[0] + gettextFuncName := l[1] + + l = strings.Split(opts.KeywordPlural, ".") + gettextSelectorPlural := l[0] + gettextFuncNamePlural := l[1] + + switch x := n.(type) { + case *ast.CallExpr: + if sel, ok := x.Fun.(*ast.SelectorExpr); ok { + i18nStr := "" + i18nStrPlural := "" + if sel.Sel.Name == gettextFuncNamePlural && sel.X.(*ast.Ident).Name == gettextSelectorPlural { + i18nStr = x.Args[0].(*ast.BasicLit).Value + i18nStrPlural = x.Args[1].(*ast.BasicLit).Value + } + + if sel.Sel.Name == gettextFuncName && sel.X.(*ast.Ident).Name == gettextSelector { + i18nStr = constructValue(x.Args[0]) + } + + formatI18nStr := func(s string) string { + if s == "" { + return "" + } + // the "`" is special + if s[0] == '`' { + // replace inner " with \" + s = strings.Replace(s, "\"", "\\\"", -1) + // replace \n with \\n + s = strings.Replace(s, "\n", "\\n", -1) + } + // strip leading and trailing " (or `) + s = s[1 : len(s)-1] + return s + } + + // FIXME: too simplistic(?), no %% is considered + formatHint := "" + if strings.Contains(i18nStr, "%") || strings.Contains(i18nStrPlural, "%") { + // well, not quite correct but close enough + formatHint = "c-format" + } + + if i18nStr != "" { + msgidStr := formatI18nStr(i18nStr) + posCall := fset.Position(n.Pos()) + msgIDs[msgidStr] = append(msgIDs[msgidStr], msgID{ + formatHint: formatHint, + msgidPlural: formatI18nStr(i18nStrPlural), + fname: posCall.Filename, + line: posCall.Line, + comment: findCommentsForTranslation(fset, f, posCall), + }) + } + } + } + + return true +} + +func processFiles(args []string) error { + // go over the input files + msgIDs = make(map[string][]msgID) + + fset := token.NewFileSet() + for _, fname := range args { + if err := processSingleGoSource(fset, fname); err != nil { + return err + } + } + + return nil +} + +func processSingleGoSource(fset *token.FileSet, fname string) error { + fnameContent, err := ioutil.ReadFile(fname) + if err != nil { + panic(err) + } + + // Create the AST by parsing src. + f, err := parser.ParseFile(fset, fname, fnameContent, parser.ParseComments) + if err != nil { + panic(err) + } + + ast.Inspect(f, func(n ast.Node) bool { + return inspectNodeForTranslations(fset, f, n) + }) + + return nil +} + +var formatTime = func() string { + return time.Now().Format("2006-01-02 15:04-0700") +} + +func writePotFile(out io.Writer) { + + header := fmt.Sprintf(`# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "Project-Id-Version: %s\n" + "Report-Msgid-Bugs-To: %s\n" + "POT-Creation-Date: %s\n" + "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" + "Last-Translator: FULL NAME \n" + "Language-Team: LANGUAGE \n" + "Language: \n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=CHARSET\n" + "Content-Transfer-Encoding: 8bit\n" + +`, opts.PackageName, opts.MsgIDBugsAddress, formatTime()) + fmt.Fprintf(out, "%s", header) + + // yes, this is the way to do it in go + sortedKeys := []string{} + for k := range msgIDs { + sortedKeys = append(sortedKeys, k) + } + if opts.SortOutput { + sort.Strings(sortedKeys) + } + + // FIXME: use template here? + for _, k := range sortedKeys { + msgidList := msgIDs[k] + for _, msgid := range msgidList { + if opts.AddComments || opts.AddCommentsTag != "" { + fmt.Fprintf(out, "%s", msgid.comment) + } + } + if !opts.NoLocation { + fmt.Fprintf(out, "#:") + for _, msgid := range msgidList { + fmt.Fprintf(out, " %s:%d", msgid.fname, msgid.line) + } + fmt.Fprintf(out, "\n") + } + msgid := msgidList[0] + if msgid.formatHint != "" { + fmt.Fprintf(out, "#, %s\n", msgid.formatHint) + } + var formatOutput = func(in string) string { + // split string with \n into multiple lines + // to make the output nicer + out := strings.Replace(in, "\\n", "\\n\"\n \"", -1) + // cleanup too aggressive splitting (empty "" lines) + return strings.TrimSuffix(out, "\"\n \"") + } + fmt.Fprintf(out, "msgid \"%v\"\n", formatOutput(k)) + if msgid.msgidPlural != "" { + fmt.Fprintf(out, "msgid_plural \"%v\"\n", formatOutput(msgid.msgidPlural)) + fmt.Fprintf(out, "msgstr[0] \"\"\n") + fmt.Fprintf(out, "msgstr[1] \"\"\n") + } else { + fmt.Fprintf(out, "msgstr \"\"\n") + } + fmt.Fprintf(out, "\n") + } + +} + +// FIXME: this must be setable via go-flags +var opts struct { + Output string `short:"o" long:"output" description:"output to specified file"` + + AddComments bool `short:"c" long:"add-comments" description:"place all comment blocks preceding keyword lines in output file"` + + AddCommentsTag string `long:"add-comments-tag" description:"place comment blocks starting with TAG and prceding keyword lines in output file"` + + SortOutput bool `short:"s" long:"sort-output" description:"generate sorted output"` + + NoLocation bool `long:"no-location" description:"do not write '#: filename:line' lines"` + + MsgIDBugsAddress string `long:"msgid-bugs-address" default:"EMAIL" description:"set report address for msgid bugs"` + + PackageName string `long:"package-name" description:"set package name in output"` + + Keyword string `short:"k" long:"keyword" default:"gettext.Gettext" description:"look for WORD as the keyword for singular strings"` + KeywordPlural string `long:"keyword-plural" default:"gettext.NGettext" description:"look for WORD as the keyword for plural strings"` +} + +func main() { + // parse args + args, err := flags.ParseArgs(&opts, os.Args) + if err != nil { + log.Fatalf("ParseArgs failed %s", err) + } + + if err := processFiles(args[1:]); err != nil { + log.Fatalf("processFiles failed with: %s", err) + } + + out := os.Stdout + if opts.Output != "" { + var err error + out, err = os.Create(opts.Output) + if err != nil { + log.Fatalf("failed to create %s: %s", opts.Output, err) + } + } + writePotFile(out) +} diff --git a/i18n/xgettext-go/main_test.go b/i18n/xgettext-go/main_test.go new file mode 100644 index 00000000..02183daf --- /dev/null +++ b/i18n/xgettext-go/main_test.go @@ -0,0 +1,516 @@ +// -*- 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 . + * + */ + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type xgettextTestSuite struct { +} + +var _ = Suite(&xgettextTestSuite{}) + +// test helper +func makeGoSourceFile(c *C, content []byte) string { + fname := filepath.Join(c.MkDir(), "foo.go") + err := ioutil.WriteFile(fname, []byte(content), 0644) + c.Assert(err, IsNil) + + return fname +} + +func (s *xgettextTestSuite) SetUpTest(c *C) { + // our test defaults + opts.NoLocation = false + opts.AddCommentsTag = "TRANSLATORS:" + opts.Keyword = "i18n.G" + opts.KeywordPlural = "i18n.NG" + opts.SortOutput = true + opts.PackageName = "snappy" + opts.MsgIDBugsAddress = "snappy-devel@lists.ubuntu.com" + + // mock time + formatTime = func() string { + return "2015-06-30 14:48+0200" + } +} + +func (s *xgettextTestSuite) TestFormatComment(c *C) { + var tests = []struct { + in string + out string + }{ + {in: "// foo ", out: "#. foo\n"}, + {in: "/* foo */", out: "#. foo\n"}, + {in: "/* foo\n */", out: "#. foo\n"}, + {in: "/* foo\nbar */", out: "#. foo\n#. bar\n"}, + } + + for _, test := range tests { + c.Assert(formatComment(test.in), Equals, test.out) + } +} + +func (s *xgettextTestSuite) TestProcessFilesSimple(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + // TRANSLATORS: foo comment + i18n.G("foo") +} +`)) + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + c.Assert(msgIDs, DeepEquals, map[string][]msgID{ + "foo": { + { + comment: "#. TRANSLATORS: foo comment\n", + fname: fname, + line: 5, + }, + }, + }) +} + +func (s *xgettextTestSuite) TestProcessFilesMultiple(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + // TRANSLATORS: foo comment + i18n.G("foo") + + // TRANSLATORS: bar comment + i18n.G("foo") +} +`)) + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + c.Assert(msgIDs, DeepEquals, map[string][]msgID{ + "foo": { + { + comment: "#. TRANSLATORS: foo comment\n", + fname: fname, + line: 5, + }, + { + comment: "#. TRANSLATORS: bar comment\n", + fname: fname, + line: 8, + }, + }, + }) +} + +const header = `# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "Project-Id-Version: snappy\n" + "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n" + "POT-Creation-Date: 2015-06-30 14:48+0200\n" + "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" + "Last-Translator: FULL NAME \n" + "Language-Team: LANGUAGE \n" + "Language: \n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=CHARSET\n" + "Content-Transfer-Encoding: 8bit\n" +` + +func (s *xgettextTestSuite) TestWriteOutputSimple(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + fname: "fname", + line: 2, + comment: "#. foo\n", + }, + }, + } + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#. foo +#: fname:2 +msgid "foo" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputMultiple(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + fname: "fname", + line: 2, + comment: "#. comment1\n", + }, + { + fname: "fname", + line: 4, + comment: "#. comment2\n", + }, + }, + } + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#. comment1 +#. comment2 +#: fname:2 fname:4 +msgid "foo" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputNoComment(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + fname: "fname", + line: 2, + }, + }, + } + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: fname:2 +msgid "foo" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputNoLocation(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + fname: "fname", + line: 2, + }, + }, + } + + opts.NoLocation = true + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +msgid "foo" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputFormatHint(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + fname: "fname", + line: 2, + formatHint: "c-format", + }, + }, + } + + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: fname:2 +#, c-format +msgid "foo" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputPlural(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + msgidPlural: "plural", + fname: "fname", + line: 2, + }, + }, + } + + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: fname:2 +msgid "foo" +msgid_plural "plural" +msgstr[0] "" +msgstr[1] "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputSorted(c *C) { + msgIDs = map[string][]msgID{ + "aaa": { + { + fname: "fname", + line: 2, + }, + }, + "zzz": { + { + fname: "fname", + line: 2, + }, + }, + } + + opts.SortOutput = true + // we need to run this a bunch of times as the ordering might + // be right by pure chance + for i := 0; i < 10; i++ { + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: fname:2 +msgid "aaa" +msgstr "" + +#: fname:2 +msgid "zzz" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) + } +} + +func (s *xgettextTestSuite) TestIntegration(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + // TRANSLATORS: foo comment + // with multiple lines + i18n.G("foo") + + // this comment has no translators tag + i18n.G("abc") + + // TRANSLATORS: plural + i18n.NG("singular", "plural", 99) + + i18n.G("zz %s") +} +`)) + + // a real integration test :) + outName := filepath.Join(c.MkDir(), "snappy.pot") + os.Args = []string{"test-binary", + "--output", outName, + "--keyword", "i18n.G", + "--keyword-plural", "i18n.NG", + "--msgid-bugs-address", "snappy-devel@lists.ubuntu.com", + "--package-name", "snappy", + fname, + } + main() + + // verify its what we expect + got, err := ioutil.ReadFile(outName) + c.Assert(err, IsNil) + expected := fmt.Sprintf(`%s +#: %[2]s:9 +msgid "abc" +msgstr "" + +#. TRANSLATORS: foo comment +#. with multiple lines +#: %[2]s:6 +msgid "foo" +msgstr "" + +#. TRANSLATORS: plural +#: %[2]s:12 +msgid "singular" +msgid_plural "plural" +msgstr[0] "" +msgstr[1] "" + +#: %[2]s:14 +#, c-format +msgid "zz %%s" +msgstr "" + +`, header, fname) + c.Assert(string(got), Equals, expected) +} + +func (s *xgettextTestSuite) TestProcessFilesConcat(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + // TRANSLATORS: foo comment + i18n.G("foo\n" + "bar\n" + "baz") +} +`)) + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + c.Assert(msgIDs, DeepEquals, map[string][]msgID{ + "foo\\nbar\\nbaz": { + { + comment: "#. TRANSLATORS: foo comment\n", + fname: fname, + line: 5, + }, + }, + }) +} + +func (s *xgettextTestSuite) TestProcessFilesWithQuote(c *C) { + fname := makeGoSourceFile(c, []byte(fmt.Sprintf(`package main + +func main() { + i18n.G(%[1]s foo "bar"%[1]s) +} +`, "`"))) + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: %[2]s:4 +msgid " foo \"bar\"" +msgstr "" + +`, header, fname) + c.Check(out.String(), Equals, expected) + +} + +func (s *xgettextTestSuite) TestWriteOutputMultilines(c *C) { + msgIDs = map[string][]msgID{ + "foo\\nbar\\nbaz": { + { + fname: "fname", + line: 2, + comment: "#. foo\n", + }, + }, + } + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + expected := fmt.Sprintf(`%s +#. foo +#: fname:2 +msgid "foo\n" + "bar\n" + "baz" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputTidy(c *C) { + msgIDs = map[string][]msgID{ + "foo\\nbar\\nbaz": { + { + fname: "fname", + line: 2, + }, + }, + "zzz\\n": { + { + fname: "fname", + line: 4, + }, + }, + } + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + expected := fmt.Sprintf(`%s +#: fname:2 +msgid "foo\n" + "bar\n" + "baz" +msgstr "" + +#: fname:4 +msgid "zzz\n" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestProcessFilesWithDoubleQuote(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + i18n.G("foo \"bar\"") +} +`)) + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: %[2]s:4 +msgid "foo \"bar\"" +msgstr "" + +`, header, fname) + c.Check(out.String(), Equals, expected) + +} diff --git a/image/export_test.go b/image/export_test.go new file mode 100644 index 00000000..d7452ba5 --- /dev/null +++ b/image/export_test.go @@ -0,0 +1,28 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package image + +var ( + LocalSnaps = localSnaps + DecodeModelAssertion = decodeModelAssertion + DownloadUnpackGadget = downloadUnpackGadget + BootstrapToRootDir = bootstrapToRootDir + InstallCloudConfig = installCloudConfig +) diff --git a/image/helpers.go b/image/helpers.go new file mode 100644 index 00000000..e5981248 --- /dev/null +++ b/image/helpers.go @@ -0,0 +1,123 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package image + +// TODO: put these in appropriate package(s) once they are clarified a bit more + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/snapasserts" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/store" + + "golang.org/x/net/context" +) + +// DownloadOptions carries options for downloading snaps plus assertions. +type DownloadOptions struct { + TargetDir string + Channel string + DevMode bool + User *auth.UserState +} + +// A Store can find metadata on snaps, download snaps and fetch assertions. +type Store interface { + SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) + Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState) error + + Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) +} + +// DownloadSnap downloads the snap with the given name and optionally revision using the provided store and options. It returns the final full path of the snap inside the opts.TargetDir and a snap.Info for the snap. +func DownloadSnap(sto Store, name string, revision snap.Revision, opts *DownloadOptions) (targetFn string, info *snap.Info, err error) { + if opts == nil { + opts = &DownloadOptions{} + } + + targetDir := opts.TargetDir + if targetDir == "" { + pwd, err := os.Getwd() + if err != nil { + return "", nil, err + } + targetDir = pwd + } + + spec := store.SnapSpec{ + Name: name, + Channel: opts.Channel, + Revision: revision, + } + snap, err := sto.SnapInfo(spec, opts.User) + if err != nil { + return "", nil, fmt.Errorf("cannot find snap %q: %v", name, err) + } + + baseName := filepath.Base(snap.MountFile()) + targetFn = filepath.Join(targetDir, baseName) + + pb := progress.NewTextProgress() + if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, opts.User); err != nil { + return "", nil, err + } + + return targetFn, snap, nil +} + +// StoreAssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them. +func StoreAssertionFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher { + retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) { + return sto.Assertion(ref.Type, ref.PrimaryKey, dlOpts.User) + } + save2 := func(a asserts.Assertion) error { + // for checking + err := db.Add(a) + if err != nil { + if _, ok := err.(*asserts.RevisionError); ok { + return nil + } + return fmt.Errorf("cannot add assertion %v: %v", a.Ref(), err) + } + return save(a) + } + return asserts.NewFetcher(db, retrieve, save2) +} + +// FetchAndCheckSnapAssertions fetches and cross checks the snap assertions matching the given snap file using the provided asserts.Fetcher and assertion database. +func FetchAndCheckSnapAssertions(snapPath string, info *snap.Info, f asserts.Fetcher, db asserts.RODatabase) error { + sha3_384, size, err := asserts.SnapFileSHA3_384(snapPath) + if err != nil { + return err + } + + if err := snapasserts.FetchSnapAssertions(f, sha3_384); err != nil { + return fmt.Errorf("cannot fetch snap signatures/assertions: %v", err) + } + + // cross checks + return snapasserts.CrossCheck(info.Name(), sha3_384, size, &info.SideInfo, db) +} diff --git a/image/image.go b/image/image.go new file mode 100644 index 00000000..c2afaa6d --- /dev/null +++ b/image/image.go @@ -0,0 +1,468 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package image + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/partition" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/squashfs" + "github.com/snapcore/snapd/store" +) + +var ( + Stdout io.Writer = os.Stdout +) + +type Options struct { + Snaps []string + RootDir string + Channel string + ModelFile string + GadgetUnpackDir string +} + +type localInfos struct { + // path to info for local snaps + pathToInfo map[string]*snap.Info + // name to path + nameToPath map[string]string +} + +func (li *localInfos) Name(pathOrName string) string { + if info := li.pathToInfo[pathOrName]; info != nil { + return info.Name() + } + return pathOrName +} + +func (li *localInfos) PreferLocal(name string) string { + if path := li.Path(name); path != "" { + return path + } + return name +} + +func (li *localInfos) Path(name string) string { + return li.nameToPath[name] +} + +func (li *localInfos) Info(name string) *snap.Info { + if p := li.nameToPath[name]; p != "" { + return li.pathToInfo[p] + } + return nil +} + +func localSnaps(opts *Options) (*localInfos, error) { + local := make(map[string]*snap.Info) + nameToPath := make(map[string]string) + for _, snapName := range opts.Snaps { + if strings.HasSuffix(snapName, ".snap") && osutil.FileExists(snapName) { + snapFile, err := snap.Open(snapName) + if err != nil { + return nil, err + } + info, err := snap.ReadInfoFromSnapFile(snapFile, nil) + if err != nil { + return nil, err + } + // local snap gets local revision + info.Revision = snap.R(-1) + nameToPath[info.Name()] = snapName + local[snapName] = info + } + } + return &localInfos{ + pathToInfo: local, + nameToPath: nameToPath, + }, nil +} + +func Prepare(opts *Options) error { + model, err := decodeModelAssertion(opts) + if err != nil { + return err + } + + local, err := localSnaps(opts) + if err != nil { + return err + } + + sto := makeStore(model) + + if err := downloadUnpackGadget(sto, model, opts, local); err != nil { + return err + } + + return bootstrapToRootDir(sto, model, opts, local) +} + +// these are postponed, not implemented or abandoned, not finalized, +// don't let them sneak in into a used model assertion +var reserved = []string{"core", "os", "class", "allowed-modes"} + +func decodeModelAssertion(opts *Options) (*asserts.Model, error) { + fn := opts.ModelFile + + rawAssert, err := ioutil.ReadFile(fn) + if err != nil { + return nil, fmt.Errorf("cannot read model assertion: %s", err) + } + + ass, err := asserts.Decode(rawAssert) + if err != nil { + return nil, fmt.Errorf("cannot decode model assertion %q: %s", fn, err) + } + modela, ok := ass.(*asserts.Model) + if !ok { + return nil, fmt.Errorf("assertion in %q is not a model assertion", fn) + } + + for _, rsvd := range reserved { + if modela.Header(rsvd) != nil { + return nil, fmt.Errorf("model assertion cannot have reserved/unsupported header %q set", rsvd) + } + } + + return modela, nil +} + +func downloadUnpackGadget(sto Store, model *asserts.Model, opts *Options, local *localInfos) error { + if err := os.MkdirAll(opts.GadgetUnpackDir, 0755); err != nil { + return fmt.Errorf("cannot create gadget unpack dir %q: %s", opts.GadgetUnpackDir, err) + } + + dlOpts := &DownloadOptions{ + TargetDir: opts.GadgetUnpackDir, + Channel: opts.Channel, + } + snapFn, _, err := acquireSnap(sto, model.Gadget(), dlOpts, local) + if err != nil { + return err + } + // FIXME: jumping through layers here, we need to make + // unpack part of the container interface (again) + snap := squashfs.New(snapFn) + return snap.Unpack("*", opts.GadgetUnpackDir) +} + +func acquireSnap(sto Store, name string, dlOpts *DownloadOptions, local *localInfos) (downloadedSnap string, info *snap.Info, err error) { + if info := local.Info(name); info != nil { + // local snap to install (unasserted only for now) + p := local.Path(name) + dst, err := copyLocalSnapFile(p, dlOpts.TargetDir, info) + if err != nil { + return "", nil, err + } + return dst, info, nil + } + return DownloadSnap(sto, name, snap.R(0), dlOpts) +} + +type addingFetcher struct { + asserts.Fetcher + addedRefs []*asserts.Ref +} + +func makeFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Database) *addingFetcher { + var f addingFetcher + save := func(a asserts.Assertion) error { + f.addedRefs = append(f.addedRefs, a.Ref()) + return nil + } + f.Fetcher = StoreAssertionFetcher(sto, dlOpts, db, save) + return &f + +} + +func installCloudConfig(gadgetDir string) error { + var err error + + cloudDir := filepath.Join(dirs.GlobalRootDir, "/etc/cloud") + if err := os.MkdirAll(cloudDir, 0755); err != nil { + return err + } + + cloudConfig := filepath.Join(gadgetDir, "cloud.conf") + if osutil.FileExists(cloudConfig) { + dst := filepath.Join(cloudDir, "cloud.cfg") + err = osutil.CopyFile(cloudConfig, dst, osutil.CopyFlagOverwrite) + } else { + dst := filepath.Join(cloudDir, "cloud-init.disabled") + err = osutil.AtomicWriteFile(dst, nil, 0644, 0) + } + return err +} + +const defaultCore = "core" + +func bootstrapToRootDir(sto Store, model *asserts.Model, opts *Options, local *localInfos) error { + // FIXME: try to avoid doing this + if opts.RootDir != "" { + dirs.SetRootDir(opts.RootDir) + defer dirs.SetRootDir("/") + } + + // sanity check target + if osutil.FileExists(dirs.SnapStateFile) { + return fmt.Errorf("cannot bootstrap over existing system") + } + + // TODO: developer database in home or use snapd (but need + // a bit more API there, potential issues when crossing stores/series) + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: sysdb.Trusted(), + }) + if err != nil { + return err + } + f := makeFetcher(sto, &DownloadOptions{}, db) + + if err := f.Save(model); err != nil { + if !osutil.GetenvBool("UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_MODEL") { + return fmt.Errorf("cannot fetch and check prerequisites for the model assertion: %v", err) + } else { + logger.Noticef("Cannot fetch and check prerequisites for the model assertion, it will not be copied into the image: %v", err) + f.addedRefs = nil + } + } + + // put snaps in place + if err := os.MkdirAll(dirs.SnapBlobDir, 0755); err != nil { + return err + } + + snapSeedDir := filepath.Join(dirs.SnapSeedDir, "snaps") + assertSeedDir := filepath.Join(dirs.SnapSeedDir, "assertions") + dlOpts := &DownloadOptions{ + TargetDir: snapSeedDir, + Channel: opts.Channel, + DevMode: false, // XXX: should this be true? + } + + for _, d := range []string{snapSeedDir, assertSeedDir} { + if err := os.MkdirAll(d, 0755); err != nil { + return err + } + } + + snaps := []string{} + // core,kernel,gadget first + snaps = append(snaps, local.PreferLocal(defaultCore)) + snaps = append(snaps, local.PreferLocal(model.Kernel())) + snaps = append(snaps, local.PreferLocal(model.Gadget())) + // then required and the user requested stuff + for _, snapName := range model.RequiredSnaps() { + snaps = append(snaps, local.PreferLocal(snapName)) + } + snaps = append(snaps, opts.Snaps...) + + seen := make(map[string]bool) + downloadedSnapsInfo := map[string]*snap.Info{} + var seedYaml snap.Seed + for _, snapName := range snaps { + name := local.Name(snapName) + if seen[name] { + fmt.Fprintf(Stdout, "%s already prepared, skipping\n", name) + continue + } + + if name != snapName { + fmt.Fprintf(Stdout, "Copying %q (%s)\n", snapName, name) + } else { + fmt.Fprintf(Stdout, "Fetching %s\n", snapName) + } + + fn, info, err := acquireSnap(sto, name, dlOpts, local) + if err != nil { + return err + } + + seen[name] = true + + // if it comes from the store fetch the snap assertions too + // TODO: support somehow including available assertions + // also for local snaps + if info.SnapID != "" { + err = FetchAndCheckSnapAssertions(fn, info, f, db) + if err != nil { + return err + } + } + + typ := info.Type + // kernel/os are required for booting + if typ == snap.TypeKernel || typ == snap.TypeOS { + dst := filepath.Join(dirs.SnapBlobDir, filepath.Base(fn)) + if err := osutil.CopyFile(fn, dst, 0); err != nil { + return err + } + // store the snap.Info for kernel/os so + // that the bootload can DTRT + downloadedSnapsInfo[dst] = info + } + + // set seed.yaml + seedYaml.Snaps = append(seedYaml.Snaps, &snap.SeedSnap{ + Name: info.Name(), + SnapID: info.SnapID, // cross-ref + Channel: info.Channel, + File: filepath.Base(fn), + DevMode: info.NeedsDevMode(), + // no assertions for this snap were put in the seed + Unasserted: info.SnapID == "", + }) + } + + for _, aRef := range f.addedRefs { + var afn string + // the names don't matter in practice as long as they don't conflict + if aRef.Type == asserts.ModelType { + afn = "model" + } else { + afn = fmt.Sprintf("%s.%s", strings.Join(aRef.PrimaryKey, ","), aRef.Type.Name) + } + a, err := aRef.Resolve(db.Find) + if err != nil { + return fmt.Errorf("internal error: lost saved assertion") + } + err = ioutil.WriteFile(filepath.Join(assertSeedDir, afn), asserts.Encode(a), 0644) + if err != nil { + return err + } + } + + // TODO: add the refs as an assertions list of maps section to seed.yaml + + seedFn := filepath.Join(dirs.SnapSeedDir, "seed.yaml") + if err := seedYaml.Write(seedFn); err != nil { + return fmt.Errorf("cannot write seed.yaml: %s", err) + } + + // now do the bootloader stuff + if err := partition.InstallBootConfig(opts.GadgetUnpackDir); err != nil { + return err + } + + if err := setBootvars(downloadedSnapsInfo); err != nil { + return err + } + + // and the cloud-init things + if err := installCloudConfig(opts.GadgetUnpackDir); err != nil { + return err + } + + return nil +} + +func setBootvars(downloadedSnapsInfo map[string]*snap.Info) error { + // Set bootvars for kernel/core snaps so the system boots and + // does the first-time initialization. There is also no + // mounted kernel/core snap, but just the blobs. + bootloader, err := partition.FindBootloader() + if err != nil { + return fmt.Errorf("cannot set kernel/core boot variables: %s", err) + } + + snaps, err := filepath.Glob(filepath.Join(dirs.SnapBlobDir, "*.snap")) + if len(snaps) == 0 || err != nil { + return fmt.Errorf("internal error: cannot find core/kernel snap") + } + + m := map[string]string{ + "snap_mode": "", + "snap_try_core": "", + "snap_try_kernel": "", + } + for _, fn := range snaps { + bootvar := "" + + info := downloadedSnapsInfo[fn] + switch info.Type { + case snap.TypeOS: + bootvar = "snap_core" + case snap.TypeKernel: + bootvar = "snap_kernel" + if err := extractKernelAssets(fn, info); err != nil { + return err + } + } + + if bootvar != "" { + name := filepath.Base(fn) + m[bootvar] = name + } + } + if err := bootloader.SetBootVars(m); err != nil { + return err + } + + return nil +} + +func runCommand(cmdStr ...string) error { + cmd := exec.Command(cmdStr[0], cmdStr[1:]...) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("cannot run %v: %s", cmdStr, osutil.OutputErr(output, err)) + } + return nil +} + +func extractKernelAssets(snapPath string, info *snap.Info) error { + snapf, err := snap.Open(snapPath) + if err != nil { + return err + } + + if err := boot.ExtractKernelAssets(info, snapf); err != nil { + return err + } + return nil +} + +func copyLocalSnapFile(snapPath, targetDir string, info *snap.Info) (dstPath string, err error) { + dst := filepath.Join(targetDir, filepath.Base(info.MountFile())) + return dst, osutil.CopyFile(snapPath, dst, 0) +} + +func makeStore(model *asserts.Model) Store { + cfg := store.DefaultConfig() + cfg.Architecture = model.Architecture() + cfg.Series = model.Series() + cfg.StoreID = model.Store() + return store.New(cfg, nil) +} diff --git a/image/image_test.go b/image/image_test.go new file mode 100644 index 00000000..21255a6e --- /dev/null +++ b/image/image_test.go @@ -0,0 +1,625 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 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 . + * + */ + +package image_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "golang.org/x/net/context" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/image" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/partition" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/testutil" +) + +func Test(t *testing.T) { TestingT(t) } + +type imageSuite struct { + root string + bootloader *boottest.MockBootloader + + stdout *bytes.Buffer + + downloadedSnaps map[string]string + storeSnapInfo map[string]*snap.Info + + storeSigning *assertstest.StoreStack + brandSigning *assertstest.SigningDB + + model *asserts.Model +} + +var _ = Suite(&imageSuite{}) + +func (s *imageSuite) SetUpTest(c *C) { + s.root = c.MkDir() + s.bootloader = boottest.NewMockBootloader("grub", c.MkDir()) + partition.ForceBootloader(s.bootloader) + + s.stdout = bytes.NewBuffer(nil) + image.Stdout = s.stdout + s.downloadedSnaps = make(map[string]string) + s.storeSnapInfo = make(map[string]*snap.Info) + + rootPrivKey, _ := assertstest.GenerateKey(1024) + storePrivKey, _ := assertstest.GenerateKey(752) + s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey) + + brandPrivKey, _ := assertstest.GenerateKey(752) + s.brandSigning = assertstest.NewSigningDB("my-brand", brandPrivKey) + + brandAcct := assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{ + "account-id": "my-brand", + "verification": "certified", + }, "") + s.storeSigning.Add(brandAcct) + + brandAccKey := assertstest.NewAccountKey(s.storeSigning, brandAcct, nil, brandPrivKey.PublicKey(), "") + s.storeSigning.Add(brandAccKey) + + model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{ + "series": "16", + "authority-id": "my-brand", + "brand-id": "my-brand", + "model": "my-model", + "architecture": "amd64", + "gadget": "pc", + "kernel": "pc-kernel", + "required-snaps": []interface{}{"required-snap1"}, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + s.model = model.(*asserts.Model) +} + +func (s *imageSuite) addSystemSnapAssertions(c *C, snapName string) { + snapID := snapName + "-Id" + decl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": snapID, + "snap-name": snapName, + "publisher-id": "can0nical", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(decl) + c.Assert(err, IsNil) + + snapSHA3_384, snapSize, err := asserts.SnapFileSHA3_384(s.downloadedSnaps[snapName]) + c.Assert(err, IsNil) + + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ + "snap-sha3-384": snapSHA3_384, + "snap-size": fmt.Sprintf("%d", snapSize), + "snap-id": snapID, + "snap-revision": s.storeSnapInfo[snapName].Revision.String(), + "developer-id": "can0nical", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapRev) + c.Assert(err, IsNil) +} + +func (s *imageSuite) TearDownTest(c *C) { + partition.ForceBootloader(nil) + image.Stdout = os.Stdout +} + +// interface for the store +func (s *imageSuite) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { + return s.storeSnapInfo[spec.Name], nil +} + +func (s *imageSuite) Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState) error { + return osutil.CopyFile(s.downloadedSnaps[name], targetFn, 0) +} + +func (s *imageSuite) Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) { + ref := &asserts.Ref{Type: assertType, PrimaryKey: primaryKey} + return ref.Resolve(s.storeSigning.Find) +} + +const packageGadget = ` +name: pc +version: 1.0 +type: gadget +` + +const packageKernel = ` +name: pc-kernel +version: 4.4-1 +type: kernel +` + +const packageCore = ` +name: core +version: 16.04 +type: os +` + +const devmodeSnap = ` +name: devmode-snap +version: 1.0 +type: app +confinement: devmode +` + +const requiredSnap1 = ` +name: required-snap1 +version: 1.0 +` + +func (s *imageSuite) TestMissingModelAssertions(c *C) { + _, err := image.DecodeModelAssertion(&image.Options{}) + c.Assert(err, ErrorMatches, "cannot read model assertion: open : no such file or directory") +} + +func (s *imageSuite) TestIncorrectModelAssertions(c *C) { + fn := filepath.Join(c.MkDir(), "broken-model.assertion") + err := ioutil.WriteFile(fn, nil, 0644) + c.Assert(err, IsNil) + _, err = image.DecodeModelAssertion(&image.Options{ + ModelFile: fn, + }) + c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot decode model assertion "%s": assertion content/signature separator not found`, fn)) +} + +func (s *imageSuite) TestValidButDifferentAssertion(c *C) { + var differentAssertion = []byte(`type: snap-declaration +authority-id: canonical +series: 16 +snap-id: snap-id-1 +snap-name: first +publisher-id: dev-id1 +timestamp: 2016-01-02T10:00:00-05:00 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AXNpZw== +`) + + fn := filepath.Join(c.MkDir(), "different.assertion") + err := ioutil.WriteFile(fn, differentAssertion, 0644) + c.Assert(err, IsNil) + + _, err = image.DecodeModelAssertion(&image.Options{ + ModelFile: fn, + }) + c.Assert(err, ErrorMatches, fmt.Sprintf(`assertion in "%s" is not a model assertion`, fn)) +} + +func (s *imageSuite) TestModelAssertionReservedHeaders(c *C) { + const mod = `type: model +authority-id: brand +series: 16 +brand-id: brand +model: baz-3000 +architecture: armhf +gadget: brand-gadget +kernel: kernel +timestamp: 2016-01-02T10:00:00-05:00 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AXNpZw== +` + + reserved := []string{ + "core", + "os", + "class", + "allowed-modes", + } + + for _, rsvd := range reserved { + tweaked := strings.Replace(mod, "kernel: kernel\n", fmt.Sprintf("kernel: kernel\n%s: stuff\n", rsvd), 1) + fn := filepath.Join(c.MkDir(), "model.assertion") + err := ioutil.WriteFile(fn, []byte(tweaked), 0644) + c.Assert(err, IsNil) + _, err = image.DecodeModelAssertion(&image.Options{ + ModelFile: fn, + }) + c.Check(err, ErrorMatches, fmt.Sprintf("model assertion cannot have reserved/unsupported header %q set", rsvd)) + } +} + +func (s *imageSuite) TestHappyDecodeModelAssertion(c *C) { + fn := filepath.Join(c.MkDir(), "model.assertion") + err := ioutil.WriteFile(fn, asserts.Encode(s.model), 0644) + c.Assert(err, IsNil) + + a, err := image.DecodeModelAssertion(&image.Options{ + ModelFile: fn, + }) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) +} + +func (s *imageSuite) TestMissingGadgetUnpackDir(c *C) { + err := image.DownloadUnpackGadget(s, s.model, &image.Options{}, nil) + c.Assert(err, ErrorMatches, `cannot create gadget unpack dir "": mkdir : no such file or directory`) +} + +func infoFromSnapYaml(c *C, snapYaml string, rev snap.Revision) *snap.Info { + info, err := snap.InfoFromSnapYaml([]byte(snapYaml)) + c.Assert(err, IsNil) + + if !rev.Unset() { + info.SnapID = info.Name() + "-Id" + info.Revision = rev + } + return info +} + +func (s *imageSuite) TestDownloadUnpackGadget(c *C) { + files := [][]string{ + {"subdir/canary.txt", "I'm a canary"}, + } + s.downloadedSnaps["pc"] = snaptest.MakeTestSnapWithFiles(c, packageGadget, files) + s.storeSnapInfo["pc"] = infoFromSnapYaml(c, packageGadget, snap.R(99)) + + gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget-unpack-dir") + opts := &image.Options{ + GadgetUnpackDir: gadgetUnpackDir, + } + local, err := image.LocalSnaps(opts) + c.Assert(err, IsNil) + + err = image.DownloadUnpackGadget(s, s.model, opts, local) + c.Assert(err, IsNil) + + // verify the right data got unpacked + for _, t := range []struct{ file, content string }{ + {"meta/snap.yaml", packageGadget}, + {files[0][0], files[0][1]}, + } { + fn := filepath.Join(gadgetUnpackDir, t.file) + content, err := ioutil.ReadFile(fn) + c.Assert(err, IsNil) + c.Check(content, DeepEquals, []byte(t.content)) + } +} + +func (s *imageSuite) setupSnaps(c *C, gadgetUnpackDir string) { + err := os.MkdirAll(gadgetUnpackDir, 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(gadgetUnpackDir, "grub.conf"), nil, 0644) + c.Assert(err, IsNil) + + s.downloadedSnaps["pc"] = snaptest.MakeTestSnapWithFiles(c, packageGadget, [][]string{{"grub.cfg", "I'm a grub.cfg"}}) + s.storeSnapInfo["pc"] = infoFromSnapYaml(c, packageGadget, snap.R(1)) + s.addSystemSnapAssertions(c, "pc") + + s.downloadedSnaps["pc-kernel"] = snaptest.MakeTestSnapWithFiles(c, packageKernel, nil) + s.storeSnapInfo["pc-kernel"] = infoFromSnapYaml(c, packageKernel, snap.R(2)) + s.addSystemSnapAssertions(c, "pc-kernel") + + s.downloadedSnaps["core"] = snaptest.MakeTestSnapWithFiles(c, packageCore, nil) + s.storeSnapInfo["core"] = infoFromSnapYaml(c, packageCore, snap.R(3)) + s.addSystemSnapAssertions(c, "core") + + s.downloadedSnaps["required-snap1"] = snaptest.MakeTestSnapWithFiles(c, requiredSnap1, nil) + s.storeSnapInfo["required-snap1"] = infoFromSnapYaml(c, requiredSnap1, snap.R(3)) + s.addSystemSnapAssertions(c, "required-snap1") +} + +func (s *imageSuite) TestBootstrapToRootDir(c *C) { + restore := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer restore() + + rootdir := filepath.Join(c.MkDir(), "imageroot") + + // FIXME: bootstrapToRootDir needs an unpacked gadget yaml + gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget") + + s.setupSnaps(c, gadgetUnpackDir) + + // mock the mount cmds (for the extract kernel assets stuff) + c1 := testutil.MockCommand(c, "mount", "") + defer c1.Restore() + c2 := testutil.MockCommand(c, "umount", "") + defer c2.Restore() + + opts := &image.Options{ + RootDir: rootdir, + GadgetUnpackDir: gadgetUnpackDir, + } + local, err := image.LocalSnaps(opts) + c.Assert(err, IsNil) + + err = image.BootstrapToRootDir(s, s.model, opts, local) + c.Assert(err, IsNil) + + // check seed yaml + seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml")) + c.Assert(err, IsNil) + + c.Check(seed.Snaps, HasLen, 4) + + // check the files are in place + for i, name := range []string{"core", "pc-kernel", "pc"} { + info := s.storeSnapInfo[name] + fn := filepath.Base(info.MountFile()) + p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn) + c.Check(osutil.FileExists(p), Equals, true) + + c.Check(seed.Snaps[i], DeepEquals, &snap.SeedSnap{ + Name: name, + SnapID: name + "-Id", + File: fn, + }) + } + + storeAccountKey := s.storeSigning.StoreAccountKey("") + brandPubKey, err := s.brandSigning.PublicKey("") + c.Assert(err, IsNil) + + // check the assertions are in place + for _, fn := range []string{"model", brandPubKey.ID() + ".account-key", "my-brand.account", storeAccountKey.PublicKeyID() + ".account-key"} { + p := filepath.Join(rootdir, "var/lib/snapd/seed/assertions", fn) + c.Check(osutil.FileExists(p), Equals, true) + } + + b, err := ioutil.ReadFile(filepath.Join(rootdir, "var/lib/snapd/seed/assertions", "model")) + c.Assert(err, IsNil) + c.Check(b, DeepEquals, asserts.Encode(s.model)) + + b, err = ioutil.ReadFile(filepath.Join(rootdir, "var/lib/snapd/seed/assertions", "my-brand.account")) + c.Assert(err, IsNil) + a, err := asserts.Decode(b) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.AccountType) + c.Check(a.HeaderString("account-id"), Equals, "my-brand") + + // check the snap assertions are also in place + for _, snapId := range []string{"pc-Id", "pc-kernel-Id", "core-Id"} { + p := filepath.Join(rootdir, "var/lib/snapd/seed/assertions", fmt.Sprintf("16,%s.snap-declaration", snapId)) + c.Check(osutil.FileExists(p), Equals, true) + } + + // check the bootloader config + m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core") + c.Assert(err, IsNil) + c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap") + c.Check(m["snap_core"], Equals, "core_3.snap") +} + +func (s *imageSuite) TestBootstrapToRootDirLocalCore(c *C) { + restore := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer restore() + + rootdir := filepath.Join(c.MkDir(), "imageroot") + + // FIXME: bootstrapToRootDir needs an unpacked gadget yaml + gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget") + + s.setupSnaps(c, gadgetUnpackDir) + + // mock the mount cmds (for the extract kernel assets stuff) + c1 := testutil.MockCommand(c, "mount", "") + defer c1.Restore() + c2 := testutil.MockCommand(c, "umount", "") + defer c2.Restore() + + opts := &image.Options{ + Snaps: []string{ + s.downloadedSnaps["core"], + s.downloadedSnaps["required-snap1"], + }, + RootDir: rootdir, + GadgetUnpackDir: gadgetUnpackDir, + } + local, err := image.LocalSnaps(opts) + c.Assert(err, IsNil) + + err = image.BootstrapToRootDir(s, s.model, opts, local) + c.Assert(err, IsNil) + + // check seed yaml + seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml")) + c.Assert(err, IsNil) + + c.Check(seed.Snaps, HasLen, 4) + + // check the files are in place + for i, name := range []string{"core_x1.snap", "pc-kernel", "pc", "required-snap1_x1.snap"} { + unasserted := false + info := s.storeSnapInfo[name] + if info == nil { + switch name { + case "core_x1.snap": + info = &snap.Info{ + SideInfo: snap.SideInfo{ + RealName: "core", + Revision: snap.R("x1"), + }, + } + unasserted = true + case "required-snap1_x1.snap": + info = &snap.Info{ + SideInfo: snap.SideInfo{ + RealName: "required-snap1", + Revision: snap.R("x1"), + }, + } + unasserted = true + } + } + + fn := filepath.Base(info.MountFile()) + p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn) + c.Check(osutil.FileExists(p), Equals, true) + + c.Check(seed.Snaps[i], DeepEquals, &snap.SeedSnap{ + Name: info.Name(), + SnapID: info.SnapID, + File: fn, + Unasserted: unasserted, + }) + } + + l, err := ioutil.ReadDir(filepath.Join(rootdir, "var/lib/snapd/seed/snaps")) + c.Assert(err, IsNil) + c.Check(l, HasLen, 4) + + storeAccountKey := s.storeSigning.StoreAccountKey("") + brandPubKey, err := s.brandSigning.PublicKey("") + c.Assert(err, IsNil) + + // check the assertions are in place + for _, fn := range []string{"model", brandPubKey.ID() + ".account-key", "my-brand.account", storeAccountKey.PublicKeyID() + ".account-key"} { + p := filepath.Join(rootdir, "var/lib/snapd/seed/assertions", fn) + c.Check(osutil.FileExists(p), Equals, true) + } + + b, err := ioutil.ReadFile(filepath.Join(rootdir, "var/lib/snapd/seed/assertions", "model")) + c.Assert(err, IsNil) + c.Check(b, DeepEquals, asserts.Encode(s.model)) + + b, err = ioutil.ReadFile(filepath.Join(rootdir, "var/lib/snapd/seed/assertions", "my-brand.account")) + c.Assert(err, IsNil) + a, err := asserts.Decode(b) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.AccountType) + c.Check(a.HeaderString("account-id"), Equals, "my-brand") + + decls, err := filepath.Glob(filepath.Join(rootdir, "var/lib/snapd/seed/assertions", "*.snap-declaration")) + c.Assert(err, IsNil) + // nothing for core + c.Check(decls, HasLen, 2) + + // check the bootloader config + m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core") + c.Assert(err, IsNil) + c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap") + c.Assert(err, IsNil) + c.Check(m["snap_core"], Equals, "core_x1.snap") + + // check that cloud-init is setup correctly + c.Check(osutil.FileExists(filepath.Join(rootdir, "etc/cloud/cloud-init.disabled")), Equals, true) +} + +func (s *imageSuite) TestBootstrapToRootDirDevmodeSnap(c *C) { + restore := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer restore() + + rootdir := filepath.Join(c.MkDir(), "imageroot") + + // FIXME: bootstrapToRootDir needs an unpacked gadget yaml + gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget") + + err := os.MkdirAll(gadgetUnpackDir, 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(gadgetUnpackDir, "grub.conf"), nil, 0644) + c.Assert(err, IsNil) + + s.setupSnaps(c, gadgetUnpackDir) + + s.downloadedSnaps["devmode-snap"] = snaptest.MakeTestSnapWithFiles(c, devmodeSnap, nil) + s.storeSnapInfo["devmode-snap"] = infoFromSnapYaml(c, devmodeSnap, snap.R(0)) + + // mock the mount cmds (for the extract kernel assets stuff) + c1 := testutil.MockCommand(c, "mount", "") + defer c1.Restore() + c2 := testutil.MockCommand(c, "umount", "") + defer c2.Restore() + + opts := &image.Options{ + Snaps: []string{s.downloadedSnaps["devmode-snap"]}, + + RootDir: rootdir, + GadgetUnpackDir: gadgetUnpackDir, + } + local, err := image.LocalSnaps(opts) + c.Assert(err, IsNil) + + err = image.BootstrapToRootDir(s, s.model, opts, local) + c.Assert(err, IsNil) + + // check seed yaml + seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml")) + c.Assert(err, IsNil) + + c.Check(seed.Snaps, HasLen, 5) + + // check devmode-snap + info := &snap.Info{ + SideInfo: snap.SideInfo{ + RealName: "devmode-snap", + Revision: snap.R("x1"), + }, + } + fn := filepath.Base(info.MountFile()) + p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn) + c.Check(osutil.FileExists(p), Equals, true) + + // ensure local snaps are put last in seed.yaml + last := len(seed.Snaps) - 1 + c.Check(seed.Snaps[last], DeepEquals, &snap.SeedSnap{ + Name: "devmode-snap", + File: fn, + DevMode: true, + Unasserted: true, + }) +} + +func (s *imageSuite) TestInstallCloudConfigNoConfig(c *C) { + targetDir := c.MkDir() + emptyGadgetDir := c.MkDir() + + dirs.SetRootDir(targetDir) + err := image.InstallCloudConfig(emptyGadgetDir) + c.Assert(err, IsNil) + c.Check(osutil.FileExists(filepath.Join(targetDir, "etc/cloud/cloud-init.disabled")), Equals, true) +} + +func (s *imageSuite) TestInstallCloudConfigWithCloudConfig(c *C) { + canary := []byte("ni! ni! ni!") + + targetDir := c.MkDir() + gadgetDir := c.MkDir() + err := ioutil.WriteFile(filepath.Join(gadgetDir, "cloud.conf"), canary, 0644) + c.Assert(err, IsNil) + + dirs.SetRootDir(targetDir) + err = image.InstallCloudConfig(gadgetDir) + c.Assert(err, IsNil) + content, err := ioutil.ReadFile(filepath.Join(targetDir, "etc/cloud/cloud.cfg")) + c.Assert(err, IsNil) + c.Check(content, DeepEquals, canary) +} diff --git a/interfaces/apparmor/apparmor.go b/interfaces/apparmor/apparmor.go new file mode 100644 index 00000000..1443bce0 --- /dev/null +++ b/interfaces/apparmor/apparmor.go @@ -0,0 +1,105 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +// Package apparmor contains primitives for working with apparmor. +// +// References: +// - http://wiki.apparmor.net/index.php/Kernel_interfaces +// - http://apparmor.wiki.kernel.org/ +// - http://manpages.ubuntu.com/manpages/xenial/en/man7/apparmor.7.html +package apparmor + +import ( + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/dirs" +) + +// LoadProfile loads an apparmor profile from the given file. +// +// If no such profile was previously loaded then it is simply added to the kernel. +// If there was a profile with the same name before, that profile is replaced. +func LoadProfile(fname string) error { + // Use no-expr-simplify since expr-simplify is actually slower on armhf (LP: #1383858) + output, err := exec.Command( + "apparmor_parser", "--replace", "--write-cache", "-O", + "no-expr-simplify", fmt.Sprintf("--cache-loc=%s", dirs.AppArmorCacheDir), + fname).CombinedOutput() + if err != nil { + return fmt.Errorf("cannot load apparmor profile: %s\napparmor_parser output:\n%s", err, string(output)) + } + return nil +} + +// UnloadProfile removes the named profile from the running kernel. +// +// The operation is done with: apparmor_parser --remove $name +// The binary cache file is removed from /var/cache/apparmor +func UnloadProfile(name string) error { + output, err := exec.Command("apparmor_parser", "--remove", name).CombinedOutput() + if err != nil { + return fmt.Errorf("cannot unload apparmor profile: %s\napparmor_parser output:\n%s", err, string(output)) + } + err = os.Remove(filepath.Join(dirs.AppArmorCacheDir, name)) + // It is not an error if the cache file wasn't there to remove. + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("cannot remove apparmor profile cache: %s", err) + } + return nil +} + +// profilesPath contains information about the currently loaded apparmor profiles. +const realProfilesPath = "/sys/kernel/security/apparmor/profiles" + +var profilesPath = realProfilesPath + +// LoadedProfiles interrogates the kernel and returns a list of loaded apparmor profiles. +// +// Snappy manages apparmor profiles named "snap.*". Other profiles might exist on +// the system (via snappy dimension) and those are filtered-out. +func LoadedProfiles() ([]string, error) { + file, err := os.Open(profilesPath) + if err != nil { + return nil, err + } + defer file.Close() + var profiles []string + for { + var name, mode string + n, err := fmt.Fscanf(file, "%s %s\n", &name, &mode) + if n > 0 && n != 2 { + return nil, fmt.Errorf("syntax error, expected: name (mode)") + } + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if strings.HasPrefix(name, "snap.") { + profiles = append(profiles, name) + } + } + return profiles, nil +} diff --git a/interfaces/apparmor/apparmor_test.go b/interfaces/apparmor/apparmor_test.go new file mode 100644 index 00000000..ddd5ca08 --- /dev/null +++ b/interfaces/apparmor/apparmor_test.go @@ -0,0 +1,171 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package apparmor_test + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/testutil" +) + +func Test(t *testing.T) { + TestingT(t) +} + +type appArmorSuite struct { + testutil.BaseTest + profilesFilename string +} + +var _ = Suite(&appArmorSuite{}) + +func (s *appArmorSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + // Mock the list of profiles in the running kernel + s.profilesFilename = path.Join(c.MkDir(), "profiles") + apparmor.MockProfilesPath(&s.BaseTest, s.profilesFilename) +} + +// Tests for LoadProfile() + +func (s *appArmorSuite) TestLoadProfileRunsAppArmorParserReplace(c *C) { + cmd := testutil.MockCommand(c, "apparmor_parser", "") + defer cmd.Restore() + err := apparmor.LoadProfile("/path/to/snap.samba.smbd") + c.Assert(err, IsNil) + c.Assert(cmd.Calls(), DeepEquals, [][]string{ + {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", "--cache-loc=/var/cache/apparmor", "/path/to/snap.samba.smbd"}, + }) +} + +func (s *appArmorSuite) TestLoadProfileReportsErrors(c *C) { + cmd := testutil.MockCommand(c, "apparmor_parser", "exit 42") + defer cmd.Restore() + err := apparmor.LoadProfile("/path/to/snap.samba.smbd") + c.Assert(err.Error(), Equals, `cannot load apparmor profile: exit status 42 +apparmor_parser output: +`) + c.Assert(cmd.Calls(), DeepEquals, [][]string{ + {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", "--cache-loc=/var/cache/apparmor", "/path/to/snap.samba.smbd"}, + }) +} + +// Tests for Profile.Unload() + +func (s *appArmorSuite) TestUnloadProfileRunsAppArmorParserRemove(c *C) { + cmd := testutil.MockCommand(c, "apparmor_parser", "") + defer cmd.Restore() + err := apparmor.UnloadProfile("snap.samba.smbd") + c.Assert(err, IsNil) + c.Assert(cmd.Calls(), DeepEquals, [][]string{ + {"apparmor_parser", "--remove", "snap.samba.smbd"}, + }) +} + +func (s *appArmorSuite) TestUnloadProfileReportsErrors(c *C) { + cmd := testutil.MockCommand(c, "apparmor_parser", "exit 42") + defer cmd.Restore() + err := apparmor.UnloadProfile("snap.samba.smbd") + c.Assert(err.Error(), Equals, `cannot unload apparmor profile: exit status 42 +apparmor_parser output: +`) +} + +func (s *appArmorSuite) TestUnloadRemovesCachedProfile(c *C) { + cmd := testutil.MockCommand(c, "apparmor_parser", "") + defer cmd.Restore() + + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("") + err := os.MkdirAll(dirs.AppArmorCacheDir, 0755) + c.Assert(err, IsNil) + + fname := filepath.Join(dirs.AppArmorCacheDir, "profile") + ioutil.WriteFile(fname, []byte("blob"), 0600) + err = apparmor.UnloadProfile("profile") + c.Assert(err, IsNil) + _, err = os.Stat(fname) + c.Check(os.IsNotExist(err), Equals, true) +} + +// Tests for LoadedProfiles() + +func (s *appArmorSuite) TestLoadedApparmorProfilesReturnsErrorOnMissingFile(c *C) { + profiles, err := apparmor.LoadedProfiles() + c.Assert(err, ErrorMatches, "open .*: no such file or directory") + c.Check(profiles, IsNil) +} + +func (s *appArmorSuite) TestLoadedApparmorProfilesCanParseEmptyFile(c *C) { + ioutil.WriteFile(s.profilesFilename, []byte(""), 0600) + profiles, err := apparmor.LoadedProfiles() + c.Assert(err, IsNil) + c.Check(profiles, HasLen, 0) +} + +func (s *appArmorSuite) TestLoadedApparmorProfilesParsesAndFiltersData(c *C) { + ioutil.WriteFile(s.profilesFilename, []byte( + // The output contains some of the snappy-specific elements + // and some non-snappy elements pulled from Ubuntu 16.04 desktop + // + // The pi2-piglow.{background,foreground}.snap entries are the only + // ones that should be reported by the function. + `/sbin/dhclient (enforce) +/usr/bin/ubuntu-core-launcher (enforce) +/usr/bin/ubuntu-core-launcher (enforce) +/usr/lib/NetworkManager/nm-dhcp-client.action (enforce) +/usr/lib/NetworkManager/nm-dhcp-helper (enforce) +/usr/lib/connman/scripts/dhclient-script (enforce) +/usr/lib/lightdm/lightdm-guest-session (enforce) +/usr/lib/lightdm/lightdm-guest-session//chromium (enforce) +/usr/lib/telepathy/telepathy-* (enforce) +/usr/lib/telepathy/telepathy-*//pxgsettings (enforce) +/usr/lib/telepathy/telepathy-*//sanitized_helper (enforce) +snap.pi2-piglow.background (enforce) +snap.pi2-piglow.foreground (enforce) +webbrowser-app (enforce) +webbrowser-app//oxide_helper (enforce) +`), 0600) + profiles, err := apparmor.LoadedProfiles() + c.Assert(err, IsNil) + c.Check(profiles, DeepEquals, []string{ + "snap.pi2-piglow.background", + "snap.pi2-piglow.foreground", + }) +} + +func (s *appArmorSuite) TestLoadedApparmorProfilesHandlesParsingErrors(c *C) { + ioutil.WriteFile(s.profilesFilename, []byte("broken stuff here\n"), 0600) + profiles, err := apparmor.LoadedProfiles() + c.Assert(err, ErrorMatches, "newline in format does not match input") + c.Check(profiles, IsNil) + ioutil.WriteFile(s.profilesFilename, []byte("truncated"), 0600) + profiles, err = apparmor.LoadedProfiles() + c.Assert(err, ErrorMatches, `syntax error, expected: name \(mode\)`) + c.Check(profiles, IsNil) +} diff --git a/interfaces/apparmor/backend.go b/interfaces/apparmor/backend.go new file mode 100644 index 00000000..46e79a8e --- /dev/null +++ b/interfaces/apparmor/backend.go @@ -0,0 +1,207 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +// Package apparmor implements integration between snappy and +// ubuntu-core-launcher around apparmor. +// +// Snappy creates apparmor profiles for each application (for each snap) +// present in the system. Upon each execution of ubuntu-core-launcher +// application process is launched under the profile. Prior to that the profile +// must be parsed, compiled and loaded into the kernel using the support tool +// "apparmor_parser". +// +// Each apparmor profile contains a simple