From aaede0e1c96e61f4d2697ad194ba7fe749d112ee Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 28 Feb 2019 17:21:26 +0000 Subject: [PATCH] Import snapd_2.37.4.orig.tar.xz [dgit import orig snapd_2.37.4.orig.tar.xz] --- .travis.yml | 68 + CONTRIBUTING.md | 27 + COPYING | 674 + HACKING.md | 212 + PULL_REQUEST_TEMPLATE.md | 2 + README.md | 45 + advisor/backend.go | 300 + advisor/cmdfinder.go | 110 + advisor/cmdfinder_test.go | 153 + advisor/export_test.go | 22 + advisor/finder.go | 36 + advisor/pkgfinder.go | 43 + advisor/pkgfinder_test.go | 40 + arch/arch.go | 136 + arch/arch_test.go | 61 + asserts/account.go | 113 + asserts/account_key.go | 288 + asserts/account_key_test.go | 809 + asserts/account_test.go | 174 + asserts/asserts.go | 1018 ++ asserts/asserts_test.go | 899 ++ asserts/assertstest/assertstest.go | 446 + asserts/assertstest/assertstest_test.go | 163 + asserts/crypto.go | 398 + asserts/database.go | 623 + asserts/database_test.go | 1190 ++ asserts/device_asserts.go | 557 + asserts/device_asserts_test.go | 712 + asserts/digest.go | 43 + asserts/digest_test.go | 65 + asserts/export_test.go | 193 + asserts/fetcher.go | 121 + asserts/fetcher_test.go | 167 + asserts/findwildcard.go | 111 + asserts/findwildcard_test.go | 139 + asserts/fsbackstore.go | 221 + asserts/fsbackstore_test.go | 258 + asserts/fsentryutils.go | 70 + asserts/fskeypairmgr.go | 92 + asserts/fskeypairmgr_test.go | 65 + asserts/gpgkeypairmgr.go | 353 + asserts/gpgkeypairmgr_test.go | 331 + asserts/header_checks.go | 274 + asserts/headers.go | 318 + asserts/headers_test.go | 396 + asserts/ifacedecls.go | 1099 ++ asserts/ifacedecls_test.go | 1812 +++ asserts/membackstore.go | 191 + asserts/membackstore_test.go | 351 + asserts/memkeypairmgr.go | 59 + asserts/memkeypairmgr_test.go | 73 + asserts/privkeys_for_test.go | 54 + asserts/repair.go | 159 + asserts/repair_test.go | 183 + asserts/signtool/sign.go | 110 + asserts/signtool/sign_test.go | 262 + asserts/snap_asserts.go | 943 ++ asserts/snap_asserts_test.go | 1870 +++ asserts/snapasserts/snapasserts.go | 155 + asserts/snapasserts/snapasserts_test.go | 334 + asserts/store_asserts.go | 162 + asserts/store_asserts_test.go | 235 + asserts/sysdb/generic.go | 196 + asserts/sysdb/staging.go | 183 + asserts/sysdb/sysdb.go | 49 + asserts/sysdb/sysdb_test.go | 216 + asserts/sysdb/testkeys.go | 30 + asserts/sysdb/trusted.go | 156 + asserts/systestkeys/trusted.go | 263 + asserts/user.go | 281 + asserts/user_test.go | 214 + boot/boottest/mockbootloader.go | 72 + boot/kernel_os.go | 210 + boot/kernel_os_test.go | 296 + client/aliases.go | 102 + client/aliases_test.go | 195 + client/apps.go | 263 + client/apps_test.go | 396 + client/asserts.go | 104 + client/asserts_test.go | 159 + client/buy.go | 63 + client/change.go | 164 + client/change_test.go | 233 + client/client.go | 686 + client/client_test.go | 560 + client/conf.go | 52 + client/conf_test.go | 104 + client/export_test.go | 51 + client/icons.go | 67 + client/icons_test.go | 65 + client/interfaces.go | 155 + client/interfaces_test.go | 299 + client/login.go | 155 + client/login_test.go | 164 + client/packages.go | 240 + client/packages_test.go | 335 + client/snap_op.go | 304 + client/snap_op_test.go | 402 + client/snapctl.go | 57 + client/snapctl_test.go | 68 + client/snapshot.go | 175 + client/snapshot_test.go | 138 + client/warnings.go | 89 + client/warnings_test.go | 121 + cmd/.indent.pro | 34 + cmd/Makefile.am | 491 + cmd/appinfo.go | 133 + cmd/appinfo_test.go | 71 + cmd/autogen.sh | 55 + cmd/cmd_linux.go | 214 + cmd/cmd_linux_test.go | 117 + cmd/cmd_other.go | 28 + cmd/cmd_test.go | 307 + cmd/configure.ac | 240 + cmd/decode-mount-opts/decode-mount-opts.c | 38 + cmd/export_test.go | 60 + .../apparmor-support.c | 141 + .../apparmor-support.h | 93 + .../cgroup-freezer-support.c | 149 + .../cgroup-freezer-support.h | 35 + cmd/libsnap-confine-private/classic-test.c | 204 + cmd/libsnap-confine-private/classic.c | 63 + cmd/libsnap-confine-private/classic.h | 35 + .../cleanup-funcs-test.c | 44 + cmd/libsnap-confine-private/cleanup-funcs.c | 56 + cmd/libsnap-confine-private/cleanup-funcs.h | 74 + cmd/libsnap-confine-private/error-test.c | 256 + cmd/libsnap-confine-private/error.c | 147 + cmd/libsnap-confine-private/error.h | 163 + .../fault-injection-test.c | 63 + cmd/libsnap-confine-private/fault-injection.c | 78 + cmd/libsnap-confine-private/fault-injection.h | 67 + cmd/libsnap-confine-private/feature-test.c | 86 + cmd/libsnap-confine-private/feature.c | 62 + cmd/libsnap-confine-private/feature.h | 35 + cmd/libsnap-confine-private/locking-test.c | 143 + cmd/libsnap-confine-private/locking.c | 204 + cmd/libsnap-confine-private/locking.h | 107 + cmd/libsnap-confine-private/mount-opt-test.c | 343 + cmd/libsnap-confine-private/mount-opt.c | 338 + cmd/libsnap-confine-private/mount-opt.h | 89 + cmd/libsnap-confine-private/mountinfo-test.c | 200 + cmd/libsnap-confine-private/mountinfo.c | 274 + cmd/libsnap-confine-private/mountinfo.h | 135 + cmd/libsnap-confine-private/privs-test.c | 67 + cmd/libsnap-confine-private/privs.c | 78 + cmd/libsnap-confine-private/privs.h | 38 + .../secure-getenv-test.c | 23 + cmd/libsnap-confine-private/secure-getenv.c | 31 + cmd/libsnap-confine-private/secure-getenv.h | 36 + cmd/libsnap-confine-private/snap-test.c | 568 + cmd/libsnap-confine-private/snap.c | 314 + cmd/libsnap-confine-private/snap.h | 119 + .../string-utils-test.c | 848 + cmd/libsnap-confine-private/string-utils.c | 260 + cmd/libsnap-confine-private/string-utils.h | 111 + cmd/libsnap-confine-private/test-utils-test.c | 45 + cmd/libsnap-confine-private/test-utils.c | 73 + cmd/libsnap-confine-private/test-utils.h | 26 + cmd/libsnap-confine-private/tool.c | 230 + cmd/libsnap-confine-private/tool.h | 52 + cmd/libsnap-confine-private/unit-tests-main.c | 22 + cmd/libsnap-confine-private/unit-tests.c | 29 + cmd/libsnap-confine-private/unit-tests.h | 29 + cmd/libsnap-confine-private/utils-test.c | 203 + cmd/libsnap-confine-private/utils.c | 219 + cmd/libsnap-confine-private/utils.h | 63 + cmd/snap-confine/PORTING | 15 + cmd/snap-confine/README.mount_namespace | 92 + cmd/snap-confine/README.nvidia | 26 + cmd/snap-confine/README.syscalls | 436 + cmd/snap-confine/cookie-support-test.c | 102 + cmd/snap-confine/cookie-support.c | 75 + cmd/snap-confine/cookie-support.h | 34 + ...printk-based-debugging-to-pivot_root.patch | 132 + cmd/snap-confine/mount-support-nvidia.c | 547 + cmd/snap-confine/mount-support-nvidia.h | 48 + cmd/snap-confine/mount-support-test.c | 100 + cmd/snap-confine/mount-support.c | 704 + cmd/snap-confine/mount-support.h | 77 + cmd/snap-confine/ns-support-test.c | 154 + cmd/snap-confine/ns-support.c | 802 + cmd/snap-confine/ns-support.h | 148 + cmd/snap-confine/seccomp-support-ext.c | 122 + cmd/snap-confine/seccomp-support-ext.h | 30 + cmd/snap-confine/seccomp-support.c | 185 + cmd/snap-confine/seccomp-support.h | 51 + cmd/snap-confine/snap-confine-args-test.c | 482 + cmd/snap-confine/snap-confine-args.c | 255 + cmd/snap-confine/snap-confine-args.h | 124 + cmd/snap-confine/snap-confine.apparmor.in | 519 + cmd/snap-confine/snap-confine.c | 411 + cmd/snap-confine/snap-confine.rst | 188 + cmd/snap-confine/snap-device-helper | 73 + cmd/snap-confine/snap-device-helper-test.c | 235 + .../spread-tests/data/apt-keys/README.md | 4 + .../spread-tests/data/apt-keys/sbuild-key.pub | Bin 0 -> 427 bytes .../spread-tests/data/apt-keys/sbuild-key.sec | Bin 0 -> 759 bytes cmd/snap-confine/spread-tests/distros/debian. | 2 + .../spread-tests/distros/debian.common | 12 + .../spread-tests/distros/ubuntu.14.04 | 2 + .../spread-tests/distros/ubuntu.16.04 | 2 + .../spread-tests/distros/ubuntu.16.10 | 2 + .../spread-tests/distros/ubuntu.common | 7 + .../spread-tests/main/cgroup-used/task.yaml | 37 + .../main/core-is-preferred/task.yaml | 10 + .../spread-tests/main/debug-flags/task.yaml | 12 + .../main/hostfs-created-on-demand/task.yaml | 24 + .../main/media-visible-in-devmode/task.yaml | 15 + .../expected.classic.core.json | 1028 ++ .../expected.classic.core.linode.amd64.json | 798 + .../expected.classic.core.linode.i386.json | 808 + .../expected.classic.core.qemu.amd64.json | 808 + .../expected.classic.core.qemu.i386.json | 808 + .../expected.classic.ubuntu-core.json | 1028 ++ ...cted.classic.ubuntu-core.linode.amd64.json | 788 + ...ected.classic.ubuntu-core.linode.i386.json | 798 + ...pected.classic.ubuntu-core.qemu.amd64.json | 798 + ...xpected.classic.ubuntu-core.qemu.i386.json | 798 + .../main/mount-ns-layout/expected.core.json | 2050 +++ .../mount-ns-layout/expected.core.linode.json | 1800 +++ .../main/mount-ns-layout/process.py | 117 + .../main/mount-ns-layout/snap-arch.py | 22 + .../main/mount-ns-layout/task.yaml | 46 + .../main/mount-ns-sharing/task.yaml | 20 + .../task.yaml | 26 + .../mount-profiles-bin-snap-source/task.yaml | 26 + .../main/mount-profiles-missing-dst/task.yaml | 19 + .../main/mount-profiles-missing-src/task.yaml | 19 + .../main/mount-profiles-mount-tmpfs/task.yaml | 20 + .../main/mount-profiles-ro-mount/task.yaml | 18 + .../main/mount-profiles-rw-mount/task.yaml | 24 + .../spread-tests/main/mount-usr-src/task.yaml | 17 + .../main/test-seccomp-compat/task.yaml | 16 + .../main/test-snap-runs/task.yaml | 15 + .../ubuntu-core-launcher-exists/task.yaml | 6 + .../main/user-data-dir-created/task.yaml | 23 + .../user-xdg-runtime-dir-created/task.yaml | 21 + .../regression/lp-1599608/task.yaml | 69 + cmd/snap-confine/spread-tests/release.sh | 41 + .../spread-tests/spread-prepare.sh | 179 + cmd/snap-confine/udev-support.c | 302 + cmd/snap-confine/udev-support.h | 40 + cmd/snap-confine/user-support.c | 65 + cmd/snap-confine/user-support.h | 25 + cmd/snap-discard-ns/snap-discard-ns.c | 222 + cmd/snap-discard-ns/snap-discard-ns.rst | 62 + cmd/snap-exec/export_test.go | 51 + cmd/snap-exec/main.go | 248 + cmd/snap-exec/main_test.go | 467 + cmd/snap-failure/cmd_snapd.go | 130 + cmd/snap-failure/cmd_snapd_test.go | 38 + cmd/snap-failure/export_test.go | 25 + cmd/snap-failure/main.go | 77 + cmd/snap-failure/main_test.go | 75 + cmd/snap-gdb-shim/snap-gdb-shim.c | 46 + cmd/snap-mgmt/snap-mgmt.sh.in | 190 + cmd/snap-repair/cmd_done_retry_skip.go | 82 + cmd/snap-repair/cmd_done_retry_skip_test.go | 81 + cmd/snap-repair/cmd_list.go | 74 + cmd/snap-repair/cmd_list_test.go | 47 + cmd/snap-repair/cmd_run.go | 102 + cmd/snap-repair/cmd_run_test.go | 78 + cmd/snap-repair/cmd_show.go | 91 + cmd/snap-repair/cmd_show_test.go | 142 + cmd/snap-repair/export_test.go | 141 + cmd/snap-repair/main.go | 89 + cmd/snap-repair/main_test.go | 99 + cmd/snap-repair/runner.go | 993 ++ cmd/snap-repair/runner_test.go | 1778 +++ cmd/snap-repair/staging.go | 81 + cmd/snap-repair/trace.go | 176 + cmd/snap-repair/trace_test.go | 66 + cmd/snap-repair/trusted.go | 89 + cmd/snap-seccomp/export_test.go | 49 + cmd/snap-seccomp/main.go | 785 + cmd/snap-seccomp/main_ppc64le.go | 31 + cmd/snap-seccomp/main_test.go | 805 + cmd/snap-update-ns/bootstrap.c | 499 + cmd/snap-update-ns/bootstrap.go | 134 + cmd/snap-update-ns/bootstrap.h | 34 + cmd/snap-update-ns/bootstrap_ppc64le.go | 31 + cmd/snap-update-ns/bootstrap_test.go | 159 + cmd/snap-update-ns/change.go | 492 + cmd/snap-update-ns/change_test.go | 2355 +++ cmd/snap-update-ns/export_test.go | 183 + cmd/snap-update-ns/freezer.go | 74 + cmd/snap-update-ns/freezer_test.go | 91 + cmd/snap-update-ns/main.go | 291 + cmd/snap-update-ns/main_test.go | 387 + cmd/snap-update-ns/secure_bindmount.go | 97 + cmd/snap-update-ns/secure_bindmount_test.go | 200 + cmd/snap-update-ns/sorting.go | 57 + cmd/snap-update-ns/sorting_test.go | 72 + cmd/snap-update-ns/trespassing.go | 255 + cmd/snap-update-ns/trespassing_test.go | 428 + cmd/snap-update-ns/utils.go | 655 + cmd/snap-update-ns/utils_test.go | 1144 ++ cmd/snap/cmd_abort.go | 62 + cmd/snap/cmd_abort_test.go | 89 + cmd/snap/cmd_ack.go | 79 + cmd/snap/cmd_advise.go | 287 + cmd/snap/cmd_advise_test.go | 243 + cmd/snap/cmd_alias.go | 118 + cmd/snap/cmd_alias_test.go | 74 + cmd/snap/cmd_aliases.go | 147 + cmd/snap/cmd_aliases_test.go | 179 + cmd/snap/cmd_auto_import.go | 305 + cmd/snap/cmd_auto_import_test.go | 302 + cmd/snap/cmd_blame.go | 55 + cmd/snap/cmd_blame_generated.go | 7 + cmd/snap/cmd_booted.go | 50 + cmd/snap/cmd_buy.go | 137 + cmd/snap/cmd_buy_test.go | 462 + cmd/snap/cmd_can_manage_refreshes.go | 53 + cmd/snap/cmd_changes.go | 209 + cmd/snap/cmd_changes_test.go | 233 + cmd/snap/cmd_confinement.go | 57 + cmd/snap/cmd_confinement_test.go | 39 + cmd/snap/cmd_connect.go | 93 + cmd/snap/cmd_connect_test.go | 325 + cmd/snap/cmd_connectivity_check.go | 64 + cmd/snap/cmd_connectivity_check_test.go | 84 + cmd/snap/cmd_create_key.go | 91 + cmd/snap/cmd_create_key_test.go | 34 + cmd/snap/cmd_create_user.go | 118 + cmd/snap/cmd_create_user_test.go | 150 + cmd/snap/cmd_debug.go | 34 + cmd/snap/cmd_delete_key.go | 60 + cmd/snap/cmd_delete_key_test.go | 64 + cmd/snap/cmd_disconnect.go | 101 + cmd/snap/cmd_disconnect_test.go | 224 + cmd/snap/cmd_download.go | 151 + cmd/snap/cmd_ensure_state_soon.go | 46 + cmd/snap/cmd_ensure_state_soon_test.go | 55 + cmd/snap/cmd_export_key.go | 100 + cmd/snap/cmd_export_key_test.go | 84 + cmd/snap/cmd_find.go | 274 + cmd/snap/cmd_find_test.go | 594 + cmd/snap/cmd_first_boot.go | 50 + cmd/snap/cmd_get.go | 259 + cmd/snap/cmd_get_base_declaration.go | 54 + cmd/snap/cmd_get_base_declaration_test.go | 55 + cmd/snap/cmd_get_test.go | 226 + cmd/snap/cmd_handle_link.go | 91 + cmd/snap/cmd_help.go | 276 + cmd/snap/cmd_help_test.go | 176 + cmd/snap/cmd_info.go | 511 + cmd/snap/cmd_info_test.go | 552 + cmd/snap/cmd_interface.go | 190 + cmd/snap/cmd_interface_test.go | 287 + cmd/snap/cmd_interfaces.go | 168 + cmd/snap/cmd_interfaces_test.go | 674 + cmd/snap/cmd_keys.go | 108 + cmd/snap/cmd_keys_test.go | 144 + cmd/snap/cmd_known.go | 128 + cmd/snap/cmd_known_test.go | 109 + cmd/snap/cmd_list.go | 153 + cmd/snap/cmd_list_test.go | 249 + cmd/snap/cmd_login.go | 135 + cmd/snap/cmd_login_test.go | 93 + cmd/snap/cmd_logout.go | 53 + cmd/snap/cmd_managed.go | 57 + cmd/snap/cmd_managed_test.go | 46 + cmd/snap/cmd_pack.go | 109 + cmd/snap/cmd_pack_test.go | 103 + cmd/snap/cmd_paths.go | 62 + cmd/snap/cmd_paths_test.go | 90 + cmd/snap/cmd_prefer.go | 68 + cmd/snap/cmd_prefer_test.go | 71 + cmd/snap/cmd_prepare_image.go | 82 + cmd/snap/cmd_repair_repairs.go | 93 + cmd/snap/cmd_repair_repairs_test.go | 67 + cmd/snap/cmd_run.go | 943 ++ cmd/snap/cmd_run_test.go | 1111 ++ cmd/snap/cmd_sandbox_features.go | 88 + cmd/snap/cmd_sandbox_features_test.go | 61 + cmd/snap/cmd_services.go | 264 + cmd/snap/cmd_services_test.go | 254 + cmd/snap/cmd_set.go | 99 + cmd/snap/cmd_set_test.go | 117 + cmd/snap/cmd_sign.go | 85 + cmd/snap/cmd_sign_build.go | 124 + cmd/snap/cmd_sign_build_test.go | 134 + cmd/snap/cmd_sign_test.go | 65 + cmd/snap/cmd_snap_op.go | 981 ++ cmd/snap/cmd_snap_op_test.go | 1772 +++ cmd/snap/cmd_snapshot.go | 331 + cmd/snap/cmd_unalias.go | 69 + cmd/snap/cmd_unalias_test.go | 72 + cmd/snap/cmd_userd.go | 89 + cmd/snap/cmd_userd_test.go | 102 + cmd/snap/cmd_version.go | 74 + cmd/snap/cmd_version_linux.go | 53 + cmd/snap/cmd_version_other.go | 41 + cmd/snap/cmd_version_test.go | 74 + cmd/snap/cmd_wait.go | 155 + cmd/snap/cmd_wait_test.go | 174 + cmd/snap/cmd_warnings.go | 224 + cmd/snap/cmd_warnings_test.go | 206 + cmd/snap/cmd_watch.go | 61 + cmd/snap/cmd_watch_test.go | 154 + cmd/snap/cmd_whoami.go | 58 + cmd/snap/color.go | 181 + cmd/snap/color_test.go | 196 + cmd/snap/complete.go | 494 + cmd/snap/error.go | 400 + cmd/snap/export_test.go | 243 + cmd/snap/gnupg2_test.go | 27 + cmd/snap/interfaces_common.go | 55 + cmd/snap/interfaces_common_test.go | 56 + cmd/snap/last.go | 106 + cmd/snap/main.go | 521 + cmd/snap/main_test.go | 400 + cmd/snap/notes.go | 169 + cmd/snap/notes_test.go | 107 + cmd/snap/test-data/pubring.gpg | Bin 0 -> 2192 bytes cmd/snap/test-data/secring.gpg | Bin 0 -> 4774 bytes cmd/snap/test-data/trustdb.gpg | Bin 0 -> 1360 bytes cmd/snap/times.go | 63 + cmd/snap/wait.go | 166 + cmd/snapctl/main.go | 86 + cmd/snapctl/main_test.go | 117 + cmd/snapd-apparmor/snapd-apparmor | 97 + cmd/snapd-env-generator/main.c | 51 + .../snapd-env-generator.rst | 30 + cmd/snapd-generator/main.c | 100 + cmd/snapd/export_test.go | 44 + cmd/snapd/main.go | 161 + cmd/snapd/main_test.go | 118 + .../system-shutdown-utils-test.c | 21 + cmd/system-shutdown/system-shutdown-utils.c | 160 + cmd/system-shutdown/system-shutdown-utils.h | 37 + cmd/system-shutdown/system-shutdown.c | 137 + cmd/version.go | 31 + daemon/api.go | 2967 ++++ daemon/api_json.go | 64 + daemon/api_mock_test.go | 130 + daemon/api_snapshots.go | 140 + daemon/api_snapshots_test.go | 321 + daemon/api_test.go | 7553 +++++++++ daemon/command_counter_test.go | 220 + daemon/daemon.go | 701 + daemon/daemon_test.go | 987 ++ daemon/export_snapshots_test.go | 112 + daemon/response.go | 574 + daemon/response_test.go | 139 + daemon/snap.go | 385 + daemon/ucrednet.go | 132 + daemon/ucrednet_test.go | 180 + data/Makefile | 6 + data/apt/20snapd.conf | 1 + data/completion/complete.sh | 129 + data/completion/etelpmoc.sh | 225 + data/completion/snap | 75 + data/dbus/Makefile | 33 + data/dbus/io.snapcraft.Launcher.service.in | 4 + data/dbus/io.snapcraft.Settings.service.in | 4 + data/desktop/Makefile | 46 + data/desktop/snap-handle-link.desktop.in | 7 + data/desktop/snap-userd-autostart.desktop.in | 5 + data/env/Makefile | 37 + data/env/snapd.sh.in | 22 + data/failure.txt | 8 + data/polkit/io.snapcraft.snapd.policy | 40 + data/selinux/COPYING | 339 + data/selinux/INSTALL.md | 32 + data/selinux/Makefile | 35 + data/selinux/README.md | 25 + data/selinux/snappy.fc | 47 + data/selinux/snappy.if | 273 + data/selinux/snappy.te | 284 + data/success.txt | 20 + data/sysctl/rhel7-snap.conf | 4 + data/systemd-env/990-snapd.conf.in | 1 + data/systemd-env/Makefile | 37 + data/systemd/Makefile | 52 + data/systemd/snapd.apparmor.service.in | 21 + data/systemd/snapd.autoimport.service.in | 14 + data/systemd/snapd.core-fixup.service.in | 15 + data/systemd/snapd.core-fixup.sh | 79 + data/systemd/snapd.failure.service.in | 8 + data/systemd/snapd.run-from-snap | 6 + data/systemd/snapd.seeded.service.in | 14 + data/systemd/snapd.service.in | 23 + data/systemd/snapd.snap-repair.service.in | 13 + data/systemd/snapd.snap-repair.timer | 14 + data/systemd/snapd.socket | 13 + data/systemd/snapd.system-shutdown.service.in | 18 + data/udev/rules.d/66-snapd-autoimport.rules | 3 + debian | 1 + dirs/dirs.go | 332 + dirs/dirs_test.go | 158 + dirs/export_test.go | 32 + docs/MOVED.md | 1 + errtracker/errtracker.go | 516 + errtracker/errtracker_test.go | 544 + errtracker/export_test.go | 126 + features/export_test.go | 24 + features/features.go | 130 + features/features_test.go | 102 + gen-coverage.sh | 9 + generate-packaging-dir | 17 + get-deps.sh | 37 + httputil/client.go | 58 + httputil/client_test.go | 62 + httputil/export_test.go | 34 + httputil/logger.go | 106 + httputil/logger_test.go | 182 + httputil/redirect17.go | 40 + httputil/redirect18.go | 28 + httputil/retry.go | 166 + httputil/retry_test.go | 463 + httputil/transport16.go | 41 + httputil/transport17.go | 43 + httputil/useragent.go | 97 + httputil/useragent_test.go | 79 + httputil/withtestkeys.go | 26 + i18n/i18n.go | 119 + i18n/i18n_test.go | 162 + i18n/xgettext-go/main.go | 329 + i18n/xgettext-go/main_test.go | 515 + image/export_test.go | 48 + image/helpers.go | 330 + image/image.go | 672 + image/image_test.go | 1739 ++ interfaces/apparmor/apparmor.go | 140 + interfaces/apparmor/apparmor_test.go | 237 + interfaces/apparmor/backend.go | 626 + interfaces/apparmor/backend_test.go | 1768 +++ interfaces/apparmor/export_test.go | 110 + interfaces/apparmor/spec.go | 442 + interfaces/apparmor/spec_test.go | 486 + interfaces/apparmor/template.go | 727 + interfaces/apparmor/template_vars.go | 43 + interfaces/backend.go | 97 + interfaces/backends/backends.go | 74 + interfaces/backends/backends_test.go | 76 + interfaces/backends/export_test.go | 24 + interfaces/builtin/account_control.go | 125 + interfaces/builtin/account_control_test.go | 115 + interfaces/builtin/accounts_service.go | 81 + interfaces/builtin/accounts_service_test.go | 81 + interfaces/builtin/adb_support.go | 187 + interfaces/builtin/adb_support_test.go | 156 + interfaces/builtin/all.go | 138 + interfaces/builtin/all_test.go | 415 + interfaces/builtin/alsa.go | 70 + interfaces/builtin/alsa_test.go | 113 + interfaces/builtin/autopilot.go | 76 + interfaces/builtin/autopilot_test.go | 103 + interfaces/builtin/avahi_control.go | 175 + interfaces/builtin/avahi_control_test.go | 201 + interfaces/builtin/avahi_observe.go | 476 + interfaces/builtin/avahi_observe_test.go | 201 + interfaces/builtin/block_devices.go | 98 + interfaces/builtin/block_devices_test.go | 118 + interfaces/builtin/bluetooth_control.go | 75 + interfaces/builtin/bluetooth_control_test.go | 117 + interfaces/builtin/bluez.go | 284 + interfaces/builtin/bluez_test.go | 274 + interfaces/builtin/bool_file.go | 147 + interfaces/builtin/bool_file_test.go | 222 + interfaces/builtin/broadcom_asic_control.go | 79 + .../builtin/broadcom_asic_control_test.go | 128 + interfaces/builtin/browser_support.go | 350 + interfaces/builtin/browser_support_test.go | 200 + interfaces/builtin/calendar_service.go | 142 + interfaces/builtin/calendar_service_test.go | 93 + interfaces/builtin/camera.go | 64 + interfaces/builtin/camera_test.go | 113 + interfaces/builtin/can_bus.go | 56 + interfaces/builtin/can_bus_test.go | 107 + interfaces/builtin/cifs_mount.go | 78 + interfaces/builtin/cifs_mount_test.go | 107 + interfaces/builtin/classic_support.go | 132 + interfaces/builtin/classic_support_test.go | 98 + interfaces/builtin/common.go | 169 + interfaces/builtin/common_files.go | 166 + interfaces/builtin/common_test.go | 198 + interfaces/builtin/contacts_service.go | 161 + interfaces/builtin/contacts_service_test.go | 93 + interfaces/builtin/content.go | 317 + interfaces/builtin/content_test.go | 1094 ++ interfaces/builtin/core_support.go | 52 + interfaces/builtin/core_support_test.go | 95 + interfaces/builtin/cpu_control.go | 49 + interfaces/builtin/cpu_control_test.go | 95 + interfaces/builtin/cups_control.go | 49 + interfaces/builtin/daemon_notify.go | 104 + interfaces/builtin/daemon_notify_test.go | 175 + interfaces/builtin/dbus.go | 442 + interfaces/builtin/dbus_test.go | 700 + interfaces/builtin/dcdbas_control.go | 64 + interfaces/builtin/dcdbas_control_test.go | 94 + interfaces/builtin/desktop.go | 300 + interfaces/builtin/desktop_legacy.go | 255 + interfaces/builtin/desktop_legacy_test.go | 108 + interfaces/builtin/desktop_test.go | 219 + interfaces/builtin/device_buttons.go | 93 + interfaces/builtin/device_buttons_test.go | 117 + interfaces/builtin/display_control.go | 137 + interfaces/builtin/display_control_test.go | 125 + interfaces/builtin/docker.go | 55 + interfaces/builtin/docker_support.go | 618 + interfaces/builtin/docker_support_test.go | 191 + interfaces/builtin/docker_test.go | 96 + interfaces/builtin/dummy.go | 101 + interfaces/builtin/dvb.go | 52 + interfaces/builtin/dvb_test.go | 110 + interfaces/builtin/export_test.go | 104 + interfaces/builtin/firewall_control.go | 175 + interfaces/builtin/firewall_control_test.go | 123 + interfaces/builtin/framebuffer.go | 53 + interfaces/builtin/framebuffer_test.go | 115 + interfaces/builtin/fuse_support.go | 108 + interfaces/builtin/fuse_support_test.go | 122 + interfaces/builtin/fwupd.go | 274 + interfaces/builtin/fwupd_test.go | 172 + interfaces/builtin/gpg_keys.go | 64 + interfaces/builtin/gpg_keys_test.go | 103 + interfaces/builtin/gpg_public_keys.go | 62 + interfaces/builtin/gpg_public_keys_test.go | 103 + interfaces/builtin/gpio.go | 127 + interfaces/builtin/gpio_memory_control.go | 54 + .../builtin/gpio_memory_control_test.go | 112 + interfaces/builtin/gpio_test.go | 163 + interfaces/builtin/greengrass_support.go | 205 + interfaces/builtin/greengrass_support_test.go | 100 + interfaces/builtin/gsettings.go | 56 + interfaces/builtin/gsettings_test.go | 111 + interfaces/builtin/hardware_observe.go | 128 + interfaces/builtin/hardware_observe_test.go | 105 + interfaces/builtin/hardware_random_control.go | 61 + .../builtin/hardware_random_control_test.go | 113 + interfaces/builtin/hardware_random_observe.go | 56 + .../builtin/hardware_random_observe_test.go | 113 + interfaces/builtin/hidraw.go | 210 + interfaces/builtin/hidraw_test.go | 355 + interfaces/builtin/home.go | 132 + interfaces/builtin/home_test.go | 183 + interfaces/builtin/hostname_control.go | 91 + interfaces/builtin/hostname_control_test.go | 110 + interfaces/builtin/i2c.go | 152 + interfaces/builtin/i2c_test.go | 261 + interfaces/builtin/iio.go | 136 + interfaces/builtin/iio_test.go | 220 + interfaces/builtin/io_ports_control.go | 65 + interfaces/builtin/io_ports_control_test.go | 121 + interfaces/builtin/joystick.go | 110 + interfaces/builtin/joystick_test.go | 119 + interfaces/builtin/juju_client_observe.go | 46 + .../builtin/juju_client_observe_test.go | 99 + interfaces/builtin/kernel_module_control.go | 82 + .../builtin/kernel_module_control_test.go | 121 + interfaces/builtin/kernel_module_observe.go | 58 + .../builtin/kernel_module_observe_test.go | 101 + interfaces/builtin/kubernetes_support.go | 265 + interfaces/builtin/kubernetes_support_test.go | 240 + interfaces/builtin/kvm.go | 52 + interfaces/builtin/kvm_test.go | 118 + interfaces/builtin/libvirt.go | 53 + interfaces/builtin/libvirt_test.go | 65 + interfaces/builtin/locale_control.go | 49 + interfaces/builtin/locale_control_test.go | 96 + interfaces/builtin/location_control.go | 256 + interfaces/builtin/location_control_test.go | 206 + interfaces/builtin/location_observe.go | 310 + interfaces/builtin/location_observe_test.go | 200 + interfaces/builtin/log_observe.go | 74 + interfaces/builtin/log_observe_test.go | 94 + interfaces/builtin/lxd.go | 53 + interfaces/builtin/lxd_support.go | 67 + interfaces/builtin/lxd_support_test.go | 113 + interfaces/builtin/lxd_test.go | 111 + interfaces/builtin/maliit.go | 173 + interfaces/builtin/maliit_test.go | 256 + interfaces/builtin/media_hub.go | 204 + interfaces/builtin/media_hub_test.go | 197 + interfaces/builtin/mir.go | 157 + interfaces/builtin/mir_test.go | 167 + interfaces/builtin/modem_manager.go | 1288 ++ interfaces/builtin/modem_manager_test.go | 249 + interfaces/builtin/mount_observe.go | 77 + interfaces/builtin/mount_observe_test.go | 93 + interfaces/builtin/mpris.go | 240 + interfaces/builtin/mpris_test.go | 338 + interfaces/builtin/netlink_audit.go | 65 + interfaces/builtin/netlink_audit_test.go | 103 + interfaces/builtin/netlink_connector.go | 62 + interfaces/builtin/netlink_connector_test.go | 94 + interfaces/builtin/network.go | 92 + interfaces/builtin/network_bind.go | 108 + interfaces/builtin/network_bind_test.go | 102 + interfaces/builtin/network_control.go | 296 + interfaces/builtin/network_control_test.go | 120 + interfaces/builtin/network_manager.go | 503 + interfaces/builtin/network_manager_test.go | 207 + interfaces/builtin/network_observe.go | 165 + interfaces/builtin/network_observe_test.go | 103 + interfaces/builtin/network_setup_control.go | 49 + .../builtin/network_setup_control_test.go | 94 + interfaces/builtin/network_setup_observe.go | 49 + .../builtin/network_setup_observe_test.go | 95 + interfaces/builtin/network_status.go | 154 + interfaces/builtin/network_status_test.go | 108 + interfaces/builtin/network_test.go | 103 + interfaces/builtin/ofono.go | 362 + interfaces/builtin/ofono_test.go | 217 + interfaces/builtin/online_accounts_service.go | 143 + .../builtin/online_accounts_service_test.go | 111 + interfaces/builtin/opengl.go | 132 + interfaces/builtin/opengl_test.go | 117 + interfaces/builtin/openvswitch.go | 45 + interfaces/builtin/openvswitch_support.go | 49 + .../builtin/openvswitch_support_test.go | 101 + interfaces/builtin/openvswitch_test.go | 93 + interfaces/builtin/optical_drive.go | 104 + interfaces/builtin/optical_drive_test.go | 171 + .../builtin/password_manager_service.go | 93 + .../builtin/password_manager_service_test.go | 95 + interfaces/builtin/personal_files.go | 79 + interfaces/builtin/personal_files_test.go | 159 + interfaces/builtin/physical_memory_control.go | 57 + .../builtin/physical_memory_control_test.go | 113 + interfaces/builtin/physical_memory_observe.go | 53 + .../builtin/physical_memory_observe_test.go | 114 + interfaces/builtin/ppp.go | 75 + interfaces/builtin/ppp_test.go | 123 + interfaces/builtin/process_control.go | 75 + interfaces/builtin/process_control_test.go | 102 + interfaces/builtin/pulseaudio.go | 183 + interfaces/builtin/pulseaudio_test.go | 138 + interfaces/builtin/raw_usb.go | 67 + interfaces/builtin/raw_usb_test.go | 115 + interfaces/builtin/removable_media.go | 62 + interfaces/builtin/removable_media_test.go | 95 + interfaces/builtin/screen_inhibit_control.go | 93 + .../builtin/screen_inhibit_control_test.go | 94 + interfaces/builtin/screencast_legacy.go | 64 + interfaces/builtin/screencast_legacy_test.go | 107 + interfaces/builtin/serial_port.go | 227 + interfaces/builtin/serial_port_test.go | 556 + interfaces/builtin/shutdown.go | 75 + interfaces/builtin/shutdown_test.go | 92 + interfaces/builtin/snapd_control.go | 75 + interfaces/builtin/snapd_control_test.go | 118 + interfaces/builtin/spi.go | 106 + interfaces/builtin/spi_test.go | 217 + interfaces/builtin/ssh_keys.go | 51 + interfaces/builtin/ssh_keys_test.go | 103 + interfaces/builtin/ssh_public_keys.go | 51 + interfaces/builtin/ssh_public_keys_test.go | 103 + .../builtin/storage_framework_service.go | 162 + .../builtin/storage_framework_service_test.go | 105 + interfaces/builtin/system_files.go | 79 + interfaces/builtin/system_files_test.go | 182 + interfaces/builtin/system_observe.go | 129 + interfaces/builtin/system_observe_test.go | 104 + interfaces/builtin/system_trace.go | 75 + interfaces/builtin/system_trace_test.go | 93 + interfaces/builtin/thumbnailer_service.go | 148 + .../builtin/thumbnailer_service_test.go | 124 + interfaces/builtin/time_control.go | 134 + interfaces/builtin/time_control_test.go | 122 + interfaces/builtin/timeserver_control.go | 100 + interfaces/builtin/timeserver_control_test.go | 94 + interfaces/builtin/timezone_control.go | 102 + interfaces/builtin/timezone_control_test.go | 94 + interfaces/builtin/tpm.go | 56 + interfaces/builtin/tpm_test.go | 115 + interfaces/builtin/u2f_devices.go | 149 + interfaces/builtin/u2f_devices_test.go | 116 + interfaces/builtin/ubuntu_download_manager.go | 246 + .../builtin/ubuntu_download_manager_test.go | 93 + interfaces/builtin/udisks2.go | 460 + interfaces/builtin/udisks2_test.go | 292 + interfaces/builtin/uhid.go | 52 + interfaces/builtin/uhid_test.go | 104 + interfaces/builtin/unity7.go | 675 + interfaces/builtin/unity7_test.go | 104 + interfaces/builtin/unity8.go | 124 + interfaces/builtin/unity8_calendar.go | 157 + interfaces/builtin/unity8_calendar_test.go | 218 + interfaces/builtin/unity8_contacts.go | 194 + interfaces/builtin/unity8_contacts_test.go | 221 + interfaces/builtin/unity8_pim_common.go | 171 + interfaces/builtin/unity8_test.go | 94 + interfaces/builtin/upower_observe.go | 277 + interfaces/builtin/upower_observe_test.go | 247 + interfaces/builtin/utils.go | 98 + interfaces/builtin/utils_test.go | 104 + interfaces/builtin/wayland.go | 167 + interfaces/builtin/wayland_test.go | 212 + interfaces/builtin/x11.go | 189 + interfaces/builtin/x11_test.go | 217 + interfaces/connection.go | 284 + interfaces/connection_test.go | 359 + interfaces/core.go | 244 + interfaces/core_test.go | 188 + interfaces/dbus/backend.go | 177 + interfaces/dbus/backend_test.go | 280 + interfaces/dbus/dbus.go | 52 + interfaces/dbus/dbus_test.go | 42 + interfaces/dbus/export_test.go | 35 + interfaces/dbus/spec.go | 132 + interfaces/dbus/spec_test.go | 105 + interfaces/dbus/template.go | 29 + interfaces/export_test.go | 66 + interfaces/hotplug/deviceinfo.go | 87 + interfaces/hotplug/deviceinfo_test.go | 98 + interfaces/hotplug/spec.go | 89 + interfaces/hotplug/spec_test.go | 71 + interfaces/hotplug/udevadm.go | 126 + interfaces/hotplug/udevadm_test.go | 126 + interfaces/ifacetest/backend.go | 87 + interfaces/ifacetest/backendtest.go | 207 + interfaces/ifacetest/ifacetest_test.go | 30 + interfaces/ifacetest/spec.go | 81 + interfaces/ifacetest/spec_test.go | 93 + interfaces/ifacetest/testiface.go | 428 + interfaces/ifacetest/testiface_test.go | 242 + interfaces/kmod/backend.go | 138 + interfaces/kmod/backend_test.go | 137 + interfaces/kmod/export_test.go | 24 + interfaces/kmod/kmod.go | 36 + interfaces/kmod/kmod_test.go | 56 + interfaces/kmod/spec.go | 104 + interfaces/kmod/spec_test.go | 129 + interfaces/mount/backend.go | 138 + interfaces/mount/backend_test.go | 221 + interfaces/mount/lock.go | 46 + interfaces/mount/lock_test.go | 53 + interfaces/mount/ns.go | 66 + interfaces/mount/ns_test.go | 144 + interfaces/mount/spec.go | 253 + interfaces/mount/spec_test.go | 226 + interfaces/naming.go | 34 + interfaces/naming_test.go | 38 + interfaces/policy/basedeclaration.go | 199 + interfaces/policy/basedeclaration_test.go | 966 ++ interfaces/policy/export_test.go | 26 + interfaces/policy/helpers.go | 286 + interfaces/policy/helpers_test.go | 100 + interfaces/policy/policy.go | 258 + interfaces/policy/policy_test.go | 2122 +++ interfaces/repo.go | 1152 ++ interfaces/repo_test.go | 2453 +++ interfaces/seccomp/backend.go | 346 + interfaces/seccomp/backend_test.go | 570 + interfaces/seccomp/export_test.go | 86 + interfaces/seccomp/seccomp_test.go | 30 + interfaces/seccomp/spec.go | 137 + interfaces/seccomp/spec_test.go | 105 + interfaces/seccomp/template.go | 580 + interfaces/sorting.go | 115 + interfaces/sorting_test.go | 72 + interfaces/system_key.go | 257 + interfaces/system_key_test.go | 214 + interfaces/systemd/backend.go | 172 + interfaces/systemd/backend_test.go | 159 + interfaces/systemd/service.go | 58 + interfaces/systemd/service_test.go | 45 + interfaces/systemd/spec.go | 107 + interfaces/systemd/spec_test.go | 55 + interfaces/systemd/systemd_test.go | 30 + interfaces/udev/backend.go | 171 + interfaces/udev/backend_test.go | 504 + interfaces/udev/spec.go | 191 + interfaces/udev/spec_test.go | 140 + interfaces/udev/udev.go | 107 + interfaces/udev/udev_test.go | 171 + interfaces/utils/utils.go | 82 + interfaces/utils/utils_test.go | 82 + jsonutil/json.go | 66 + jsonutil/json_test.go | 90 + jsonutil/safejson/safejson.go | 202 + jsonutil/safejson/safejson_test.go | 148 + logger/export_test.go | 36 + logger/logger.go | 146 + logger/logger_test.go | 113 + mdlint.py | 36 + mkauthors.sh | 52 + mkversion.sh | 86 + netutil/metered.go | 65 + osutil/bootid.go | 40 + osutil/bootid_test.go | 36 + osutil/buildid.go | 111 + osutil/buildid_test.go | 99 + osutil/chattr.go | 76 + osutil/chattr_32.go | 27 + osutil/chattr_64.go | 28 + osutil/chdir.go | 38 + osutil/chdir_test.go | 59 + osutil/cmp.go | 91 + osutil/cmp_test.go | 101 + osutil/context.go | 83 + osutil/context_test.go | 132 + osutil/cp.go | 171 + osutil/cp_linux.go | 48 + osutil/cp_linux_test.go | 45 + osutil/cp_other.go | 31 + osutil/cp_test.go | 302 + osutil/digest.go | 46 + osutil/digest_test.go | 50 + osutil/env.go | 115 + osutil/env_test.go | 162 + osutil/exec.go | 278 + osutil/exec_test.go | 228 + osutil/exitcode.go | 37 + osutil/exitcode_test.go | 56 + osutil/export_test.go | 157 + osutil/flock.go | 73 + osutil/flock_test.go | 145 + osutil/fshelpers.go | 38 + osutil/fshelpers_test.go | 51 + osutil/group.go | 158 + osutil/io.go | 228 + osutil/io_test.go | 254 + osutil/mkdirallchown.go | 87 + osutil/mkdirallchown_test.go | 48 + osutil/mockable.go | 45 + osutil/mount_darwin.go | 25 + osutil/mount_linux.go | 34 + osutil/mount_linux_test.go | 63 + osutil/mountentry_linux.go | 458 + osutil/mountentry_linux_test.go | 434 + osutil/mountinfo_linux.go | 202 + osutil/mountinfo_linux_test.go | 168 + osutil/mountprofile_linux.go | 108 + osutil/mountprofile_linux_test.go | 152 + osutil/nfs_darwin.go | 25 + osutil/nfs_linux.go | 52 + osutil/nfs_linux_test.go | 94 + osutil/osutil_darwin.go | 27 + osutil/osutil_test.go | 29 + osutil/outputerr.go | 39 + osutil/outputerr_test.go | 54 + osutil/overlay_darwin.go | 25 + osutil/overlay_linux.go | 89 + osutil/overlay_linux_test.go | 103 + osutil/squashfs/fstype.go | 82 + osutil/stat.go | 117 + osutil/stat_test.go | 205 + osutil/strace/export_test.go | 32 + osutil/strace/strace.go | 93 + osutil/strace/strace_test.go | 118 + osutil/strace/timing.go | 233 + osutil/strace/timing_test.go | 137 + osutil/syncdir.go | 184 + osutil/syncdir_test.go | 243 + osutil/sys/syscall.go | 109 + osutil/sys/sysnum_16_linux.go | 33 + osutil/sys/sysnum_32_linux.go | 32 + osutil/sys/sysnum_darwin.go | 30 + osutil/sys/sysnum_linux.go | 26 + osutil/sys_linux.go | 64 + osutil/sys_linux_test.go | 72 + osutil/udev/.travis.yml | 8 + osutil/udev/LICENSE | 674 + osutil/udev/README.md | 126 + osutil/udev/crawler/device.go | 112 + osutil/udev/main.go.sample | 145 + osutil/udev/matcher.sample | 21 + osutil/udev/netlink/conn.go | 137 + osutil/udev/netlink/conn_test.go | 20 + osutil/udev/netlink/matcher.go | 169 + osutil/udev/netlink/matcher_test.go | 119 + osutil/udev/netlink/uevent.go | 178 + osutil/udev/netlink/uevent_test.go | 188 + osutil/uname.go | 79 + osutil/uname_darwin.go | 30 + osutil/uname_linux.go | 32 + osutil/uname_linux_test.go | 96 + osutil/unlink.go | 72 + osutil/unlink_darwin.go | 29 + osutil/unlink_linux.go | 27 + osutil/unlink_test.go | 108 + osutil/user.go | 189 + osutil/user_test.go | 253 + osutil/winsize.go | 48 + overlord/assertstate/assertmgr.go | 145 + overlord/assertstate/assertstate.go | 404 + overlord/assertstate/assertstate_test.go | 1549 ++ overlord/assertstate/export_test.go | 25 + overlord/assertstate/helpers.go | 127 + overlord/auth/auth.go | 569 + overlord/auth/auth_test.go | 836 + overlord/backend.go | 45 + overlord/cmdstate/cmdmgr.go | 75 + overlord/cmdstate/cmdstate.go | 37 + overlord/cmdstate/cmdstate_test.go | 200 + overlord/cmdstate/export_test.go | 32 + overlord/configstate/config/helpers.go | 310 + overlord/configstate/config/helpers_test.go | 203 + overlord/configstate/config/transaction.go | 307 + .../configstate/config/transaction_test.go | 348 + overlord/configstate/configcore/cloud.go | 122 + overlord/configstate/configcore/cloud_test.go | 219 + overlord/configstate/configcore/corecfg.go | 138 + .../configstate/configcore/corecfg_test.go | 133 + .../configstate/configcore/experimental.go | 72 + .../configcore/experimental_test.go | 79 + .../configstate/configcore/export_test.go | 27 + overlord/configstate/configcore/network.go | 88 + .../configstate/configcore/network_test.go | 117 + overlord/configstate/configcore/picfg.go | 109 + overlord/configstate/configcore/picfg_test.go | 172 + overlord/configstate/configcore/powerbtn.go | 86 + .../configstate/configcore/powerbtn_test.go | 79 + overlord/configstate/configcore/proxy.go | 119 + overlord/configstate/configcore/proxy_test.go | 199 + overlord/configstate/configcore/refresh.go | 134 + .../configstate/configcore/refresh_test.go | 169 + overlord/configstate/configcore/services.go | 126 + .../configstate/configcore/services_test.go | 186 + overlord/configstate/configcore/utils.go | 97 + overlord/configstate/configcore/utils_test.go | 65 + overlord/configstate/configcore/watchdog.go | 120 + .../configstate/configcore/watchdog_test.go | 212 + overlord/configstate/configmgr.go | 53 + overlord/configstate/configstate.go | 138 + overlord/configstate/configstate_test.go | 311 + overlord/configstate/export_test.go | 22 + overlord/configstate/handler_test.go | 230 + overlord/configstate/hooks.go | 129 + overlord/configstate/proxyconf/proxy.go | 57 + overlord/configstate/proxyconf/proxy_test.go | 74 + overlord/configstate/settings/settings.go | 41 + .../configstate/settings/settings_test.go | 60 + overlord/devicestate/crypto.go | 80 + overlord/devicestate/devicemgr.go | 574 + overlord/devicestate/devicestate.go | 288 + overlord/devicestate/devicestate_test.go | 2597 +++ overlord/devicestate/export_test.go | 119 + overlord/devicestate/firstboot.go | 357 + overlord/devicestate/firstboot_test.go | 1676 ++ overlord/devicestate/handlers.go | 608 + overlord/export_test.go | 76 + overlord/hookstate/context.go | 250 + overlord/hookstate/context_test.go | 166 + overlord/hookstate/ctlcmd/ctlcmd.go | 141 + overlord/hookstate/ctlcmd/ctlcmd_test.go | 76 + overlord/hookstate/ctlcmd/export_test.go | 80 + overlord/hookstate/ctlcmd/get.go | 338 + overlord/hookstate/ctlcmd/get_test.go | 352 + overlord/hookstate/ctlcmd/helpers.go | 150 + overlord/hookstate/ctlcmd/restart.go | 56 + overlord/hookstate/ctlcmd/services.go | 97 + overlord/hookstate/ctlcmd/services_test.go | 488 + overlord/hookstate/ctlcmd/set.go | 206 + overlord/hookstate/ctlcmd/set_test.go | 326 + overlord/hookstate/ctlcmd/start.go | 56 + overlord/hookstate/ctlcmd/stop.go | 56 + overlord/hookstate/export_test.go | 46 + overlord/hookstate/hookmgr.go | 476 + overlord/hookstate/hooks.go | 111 + overlord/hookstate/hookstate.go | 48 + overlord/hookstate/hookstate_test.go | 1196 ++ overlord/hookstate/hooktest/handler.go | 78 + overlord/hookstate/hooktest/handler_test.go | 91 + overlord/hookstate/repository.go | 74 + overlord/hookstate/repository_test.go | 69 + overlord/ifacestate/export_test.go | 148 + overlord/ifacestate/handlers.go | 1414 ++ overlord/ifacestate/handlers_test.go | 66 + overlord/ifacestate/helpers.go | 969 ++ overlord/ifacestate/helpers_test.go | 525 + overlord/ifacestate/hooks.go | 58 + overlord/ifacestate/hotplug.go | 210 + overlord/ifacestate/hotplug_test.go | 394 + overlord/ifacestate/ifacemgr.go | 195 + overlord/ifacestate/ifacerepo/repo.go | 41 + overlord/ifacestate/ifacerepo/repo_test.go | 63 + overlord/ifacestate/ifacestate.go | 475 + overlord/ifacestate/ifacestate_test.go | 5500 +++++++ overlord/ifacestate/implicit.go | 96 + overlord/ifacestate/implicit_test.go | 116 + overlord/ifacestate/udevmonitor/udevmon.go | 197 + .../ifacestate/udevmonitor/udevmon_test.go | 182 + overlord/managers_test.go | 2772 ++++ overlord/overlord.go | 495 + overlord/overlord_test.go | 817 + overlord/patch/export_test.go | 73 + overlord/patch/patch.go | 269 + overlord/patch/patch1.go | 123 + overlord/patch/patch1_test.go | 158 + overlord/patch/patch2.go | 165 + overlord/patch/patch2_test.go | 179 + overlord/patch/patch3.go | 61 + overlord/patch/patch3_test.go | 149 + overlord/patch/patch4.go | 322 + overlord/patch/patch4_test.go | 451 + overlord/patch/patch5.go | 85 + overlord/patch/patch6.go | 115 + overlord/patch/patch6_1.go | 149 + overlord/patch/patch6_1_test.go | 284 + overlord/patch/patch6_test.go | 209 + overlord/patch/patch_test.go | 458 + overlord/servicestate/servicestate.go | 121 + overlord/snapshotstate/backend/backend.go | 314 + .../snapshotstate/backend/backend_test.go | 714 + overlord/snapshotstate/backend/export_test.go | 97 + overlord/snapshotstate/backend/helpers.go | 229 + overlord/snapshotstate/backend/reader.go | 378 + .../snapshotstate/backend/restorestate.go | 96 + overlord/snapshotstate/backend/sizer.go | 34 + overlord/snapshotstate/export_test.go | 163 + overlord/snapshotstate/snapshotmgr.go | 307 + overlord/snapshotstate/snapshotmgr_test.go | 481 + overlord/snapshotstate/snapshotstate.go | 326 + overlord/snapshotstate/snapshotstate_test.go | 1343 ++ overlord/snapstate/aliasesv2.go | 704 + overlord/snapstate/aliasesv2_test.go | 1557 ++ overlord/snapstate/autorefresh.go | 465 + overlord/snapstate/autorefresh_test.go | 580 + overlord/snapstate/backend.go | 89 + overlord/snapstate/backend/aliases.go | 111 + overlord/snapstate/backend/aliases_test.go | 368 + overlord/snapstate/backend/backend.go | 51 + overlord/snapstate/backend/backend_test.go | 108 + overlord/snapstate/backend/copydata.go | 100 + overlord/snapstate/backend/copydata_test.go | 540 + overlord/snapstate/backend/export_test.go | 33 + overlord/snapstate/backend/fontconfig.go | 44 + overlord/snapstate/backend/link.go | 185 + overlord/snapstate/backend/link_test.go | 338 + overlord/snapstate/backend/mountns.go | 29 + overlord/snapstate/backend/mountunit.go | 41 + overlord/snapstate/backend/mountunit_test.go | 121 + overlord/snapstate/backend/setup.go | 137 + overlord/snapstate/backend/setup_test.go | 353 + overlord/snapstate/backend/snapdata.go | 261 + overlord/snapstate/backend/snapdata_test.go | 120 + overlord/snapstate/backend/utils.go | 30 + overlord/snapstate/backend_test.go | 994 ++ overlord/snapstate/booted.go | 172 + overlord/snapstate/booted_test.go | 396 + overlord/snapstate/catalogrefresh.go | 143 + overlord/snapstate/catalogrefresh_test.go | 173 + overlord/snapstate/check_snap.go | 411 + overlord/snapstate/check_snap_test.go | 823 + overlord/snapstate/conflict.go | 169 + overlord/snapstate/cookies.go | 168 + overlord/snapstate/cookies_test.go | 156 + overlord/snapstate/export_test.go | 210 + overlord/snapstate/flags.go | 74 + overlord/snapstate/handlers.go | 1984 +++ overlord/snapstate/handlers_aliasesv2_test.go | 1952 +++ overlord/snapstate/handlers_discard_test.go | 170 + overlord/snapstate/handlers_download_test.go | 237 + overlord/snapstate/handlers_link_test.go | 744 + overlord/snapstate/handlers_mount_test.go | 492 + overlord/snapstate/handlers_prepare_test.go | 108 + overlord/snapstate/handlers_prereq_test.go | 371 + overlord/snapstate/progress.go | 99 + overlord/snapstate/progress_test.go | 58 + overlord/snapstate/readme.go | 65 + overlord/snapstate/readme_test.go | 76 + overlord/snapstate/refreshhints.go | 116 + overlord/snapstate/refreshhints_test.go | 163 + overlord/snapstate/snapmgr.go | 661 + overlord/snapstate/snapstate.go | 2160 +++ overlord/snapstate/snapstate_test.go | 13078 ++++++++++++++++ overlord/snapstate/storehelpers.go | 479 + overlord/standby/export_test.go | 37 + overlord/standby/standby.go | 130 + overlord/standby/standby_test.go | 205 + overlord/state/change.go | 610 + overlord/state/change_test.go | 725 + overlord/state/export_test.go | 67 + overlord/state/state.go | 484 + overlord/state/state_test.go | 953 ++ overlord/state/task.go | 483 + overlord/state/task_test.go | 512 + overlord/state/taskrunner.go | 517 + overlord/state/taskrunner_test.go | 909 ++ overlord/state/warning.go | 292 + overlord/state/warning_test.go | 267 + overlord/stateengine.go | 146 + overlord/stateengine_test.go | 123 + overlord/unknowntask.go | 34 + packaging/amzn-2 | 1 + packaging/arch/PKGBUILD | 192 + packaging/arch/snapd.install | 49 + packaging/build-tools/go | 16 + packaging/centos-7 | 1 + packaging/fedora-25 | 1 + packaging/fedora-26 | 1 + packaging/fedora-27 | 1 + packaging/fedora-28 | 1 + packaging/fedora-29 | 1 + packaging/fedora-rawhide | 1 + packaging/fedora/snapd.spec | 4516 ++++++ packaging/opensuse-15.0 | 1 + packaging/opensuse-42.1 | 1 + packaging/opensuse-42.2 | 1 + packaging/opensuse-42.3 | 1 + packaging/opensuse-tumbleweed | 1 + packaging/opensuse/permissions | 1 + packaging/opensuse/permissions.easy | 1 + packaging/opensuse/permissions.paranoid | 1 + packaging/opensuse/permissions.secure | 1 + packaging/opensuse/snapd-rpmlintrc | 4 + packaging/opensuse/snapd.changes | 350 + packaging/opensuse/snapd.spec | 434 + packaging/ubuntu-14.04/changelog | 6767 ++++++++ packaging/ubuntu-14.04/compat | 1 + packaging/ubuntu-14.04/control | 133 + packaging/ubuntu-14.04/copyright | 22 + .../golang-github-snapcore-snapd-dev.install | 1 + packaging/ubuntu-14.04/rules | 217 + .../ubuntu-14.04/snap-confine.maintscript | 1 + packaging/ubuntu-14.04/snap.mount.service | 16 + packaging/ubuntu-14.04/snapd.autoimport.udev | 1 + packaging/ubuntu-14.04/snapd.dirs | 15 + packaging/ubuntu-14.04/snapd.install | 39 + packaging/ubuntu-14.04/snapd.links | 2 + packaging/ubuntu-14.04/snapd.maintscript | 1 + packaging/ubuntu-14.04/snapd.manpages | 1 + packaging/ubuntu-14.04/snapd.postinst | 31 + packaging/ubuntu-14.04/snapd.postrm | 125 + packaging/ubuntu-14.04/snapd.prerm | 8 + packaging/ubuntu-14.04/source | 1 + packaging/ubuntu-14.04/tests/README.md | 1 + packaging/ubuntu-14.04/tests/control | 11 + packaging/ubuntu-14.04/tests/integrationtests | 41 + packaging/ubuntu-14.04/tests/testconfig.json | 1 + packaging/ubuntu-14.04/ubuntu-snappy-cli.dirs | 2 + packaging/ubuntu-16.04/changelog | 6805 ++++++++ packaging/ubuntu-16.04/compat | 1 + packaging/ubuntu-16.04/control | 128 + packaging/ubuntu-16.04/copyright | 22 + packaging/ubuntu-16.04/gbp.conf | 7 + .../golang-github-snapcore-snapd-dev.install | 1 + packaging/ubuntu-16.04/rules | 264 + .../ubuntu-16.04/snap-confine.maintscript | 1 + packaging/ubuntu-16.04/snapd.autoimport.udev | 3 + packaging/ubuntu-16.04/snapd.dirs | 15 + packaging/ubuntu-16.04/snapd.install | 44 + packaging/ubuntu-16.04/snapd.links | 2 + packaging/ubuntu-16.04/snapd.maintscript | 5 + packaging/ubuntu-16.04/snapd.manpages | 1 + packaging/ubuntu-16.04/snapd.postinst | 49 + packaging/ubuntu-16.04/snapd.postrm | 128 + packaging/ubuntu-16.04/source/format | 1 + packaging/ubuntu-16.04/source/options | 1 + packaging/ubuntu-16.04/tests/README.md | 10 + packaging/ubuntu-16.04/tests/control | 12 + packaging/ubuntu-16.04/tests/integrationtests | 51 + packaging/ubuntu-16.04/tests/testconfig.json | 3 + packaging/ubuntu-16.04/ubuntu-snappy-cli.dirs | 2 + packaging/ubuntu-16.10 | 1 + packaging/ubuntu-17.04 | 1 + partition/androidboot.go | 77 + partition/androidboot_test.go | 65 + partition/androidbootenv/androidbootenv.go | 90 + .../androidbootenv/androidbootenv_test.go | 69 + partition/bootloader.go | 165 + partition/bootloader_test.go | 159 + partition/export_test.go | 40 + partition/grub.go | 83 + partition/grub_test.go | 127 + partition/grubenv/grubenv.go | 117 + partition/grubenv/grubenv_test.go | 92 + partition/uboot.go | 94 + partition/uboot_test.go | 134 + partition/ubootenv/env.go | 294 + partition/ubootenv/env_test.go | 298 + partition/ubootenv/export_test.go | 24 + parts/plugins/x_builddeb.py | 57 + po/af.po | 2127 +++ po/am.po | 2127 +++ po/ar.po | 2127 +++ po/bs.po | 2145 +++ po/ca.po | 2127 +++ po/cs.po | 2138 +++ po/cy.po | 2127 +++ po/da.po | 2325 +++ po/de.po | 2131 +++ po/el.po | 2189 +++ po/en_GB.po | 2519 +++ po/eo.po | 2127 +++ po/es.po | 2552 +++ po/fi.po | 2137 +++ po/fr.po | 2624 ++++ po/gl.po | 2540 +++ po/hr.po | 2516 +++ po/hu.po | 2127 +++ po/ia.po | 2127 +++ po/id.po | 2127 +++ po/it.po | 2128 +++ po/its/polkit.its | 8 + po/ja.po | 2127 +++ po/km.po | 2127 +++ po/ko.po | 2127 +++ po/lt.po | 2127 +++ po/ms.po | 2127 +++ po/nb.po | 2221 +++ po/nl.po | 2195 +++ po/oc.po | 2127 +++ po/pl.po | 2127 +++ po/pt.po | 2128 +++ po/pt_BR.po | 2127 +++ po/ro.po | 2127 +++ po/ru.po | 2128 +++ po/sq.po | 2131 +++ po/sv.po | 2155 +++ po/th.po | 2127 +++ po/tr.po | 2142 +++ po/ug.po | 2127 +++ po/uk.po | 2179 +++ po/zh_CN.po | 2127 +++ po/zh_TW.po | 2127 +++ polkit/authority.go | 94 + polkit/pid_start_time.go | 68 + polkit/pid_start_time_test.go | 71 + progress/ansimeter.go | 205 + progress/ansimeter_test.go | 264 + progress/export_test.go | 102 + progress/progress.go | 105 + progress/progress_test.go | 65 + progress/progresstest/progresstest.go | 68 + release-tools/repack-debian-tarball.sh | 92 + release/apparmor.go | 382 + release/apparmor_test.go | 273 + release/export_test.go | 89 + release/release.go | 168 + release/release_test.go | 174 + release/seccomp.go | 81 + release/seccomp_test.go | 69 + release/selinux.go | 90 + release/selinux_test.go | 98 + run-checks | 281 + sanity/apparmor_lxd.go | 51 + sanity/apparmor_lxd_test.go | 40 + sanity/check.go | 32 + sanity/check_test.go | 123 + sanity/export_test.go | 47 + sanity/squashfs.go | 122 + sanity/squashfs_test.go | 95 + sanity/version.go | 102 + sanity/version_test.go | 167 + sanity/wsl.go | 38 + sanity/wsl_test.go | 51 + selinux/export_test.go | 43 + selinux/label.go | 26 + selinux/label_darwin.go | 30 + selinux/label_linux.go | 79 + selinux/label_linux_test.go | 139 + selinux/selinux_darwin.go | 29 + selinux/selinux_linux.go | 78 + selinux/selinux_linux_test.go | 153 + snap/broken.go | 107 + snap/broken_test.go | 132 + snap/channel.go | 171 + snap/channel_test.go | 211 + snap/container.go | 280 + snap/container_test.go | 410 + snap/epoch.go | 374 + snap/epoch_test.go | 375 + snap/errors.go | 50 + snap/export_test.go | 31 + snap/gadget.go | 227 + snap/gadget_test.go | 362 + snap/hooktypes.go | 78 + snap/implicit.go | 83 + snap/implicit_test.go | 28 + snap/info.go | 1227 ++ snap/info_snap_yaml.go | 646 + snap/info_snap_yaml_test.go | 1831 +++ snap/info_test.go | 1613 ++ snap/pack/export_test.go | 24 + snap/pack/pack.go | 191 + snap/pack/pack_test.go | 256 + snap/restartcond.go | 77 + snap/restartcond_test.go | 51 + snap/revision.go | 123 + snap/revision_test.go | 199 + snap/seed_yaml.go | 90 + snap/seed_yaml_test.go | 83 + snap/snapdir/snapdir.go | 178 + snap/snapdir/snapdir_test.go | 156 + snap/snapenv/snapenv.go | 210 + snap/snapenv/snapenv_test.go | 286 + snap/snaptest/snaptest.go | 260 + snap/snaptest/snaptest_test.go | 200 + snap/squashfs/export_test.go | 77 + snap/squashfs/squashfs.go | 367 + snap/squashfs/squashfs_test.go | 614 + snap/squashfs/stat.go | 370 + snap/squashfs/stat_test.go | 299 + snap/types.go | 145 + snap/types_test.go | 228 + snap/validate.go | 906 ++ snap/validate_test.go | 1596 ++ snapcraft.yaml | 32 + spdx/licenses.go | 500 + spdx/parser.go | 155 + spdx/parser_test.go | 77 + spdx/scanner.go | 69 + spdx/scanner_test.go | 49 + spdx/validate.go | 34 + spread-shellcheck | 253 + spread.yaml | 778 + store/auth.go | 320 + store/auth_test.go | 435 + store/cache.go | 204 + store/cache_test.go | 217 + store/details.go | 122 + store/details_v2.go | 311 + store/details_v2_test.go | 412 + store/download_test.go | 512 + store/errors.go | 266 + store/export_test.go | 177 + store/store.go | 2447 +++ store/store_test.go | 6905 ++++++++ store/storetest/storetest.go | 95 + store/stringlist_test.go | 41 + store/userinfo.go | 78 + store/userinfo_test.go | 152 + strutil/chrorder.go | 21 + strutil/chrorder/main.go | 74 + strutil/ctrl16.go | 51 + strutil/ctrl17.go | 52 + strutil/limbuffer.go | 51 + strutil/limbuffer_test.go | 67 + strutil/map.go | 121 + strutil/map_test.go | 96 + strutil/matchcounter.go | 103 + strutil/matchcounter_benchmark_test.go | 52 + strutil/matchcounter_test.go | 207 + strutil/pathiter.go | 138 + strutil/pathiter_test.go | 226 + strutil/quantity/example_test.go | 112 + strutil/quantity/quantity.go | 202 + strutil/shlex/shlex.go | 417 + strutil/shlex/shlex_test.go | 155 + strutil/strutil.go | 200 + strutil/strutil_test.go | 224 + strutil/version.go | 196 + strutil/version_benchmark_test.go | 111 + strutil/version_test.go | 128 + systemd/escape.go | 66 + systemd/escape_test.go | 37 + systemd/export_test.go | 56 + systemd/journal.go | 72 + systemd/journal_test.go | 89 + systemd/sdnotify.go | 59 + systemd/sdnotify_test.go | 87 + systemd/systemd.go | 632 + systemd/systemd_test.go | 751 + tests/completion/data/files/a/a_thing.txt | 0 tests/completion/data/files/b/b_thing.txt | 0 tests/completion/data/files/b/c/b_c_thing.txt | 0 tests/completion/data/files/d/d_thing.txt | 0 tests/completion/data/files/thing.txt | 0 tests/completion/data/hosts.txt | 1 + .../data/twisted/.just a hidden file | 0 .../this is a file with spaces in it.doc | 0 .../completion/data/twisted/this isn't.innit | 0 tests/completion/dirs.complete | 5 + tests/completion/dirs.sh | 1 + tests/completion/dirs.vars | 8 + tests/completion/files.complete | 4 + tests/completion/files.sh | 1 + tests/completion/files.vars | 7 + tests/completion/func.complete | 8 + tests/completion/func.sh | 0 tests/completion/func.vars | 7 + tests/completion/funcarg.complete | 12 + tests/completion/funcarg.sh | 0 tests/completion/funcarg.vars | 7 + tests/completion/funky.complete | 4 + tests/completion/funky.sh | 0 tests/completion/funky.vars | 7 + tests/completion/funkyfunc.complete | 12 + tests/completion/funkyfunc.sh | 0 tests/completion/funkyfunc.vars | 7 + tests/completion/hosts.complete | 4 + tests/completion/hosts.sh | 1 + tests/completion/hosts.vars | 7 + tests/completion/hosts_n_dirs.complete | 4 + tests/completion/hosts_n_dirs.sh | 2 + tests/completion/hosts_n_dirs.vars | 7 + tests/completion/indirect/task.exp | 21 + tests/completion/indirect/task.yaml | 28 + tests/completion/lib.exp0 | 70 + tests/completion/plain.complete | 4 + tests/completion/plain.sh | 0 tests/completion/plain.vars | 7 + tests/completion/plain_plusdirs.complete | 4 + tests/completion/plain_plusdirs.sh | 1 + tests/completion/plain_plusdirs.vars | 7 + tests/completion/simple/task.exp | 16 + tests/completion/simple/task.yaml | 8 + tests/completion/snippets/task.exp | 17 + tests/completion/snippets/task.yaml | 23 + tests/completion/twisted.complete | 4 + tests/completion/twisted.sh | 1 + tests/completion/twisted.vars | 7 + tests/core18/basic/task.yaml | 25 + tests/core18/compat/task.yaml | 11 + tests/core18/kernel/task.yaml | 5 + tests/core18/remove/task.yaml | 25 + tests/core18/snapd-failover/task.yaml | 36 + tests/core18/snapd-refresh/task.yaml | 40 + tests/cross/go-build/task.yaml | 57 + tests/external-backend.md | 37 + tests/lib/assertions/auto-import.assert | 27 + tests/lib/assertions/auto-import.assert.json | 13 + .../developer1-my-classic-w-gadget.model | 20 + .../assertions/developer1-my-classic.model | 19 + .../assertions/developer1-pc-w-config.model | 24 + tests/lib/assertions/developer1-pc.model | 21 + tests/lib/assertions/developer1.account | 19 + tests/lib/assertions/developer1.account-key | 30 + tests/lib/assertions/fake.store | 19 + tests/lib/assertions/nested-amd64.model | 21 + tests/lib/assertions/nested-amd64.model.json | 11 + tests/lib/assertions/nested-i386.model | 21 + tests/lib/assertions/nested-i386.model.json | 11 + tests/lib/assertions/pc-production.model | 21 + tests/lib/assertions/pc-staging.model | 21 + tests/lib/assertions/pi2.model | 21 + tests/lib/assertions/pi2.model.json | 11 + .../assertions/testrootorg-store.account-key | 30 + .../lib/assertions/ubuntu-core-18-amd64.model | 23 + tests/lib/best_golang.py | 12 + tests/lib/boot.sh | 47 + tests/lib/changes.sh | 8 + tests/lib/cla_check.py | 136 + tests/lib/dbus.sh | 35 + tests/lib/dirs.sh | 23 + tests/lib/external/prepare-ssh.sh | 15 + tests/lib/fakedevicesvc/main.go | 135 + tests/lib/fakegpio/fake-gpio.py | 108 + .../cmd/fakestore/cmd_make_refreshable.go | 39 + .../cmd/fakestore/cmd_new_snap_decl.go | 63 + .../cmd/fakestore/cmd_new_snap_rev.go | 63 + tests/lib/fakestore/cmd/fakestore/cmd_run.go | 70 + tests/lib/fakestore/cmd/fakestore/main.go | 51 + tests/lib/fakestore/refresh/refresh.go | 258 + tests/lib/fakestore/refresh/snap_asserts.go | 95 + tests/lib/fakestore/store/store.go | 730 + tests/lib/fakestore/store/store_test.go | 624 + tests/lib/files.sh | 74 + tests/lib/journalctl.sh | 65 + tests/lib/list-interfaces.go | 10 + tests/lib/mkpinentry.sh | 21 + tests/lib/names.sh | 10 + tests/lib/nested.sh | 189 + tests/lib/network.sh | 34 + tests/lib/os-release.16 | 7 + tests/lib/pinentry-fake.sh | 20 + tests/lib/pkgdb.sh | 791 + tests/lib/prepare-restore.sh | 556 + tests/lib/prepare.sh | 638 + tests/lib/quiet.sh | 30 + tests/lib/ramdisk.sh | 8 + tests/lib/random.sh | 39 + tests/lib/reset.sh | 171 + tests/lib/snaps.sh | 92 + .../bin/chpasswd | 3 + .../bin/deluser | 3 + .../bin/useradd | 3 + .../meta/snap.yaml | 16 + .../account-control-consumer/bin/chpasswd | 3 + .../account-control-consumer/bin/deluser | 3 + .../account-control-consumer/bin/useradd | 3 + .../account-control-consumer/meta/snap.yaml | 15 + tests/lib/snaps/aliases/bin/cmd1 | 2 + tests/lib/snaps/aliases/bin/cmd2 | 2 + tests/lib/snaps/aliases/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/aliases/meta/snap.yaml | 9 + tests/lib/snaps/basic-desktop/bin/echo | 3 + .../snaps/basic-desktop/meta/gui/echo.desktop | 10 + .../lib/snaps/basic-desktop/meta/gui/icon.png | Bin 0 -> 3371 bytes .../meta/gui/io.snapcraft.echoecho.desktop | 10 + tests/lib/snaps/basic-desktop/meta/snap.yaml | 8 + .../snaps/basic-hooks/meta/hooks/configure | 7 + .../snaps/basic-hooks/meta/hooks/invalid-hook | 3 + tests/lib/snaps/basic-hooks/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/basic-hooks/meta/snap.yaml | 11 + .../meta/hooks/configure | 3 + .../meta/hooks/connect-plug-consumer | 68 + .../meta/hooks/disconnect-plug-consumer | 61 + .../meta/hooks/prepare-plug-consumer | 41 + .../meta/hooks/unprepare-plug-consumer | 5 + .../basic-iface-hooks-consumer/meta/icon.png | Bin 0 -> 3371 bytes .../basic-iface-hooks-consumer/meta/snap.yaml | 7 + .../meta/hooks/configure | 1 + .../meta/hooks/connect-slot-producer | 52 + .../meta/hooks/disconnect-slot-producer | 52 + .../meta/hooks/prepare-slot-producer | 35 + .../meta/hooks/unprepare-slot-producer | 3 + .../basic-iface-hooks-producer/meta/icon.png | Bin 0 -> 3371 bytes .../basic-iface-hooks-producer/meta/snap.yaml | 8 + tests/lib/snaps/basic-run/bin/echo | 3 + tests/lib/snaps/basic-run/meta/snap.yaml | 6 + tests/lib/snaps/basic/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/basic/meta/snap.yaml | 4 + .../snaps/browser-support-consumer/bin/cmd | 2 + .../meta/snap.yaml.in | 10 + .../lib/snaps/classic-gadget/meta/gadget.yaml | 1 + .../classic-gadget/meta/hooks/prepare-device | 2 + tests/lib/snaps/classic-gadget/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/classic-gadget/meta/snap.yaml | 4 + tests/lib/snaps/command-chain/chain1 | 6 + tests/lib/snaps/command-chain/chain2 | 6 + tests/lib/snaps/command-chain/hello | 3 + .../snaps/command-chain/meta/hooks/configure | 4 + tests/lib/snaps/command-chain/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/command-chain/meta/snap.yaml | 13 + tests/lib/snaps/config-versions-v2/bin/sh | 3 + .../config-versions-v2/meta/hooks/configure | 2 + .../snaps/config-versions-v2/meta/icon.png | Bin 0 -> 3371 bytes .../snaps/config-versions-v2/meta/snap.yaml | 7 + tests/lib/snaps/config-versions/bin/sh | 3 + .../config-versions/meta/hooks/configure | 2 + tests/lib/snaps/config-versions/meta/icon.png | Bin 0 -> 3371 bytes .../lib/snaps/config-versions/meta/snap.yaml | 7 + tests/lib/snaps/data-writer/bin/write-data | 20 + tests/lib/snaps/data-writer/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/data-writer/meta/snap.yaml | 9 + .../failing-config-hooks/meta/hooks/configure | 4 + .../snaps/failing-config-hooks/meta/icon.png | Bin 0 -> 3371 bytes .../snaps/failing-config-hooks/meta/snap.yaml | 2 + .../firewall-control-consumer/bin/consumer | 3 + .../firewall-control-consumer/meta/snap.yaml | 12 + tests/lib/snaps/generic-consumer/bin/cmd | 5 + .../snaps/generic-consumer/meta/snap.yaml.in | 6 + tests/lib/snaps/gpio-consumer/bin/read | 3 + tests/lib/snaps/gpio-consumer/meta/snap.yaml | 9 + .../hardware-observe-consumer/bin/consumer | 5 + .../hardware-observe-consumer/meta/snap.yaml | 9 + tests/lib/snaps/home-consumer/bin/reader | 14 + tests/lib/snaps/home-consumer/bin/writer | 15 + tests/lib/snaps/home-consumer/meta/snap.yaml | 12 + tests/lib/snaps/iio-consumer/bin/read | 2 + tests/lib/snaps/iio-consumer/bin/write | 2 + tests/lib/snaps/iio-consumer/meta/snap.yaml | 12 + .../lib/snaps/locale-control-consumer/bin/get | 14 + .../lib/snaps/locale-control-consumer/bin/set | 23 + .../locale-control-consumer/meta/snap.yaml | 12 + tests/lib/snaps/log-observe-consumer/bin/cmd | 6 + .../snaps/log-observe-consumer/bin/consumer | 15 + .../snaps/log-observe-consumer/meta/snap.yaml | 12 + .../snaps/modem-manager-consumer/bin/consumer | 3 + .../modem-manager-consumer/meta/snap.yaml | 9 + .../snaps/mount-observe-consumer/bin/consumer | 12 + .../mount-observe-consumer/meta/snap.yaml | 9 + .../snaps/network-bind-consumer/bin/consumer | 22 + .../network-bind-consumer/meta/snap.yaml | 10 + tests/lib/snaps/network-consumer/bin/consumer | 21 + .../lib/snaps/network-consumer/meta/snap.yaml | 9 + .../snaps/network-control-consumer/bin/cmd | 6 + .../network-control-consumer/meta/snap.yaml | 9 + .../network-observe-consumer/bin/consumer | 10 + .../network-observe-consumer/meta/snap.yaml | 9 + .../snaps/process-control-consumer/bin/signal | 6 + .../process-control-consumer/meta/snap.yaml | 9 + .../bin/consumer | 5 + .../meta/snap.yaml | 9 + .../snap-hooks-bad-install/meta/hooks/install | 9 + .../snap-hooks-bad-install/meta/snap.yaml | 5 + tests/lib/snaps/snap-hooks-bad-install/true | 0 .../lib/snaps/snap-hooks/meta/hooks/configure | 3 + tests/lib/snaps/snap-hooks/meta/hooks/install | 3 + .../snaps/snap-hooks/meta/hooks/post-refresh | 3 + .../snaps/snap-hooks/meta/hooks/pre-refresh | 3 + tests/lib/snaps/snap-hooks/meta/hooks/remove | 5 + tests/lib/snaps/snap-hooks/meta/snap.yaml | 8 + tests/lib/snaps/snap-hooks/true | 0 .../meta/hooks/install | 4 + .../snap-install-hook-broken/meta/snap.yaml | 5 + tests/lib/snaps/snap-store/bin/snap-store | 2 + tests/lib/snaps/snap-store/meta/snap.yaml | 8 + .../snapctl-from-snap-core18/bin/snapctl-get | 2 + .../snapctl-from-snap-core18/bin/snapctl-set | 2 + .../meta/hooks/configure | 3 + .../snapctl-from-snap-core18/meta/snap.yaml | 8 + .../snaps/snapctl-from-snap/bin/snapctl-get | 2 + .../snaps/snapctl-from-snap/bin/snapctl-set | 2 + .../snapctl-from-snap/meta/hooks/configure | 3 + .../snaps/snapctl-from-snap/meta/snap.yaml | 7 + .../snapctl-hooks-v2/meta/hooks/configure | 13 + .../lib/snaps/snapctl-hooks-v2/meta/icon.png | Bin 0 -> 3371 bytes .../lib/snaps/snapctl-hooks-v2/meta/snap.yaml | 2 + .../snaps/snapctl-hooks/meta/hooks/configure | 111 + tests/lib/snaps/snapctl-hooks/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/snapctl-hooks/meta/snap.yaml | 2 + tests/lib/snaps/socket-activation/bin/sleep | 3 + .../snaps/socket-activation/meta/snap.yaml | 12 + .../lib/snaps/test-classic-cgroup/bin/read-fb | 3 + .../snaps/test-classic-cgroup/bin/read-kmsg | 3 + .../snaps/test-classic-cgroup/meta/snap.yaml | 12 + .../snaps/test-devmode-cgroup/bin/read-dev | 4 + .../snaps/test-devmode-cgroup/meta/snap.yaml | 11 + .../list-accounts.c | 69 + .../snapcraft.yaml | 25 + tests/lib/snaps/test-snapd-adb-support/bin/sh | 2 + .../test-snapd-adb-support/meta/snap.yaml | 7 + .../test-snapd-after-before-service/bin/start | 11 + .../meta/snap.yaml | 19 + .../lib/snaps/test-snapd-appstreamid/bin/run | 3 + .../test-snapd-appstreamid/meta/snap.yaml | 14 + .../test-snapd-auto-aliases/bin/wellknown1 | 2 + .../test-snapd-auto-aliases/bin/wellknown2 | 2 + .../test-snapd-auto-aliases/meta/icon.png | Bin 0 -> 3371 bytes .../test-snapd-auto-aliases/meta/snap.yaml | 9 + .../test-snapd-autopilot-consumer/consumer | 22 + .../test-snapd-autopilot-consumer/provider.py | 30 + .../snapcraft.yaml | 28 + .../test-snapd-autopilot-consumer/wrapper | 3 + tests/lib/snaps/test-snapd-base-bare/Makefile | 13 + .../snaps/test-snapd-base-bare/snapcraft.yaml | 12 + .../lib/snaps/test-snapd-base/meta/snap.yaml | 4 + tests/lib/snaps/test-snapd-base/random-file | 1 + .../test-snapd-busybox-static/snapcraft.yaml | 17 + .../bin/classic-confinement | 4 + .../bin/recurse | 6 + .../test-snapd-classic-confinement/bin/sh | 3 + .../meta/icon.png | Bin 0 -> 3371 bytes .../meta/snap.yaml | 10 + .../bin/test-snapd-complexion | 7 + .../test-snapd-complexion/meta/snap.yaml | 11 + .../test-snapd-complexion.bash-completer | 31 + .../test-snapd-content-advanced-plug/bin/sh | 3 + .../meta/snap.yaml | 22 + .../test-snapd-content-advanced-slot/bin/sh | 3 + .../meta/snap.yaml | 16 + .../source/canary | 1 + .../bin/content-plug | 6 + .../import/.placeholder | 0 .../meta/snap.yaml | 18 + .../bin/content-plug | 6 + .../import/.placeholder | 0 .../meta/snap.yaml | 18 + .../test-snapd-content-mimic-plug/bin/sh | 3 + .../dir/stuff-in-dir | 0 .../snaps/test-snapd-content-mimic-plug/file | 1 + .../meta/snap.yaml | 17 + .../test-snapd-content-mimic-plug/symlink | 1 + .../symlink-target | 0 .../test-snapd-content-mimic-slot/bin/sh | 3 + .../meta/snap.yaml | 11 + .../source/canary | 1 + .../bin/content-plug | 11 + .../import/.placeholder | 0 .../meta/snap.yaml | 11 + .../test-snapd-content-plug/bin/content-plug | 11 + .../import/.placeholder | 0 .../test-snapd-content-plug/meta/snap.yaml | 12 + .../meta/snap.yaml | 7 + .../shared-content | 3 + .../test-snapd-content-slot/meta/snap.yaml | 8 + .../test-snapd-content-slot/shared-content | 3 + .../test-snapd-content-slot2/meta/snap.yaml | 8 + .../test-snapd-content-slot2/shared-content | 3 + .../test-snapd-control-consumer/bin/install | 22 + .../test-snapd-control-consumer/bin/list | 19 + .../meta/snap.yaml | 26 + .../snapcraft.yaml | 13 + .../snaps/test-snapd-daemon-notify/bin/notify | 3 + .../test-snapd-daemon-notify/meta/snap.yaml | 10 + .../test-snapd-dbus-consumer/consumer.py | 10 + .../test-snapd-dbus-consumer/snapcraft.yaml | 25 + .../test-snapd-dbus-provider/provider.py | 24 + .../test-snapd-dbus-provider/snapcraft.yaml | 23 + .../snaps/test-snapd-dbus-provider/wrapper | 3 + .../snaps/test-snapd-desktop/bin/check-dirs | 5 + .../snaps/test-snapd-desktop/bin/check-files | 5 + .../snaps/test-snapd-desktop/meta/snap.yaml | 12 + .../snaps/test-snapd-devmode/meta/snap.yaml | 8 + tests/lib/snaps/test-snapd-devmode/true | 0 tests/lib/snaps/test-snapd-devpts/bin/openpty | 18 + tests/lib/snaps/test-snapd-devpts/bin/useptmx | 20 + .../snaps/test-snapd-devpts/meta/snap.yaml | 9 + tests/lib/snaps/test-snapd-eds/calendar.c | 201 + tests/lib/snaps/test-snapd-eds/contacts.c | 222 + tests/lib/snaps/test-snapd-eds/meson.build | 14 + .../snaps/test-snapd-eds/snap/snapcraft.yaml | 47 + .../snaps/test-snapd-epoch-1/meta/snap.yaml | 5 + .../snaps/test-snapd-epoch-2/meta/snap.yaml | 5 + .../test-snapd-event/bin/read-evdev-device | 29 + .../lib/snaps/test-snapd-event/meta/snap.yaml | 12 + .../lib/snaps/test-snapd-framebuffer/bin/read | 3 + .../snaps/test-snapd-framebuffer/bin/write | 3 + .../test-snapd-framebuffer/meta/snap.yaml | 12 + .../snaps/test-snapd-fuse-consumer/Makefile | 9 + .../test-snapd-fuse-consumer/snapcraft.yaml | 18 + .../lib/snaps/test-snapd-go-webserver/main.go | 25 + .../test-snapd-go-webserver/snapcraft.yaml | 19 + .../test-snapd-gpio-memory-control/Makefile | 5 + .../test-snapd-gpio-memory-control/gpiomem.c | 59 + .../snapcraft.yaml | 17 + .../bin/check | 5 + .../meta/snap.yaml | 10 + .../bin/check | 5 + .../meta/snap.yaml | 10 + .../snaps/test-snapd-hello-classic/Makefile | 9 + .../test-snapd-hello-classic/snapcraft.yaml | 16 + .../test-snapd-hello-classic.c | 12 + .../lib/snaps/test-snapd-just-beta/snap-name | 3 + .../snaps/test-snapd-just-beta/snapcraft.yaml | 15 + .../lib/snaps/test-snapd-just-edge/snap-name | 3 + .../snaps/test-snapd-just-edge/snapcraft.yaml | 15 + .../snapcraft.yaml | 24 + tests/lib/snaps/test-snapd-layout/bin/sh | 3 + .../snaps/test-snapd-layout/meta/snap.yaml | 39 + .../lib/snaps/test-snapd-layout/opt/demo/file | 1 + .../test-snapd-layout/usr/share/demo/file | 1 + .../bin/machine-down | 3 + .../bin/machine-up | 3 + .../snapcraft.yaml | 49 + .../vm/ping-unikernel.xml | 22 + .../consumer | 22 + .../provider.py | 29 + .../snapcraft.yaml | 26 + .../wrapper | 3 + tests/lib/snaps/test-snapd-lp-1803535/bin/sh | 2 + .../etc/OpenCL/vendors/foo.icd | 1 + .../test-snapd-lp-1803535/meta/snap.yaml | 9 + .../snaps/test-snapd-multi-service/bin/start | 6 + .../test-snapd-multi-service/meta/snap.yaml | 9 + .../snaps/test-snapd-netlink-audit/bin/bind | 15 + .../test-snapd-netlink-audit/meta/snap.yaml | 9 + .../test-snapd-netlink-connector/bin/bind | 15 + .../meta/snap.yaml | 9 + .../consumer | 31 + .../provider.py | 33 + .../snapcraft.yaml | 26 + .../wrapper | 3 + .../test-snapd-number-version/meta/snap.yaml | 3 + .../bin/ovs-vsctl | 3 + .../snapcraft.yaml | 21 + .../random-uuid | 3 + .../snapcraft.yaml | 17 + .../bin/secret-tool | 3 + .../snapcraft.yaml | 21 + .../bin/head-mem | 3 + .../meta/snap.yaml | 9 + .../test-snapd-policy-app-consumer/bin/run | 10 + .../meta/gui/test-desktop.desktop | 6 + .../meta/snap.yaml | 394 + .../bin/run | 10 + .../meta/snap.yaml | 99 + .../bin/run | 10 + .../meta/snap.yaml | 139 + .../snaps/test-snapd-private/meta/snap.yaml | 6 + .../snaps/test-snapd-public/meta/snap.yaml | 6 + .../test-snapd-python-webserver/index.html | 43 + .../test-snapd-python-webserver/server.py | 46 + .../snapcraft.yaml | 21 + .../meta/snap.yaml | 4 + .../test-snapd-requires-base/meta/snap.yaml | 4 + .../lib/snaps/test-snapd-rsync/snapcraft.yaml | 16 + .../test-snapd-service-try-v1/bin/service | 3 + .../test-snapd-service-try-v1/meta/snap.yaml | 5 + .../test-snapd-service-try-v2/bin/service | 3 + .../test-snapd-service-try-v2/meta/snap.yaml | 6 + .../snaps/test-snapd-service-v1-good/bin/good | 3 + .../test-snapd-service-v1-good/meta/snap.yaml | 7 + .../snaps/test-snapd-service-v2-bad/bin/bad | 4 + .../test-snapd-service-v2-bad/meta/snap.yaml | 7 + .../test-snapd-service-watchdog/bin/direct | 58 + .../meta/snap.yaml | 15 + tests/lib/snaps/test-snapd-service/bin/reload | 8 + tests/lib/snaps/test-snapd-service/bin/start | 9 + .../snaps/test-snapd-service/bin/start-other | 6 + .../test-snapd-service/bin/start-stop-mode | 68 + .../bin/start-stop-mode-sigterm | 15 + tests/lib/snaps/test-snapd-service/bin/stop | 7 + .../test-snapd-service/bin/stop-stop-mode | 4 + .../test-snapd-service/meta/hooks/configure | 19 + .../snaps/test-snapd-service/meta/snap.yaml | 59 + tests/lib/snaps/test-snapd-sh-core16/bin/sh | 4 + .../snaps/test-snapd-sh-core16/meta/snap.yaml | 7 + tests/lib/snaps/test-snapd-sh/bin/sh | 3 + tests/lib/snaps/test-snapd-sh/meta/snap.yaml | 69 + .../test-snapd-simple-service/bin/service | 2 + .../test-snapd-simple-service/meta/snap.yaml | 8 + .../test-snapd-snapctl-core18/bin/service | 5 + .../meta/hooks/install | 3 + .../test-snapd-snapctl-core18/meta/snap.yaml | 10 + tests/lib/snaps/test-snapd-statx/bin/statx.py | 95 + .../lib/snaps/test-snapd-statx/meta/snap.yaml | 7 + .../bin/forking.sh | 3 + .../bin/simple.sh | 3 + .../meta/hooks/install | 6 + .../meta/snap.yaml | 9 + .../consumer.py | 11 + .../dbus-introspect.py | 10 + .../snapcraft.yaml | 25 + .../bin/hwclock | 2 + .../bin/timedatectl | 2 + .../meta/snap.yaml | 18 + .../snaps/test-snapd-timer-service/bin/loop | 10 + .../test-snapd-timer-service/meta/snap.yaml | 13 + .../snaps/test-snapd-tools-core18/bin/block | 14 + .../lib/snaps/test-snapd-tools-core18/bin/cat | 3 + .../lib/snaps/test-snapd-tools-core18/bin/cmd | 6 + .../snaps/test-snapd-tools-core18/bin/echo | 3 + .../lib/snaps/test-snapd-tools-core18/bin/env | 3 + .../snaps/test-snapd-tools-core18/bin/fail | 3 + .../snaps/test-snapd-tools-core18/bin/head | 3 + .../lib/snaps/test-snapd-tools-core18/bin/sh | 13 + .../snaps/test-snapd-tools-core18/bin/success | 3 + .../test-snapd-tools-core18/meta/snap.yaml | 30 + tests/lib/snaps/test-snapd-tools/bin/block | 14 + tests/lib/snaps/test-snapd-tools/bin/cat | 3 + tests/lib/snaps/test-snapd-tools/bin/cmd | 6 + tests/lib/snaps/test-snapd-tools/bin/echo | 3 + tests/lib/snaps/test-snapd-tools/bin/env | 3 + tests/lib/snaps/test-snapd-tools/bin/fail | 3 + tests/lib/snaps/test-snapd-tools/bin/head | 3 + tests/lib/snaps/test-snapd-tools/bin/sh | 13 + tests/lib/snaps/test-snapd-tools/bin/success | 3 + .../lib/snaps/test-snapd-tools/meta/icon.png | Bin 0 -> 3371 bytes .../lib/snaps/test-snapd-tools/meta/snap.yaml | 29 + .../lib/snaps/test-snapd-tuntap/bin/tuntap.py | 79 + .../snaps/test-snapd-tuntap/meta/snap.yaml | 10 + .../bin/read-evdev-kbd | 24 + .../meta/snap.yaml | 24 + .../snaps/test-snapd-udisks2/snapcraft.yaml | 17 + tests/lib/snaps/test-snapd-udisks2/udisksctl | 3 + tests/lib/snaps/test-snapd-uhid/Makefile | 5 + .../lib/snaps/test-snapd-uhid/snapcraft.yaml | 17 + tests/lib/snaps/test-snapd-uhid/uhid-test.c | 190 + .../test-snapd-unknown-interfaces/bin/sh | 3 + .../meta/snap.yaml | 10 + .../snapcraft.yaml | 14 + .../bin/bar | 0 .../bin/foo | 0 .../comp.sh | 0 .../hell/bar | 1 + .../hell/bar -> baz | 1 + .../hell/bar -> baz -> qux | 1 + .../hell/bar -> qux | 1 + .../hell/baz | 1 + .../hell/baz -> qux | 1 + .../hell/foo | 1 + .../hell/foo -> bar | 1 + .../hell/foo -> bar -> baz | 1 + .../hell/foo -> bar -> qux | 1 + .../hell/foo -> baz | 1 + .../hell/foo -> baz -> qux | 1 + .../hell/foo -> qux | 1 + .../hell/qux | 1 + .../meta/hooks/what | 0 .../meta/snap.yaml | 11 + .../meta/unreadable | 0 .../meta/hooks/configure | 3 + .../meta/snap.yaml | 5 + .../meta/hooks/configure | 6 + .../meta/snap.yaml | 8 + .../meta/hooks/configure | 2 + .../test-snapd-with-configure/meta/snap.yaml | 4 + .../snaps/test-snapd-xdg-autostart/bin/foobar | 26 + .../test-snapd-xdg-autostart/meta/snap.yaml | 6 + .../snaps/test-snapd-xdg-settings/bin/browser | 3 + .../bin/set-default-web-browser | 5 + .../meta/gui/browser.desktop | 6 + .../test-snapd-xdg-settings/meta/snap.yaml | 9 + .../lib/snaps/test-strict-cgroup/bin/read-dev | 4 + .../snaps/test-strict-cgroup/meta/snap.yaml | 11 + tests/lib/spread-funcs.sh | 14 + tests/lib/state.sh | 122 + tests/lib/store.sh | 100 + tests/lib/strings.sh | 5 + tests/lib/successful_login.exp | 13 + tests/lib/systemd-escape/main.go | 52 + tests/lib/systemd.sh | 87 + tests/lib/systems.sh | 29 + tests/lib/tinyproxy/tinyproxy.py | 135 + tests/main/abort/task.yaml | 40 + tests/main/ack/alice.account | 19 + tests/main/ack/alice.account-key | 31 + tests/main/ack/bob.assertions | 51 + tests/main/ack/task.yaml | 33 + tests/main/alias/task.yaml | 57 + tests/main/appstream-id/task.yaml | 28 + tests/main/apt-hooks/task.yaml | 55 + tests/main/auth-errors/task.yaml | 27 + tests/main/auto-aliases/task.yaml | 39 + .../auto-refresh-private/expired_macaroons.sh | 13 + .../auto-refresh-private/successful_login.exp | 13 + tests/main/auto-refresh-private/task.yaml | 111 + tests/main/auto-refresh/task.yaml | 80 + tests/main/base-snaps-refresh/task.yaml | 23 + tests/main/base-snaps/task.yaml | 44 + tests/main/canonical-livepatch/task.yaml | 28 + tests/main/catalog-update/task.yaml | 33 + tests/main/cgroup-freezer/task.yaml | 49 + tests/main/change-errors/task.yaml | 10 + tests/main/chattr/task.yaml | 23 + tests/main/chattr/toggle.go | 51 + .../task.yaml | 39 + tests/main/classic-confinement/task.yaml | 65 + .../main/classic-custom-device-reg/task.yaml | 83 + tests/main/classic-firstboot/task.yaml | 80 + .../task.yaml | 76 + .../task.yaml | 78 + .../classic-ubuntu-core-transition/task.yaml | 131 + tests/main/cloud-init/task.yaml | 74 + tests/main/cmdline/task.yaml | 12 + tests/main/command-chain/task.yaml | 32 + tests/main/completion/abort.exp | 7 + tests/main/completion/ack.exp | 8 + tests/main/completion/alias.exp | 13 + tests/main/completion/buy.exp | 8 + tests/main/completion/change.exp | 7 + tests/main/completion/delete-key.exp | 6 + tests/main/completion/disable.exp | 6 + tests/main/completion/download.exp | 7 + tests/main/completion/enable.exp | 6 + tests/main/completion/export-key.exp | 6 + tests/main/completion/get.exp | 6 + tests/main/completion/info.exp | 11 + tests/main/completion/install.exp | 22 + tests/main/completion/key.exp0 | 17 + tests/main/completion/lib.exp0 | 1 + tests/main/completion/list.exp | 7 + tests/main/completion/refresh.exp | 6 + tests/main/completion/remove.exp | 6 + tests/main/completion/revert.exp | 6 + tests/main/completion/set.exp | 6 + tests/main/completion/sign-build.exp | 6 + tests/main/completion/sign.exp | 6 + tests/main/completion/task.yaml | 55 + tests/main/completion/toplevel.exp | 11 + tests/main/completion/try.exp | 6 + tests/main/completion/watch.exp | 7 + tests/main/config-versions/task.yaml | 59 + .../task.yaml | 23 + tests/main/confinement-classic/task.yaml | 48 + tests/main/core-snap-not-test-test/task.yaml | 7 + .../main/core-snap-refresh-on-core/task.yaml | 123 + tests/main/core-snap-refresh/task.yaml | 53 + tests/main/core-watchdog/task.yaml | 31 + tests/main/core16-base/task.yaml | 11 + tests/main/core18-configure-hook/task.yaml | 32 + tests/main/core18-with-hooks/task.yaml | 12 + tests/main/create-key/passphrase_mismatch.exp | 17 + tests/main/create-key/successful_default.exp | 48 + .../create-key/successful_non_default.exp | 48 + tests/main/create-key/task.yaml | 30 + tests/main/create-user/task.yaml | 41 + tests/main/debs-have-built-using/task.yaml | 12 + tests/main/debug-confinement/task.yaml | 12 + tests/main/debug-paths/task.yaml | 13 + tests/main/debug-sandbox/task.yaml | 25 + tests/main/degraded/task.yaml | 31 + .../main/dirs-not-shared-with-host/task.yaml | 35 + tests/main/disable-autoconnect/task.yaml | 48 + .../fake-document-portal.py | 56 + .../main/document-portal-activation/task.yaml | 99 + tests/main/econnreset/task.yaml | 67 + .../main/enable-disable-units-gpio/task.yaml | 76 + tests/main/enable-disable/task.yaml | 52 + tests/main/experimental-features/task.yaml | 17 + tests/main/failover/task.yaml | 137 + tests/main/fakestore-install/task.yaml | 38 + tests/main/fedora-base-smoke/task.yaml | 17 + tests/main/find-private/task.yaml | 51 + tests/main/generic-classic-reg/task.yaml | 28 + tests/main/help/task.yaml | 24 + tests/main/high-user-handling/task.yaml | 12 + tests/main/high-user-handling/test.go | 16 + tests/main/i18n/task.yaml | 25 + tests/main/install-cache/task.yaml | 10 + tests/main/install-closed-channel/task.yaml | 9 + tests/main/install-errors/task.yaml | 90 + tests/main/install-refresh-private/task.yaml | 52 + .../install-refresh-remove-hooks/task.yaml | 97 + tests/main/install-remove-multi/task.yaml | 21 + tests/main/install-sideload-epochs/task.yaml | 30 + tests/main/install-sideload/task.yaml | 85 + tests/main/install-snaps/task.yaml | 134 + .../main/install-socket-activation/task.yaml | 17 + tests/main/install-store-laaaarge/task.yaml | 23 + tests/main/install-store/task.yaml | 47 + tests/main/install/task.yaml | 20 + .../main/interfaces-account-control/task.yaml | 36 + .../interfaces-accounts-service/task.yaml | 80 + tests/main/interfaces-adb-support/task.yaml | 23 + tests/main/interfaces-alsa/task.yaml | 95 + .../task.yaml | 83 + tests/main/interfaces-avahi-observe/task.yaml | 46 + .../interfaces-bluetooth-control/task.yaml | 60 + tests/main/interfaces-bluez/task.yaml | 21 + .../task.yaml | 86 + .../main/interfaces-browser-support/task.yaml | 167 + .../interfaces-calendar-service/task.yaml | 78 + tests/main/interfaces-cli/task.yaml | 30 + .../interfaces-contacts-service/task.yaml | 101 + .../interfaces-content-circular/task.yaml | 18 + .../task.yaml | 23 + .../task.yaml | 34 + tests/main/interfaces-content-mimic/task.yaml | 63 + .../task.yaml | 97 + tests/main/interfaces-content/task.yaml | 60 + tests/main/interfaces-cups-control/task.yaml | 75 + tests/main/interfaces-daemon-notify/task.yaml | 62 + tests/main/interfaces-dbus/task.yaml | 64 + .../task.yaml | 69 + .../interfaces-desktop-host-fonts/task.yaml | 62 + tests/main/interfaces-desktop/task.yaml | 51 + .../main/interfaces-device-buttons/task.yaml | 58 + tests/main/interfaces-dvb/task.yaml | 52 + .../interfaces-firewall-control/task.yaml | 86 + tests/main/interfaces-framebuffer/task.yaml | 49 + tests/main/interfaces-fuse_support/task.yaml | 98 + tests/main/interfaces-gpg-keys/task.yaml | 74 + .../main/interfaces-gpg-public-keys/task.yaml | 71 + .../interfaces-gpio-memory-control/task.yaml | 43 + .../interfaces-hardware-observe/task.yaml | 44 + .../task.yaml | 56 + .../task.yaml | 56 + tests/main/interfaces-home/task.yaml | 125 + .../interfaces-hooks-misbehaving/task.yaml | 11 + tests/main/interfaces-hooks/task.yaml | 102 + .../interfaces-hostname-control/task.yaml | 67 + tests/main/interfaces-iio/task.yaml | 50 + tests/main/interfaces-joystick/task.yaml | 68 + .../interfaces-juju-client-observe/task.yaml | 52 + .../task.yaml | 123 + tests/main/interfaces-kvm/task.yaml | 47 + tests/main/interfaces-libvirt/task.yaml | 69 + .../main/interfaces-locale-control/task.yaml | 98 + .../interfaces-location-control/task.yaml | 67 + tests/main/interfaces-log-observe/task.yaml | 49 + tests/main/interfaces-many/task.yaml | 115 + tests/main/interfaces-mount-observe/task.yaml | 56 + tests/main/interfaces-netlink-audit/task.yaml | 39 + .../interfaces-netlink-connector/task.yaml | 36 + tests/main/interfaces-network-bind/task.yaml | 60 + .../task.yaml | 43 + .../task.yaml | 43 + .../main/interfaces-network-control/task.yaml | 147 + .../main/interfaces-network-manager/task.yaml | 53 + .../main/interfaces-network-observe/task.yaml | 61 + .../task.yaml | 53 + .../task.yaml | 50 + .../main/interfaces-network-status/task.yaml | 65 + tests/main/interfaces-network/task.yaml | 65 + tests/main/interfaces-opengl-nvidia/task.yaml | 127 + .../interfaces-openvswitch-support/task.yaml | 46 + tests/main/interfaces-openvswitch/task.yaml | 113 + .../task.yaml | 47 + .../main/interfaces-personal-files/task.yaml | 98 + .../task.yaml | 51 + .../main/interfaces-process-control/task.yaml | 60 + tests/main/interfaces-raw-usb/task.yaml | 52 + .../main/interfaces-removable-media/task.yaml | 88 + .../task.yaml | 40 + .../task.yaml | 137 + tests/main/interfaces-snapd-control/task.yaml | 46 + tests/main/interfaces-ssh-keys/task.yaml | 58 + .../main/interfaces-ssh-public-keys/task.yaml | 64 + tests/main/interfaces-system-files/task.yaml | 94 + .../main/interfaces-system-observe/task.yaml | 68 + tests/main/interfaces-time-control/task.yaml | 66 + .../interfaces-timeserver-control/task.yaml | 75 + .../interfaces-timezone-control/task.yaml | 65 + tests/main/interfaces-udev/task.yaml | 30 + tests/main/interfaces-udisks2/task.yaml | 59 + tests/main/interfaces-uhid/task.yaml | 45 + .../main/interfaces-upower-observe/task.yaml | 60 + tests/main/interfaces-wayland/task.yaml | 60 + .../kernel-snap-refresh-on-core/task.yaml | 117 + tests/main/known-remote/task.yaml | 9 + tests/main/known/task.yaml | 16 + .../layout-symlink-bind-revert/app.v1/bin/app | 2 + .../app.v1/meta/snap.yaml | 18 + .../app.v1/runtime/.keep | 0 .../layout-symlink-bind-revert/app.v2/bin/app | 2 + .../app.v2/meta/snap.yaml | 18 + .../app.v2/runtime/.keep | 0 .../runtime/meta/snap.yaml | 7 + .../runtime/opt/runtime/runner | 2 + .../main/layout-symlink-bind-revert/task.yaml | 24 + tests/main/layout/task.yaml | 102 + tests/main/listing/task.yaml | 62 + tests/main/local-install-w-metadata/digest.go | 17 + tests/main/local-install-w-metadata/task.yaml | 26 + tests/main/login/missing_email_error.exp | 9 + tests/main/login/task.yaml | 33 + tests/main/login/unsuccessful_login.exp | 14 + tests/main/lxd/task.yaml | 119 + tests/main/manpages/task.yaml | 31 + tests/main/media-sharing/task.yaml | 33 + tests/main/mount-protocol-error/task.yaml | 28 + tests/main/network-retry/task.yaml | 42 + tests/main/nfs-support/task.yaml | 186 + tests/main/op-install-failed-undone/task.yaml | 56 + tests/main/op-remove-retry/task.yaml | 44 + tests/main/op-remove/task.yaml | 44 + tests/main/parallel-install-aliases/task.yaml | 81 + .../parallel-install-auto-aliases/task.yaml | 90 + tests/main/parallel-install-basic/task.yaml | 85 + .../task.yaml | 37 + .../parallel-install-common-dirs/task.yaml | 84 + tests/main/parallel-install-desktop/task.yaml | 44 + .../task.yaml | 77 + .../parallel-install-interfaces/task.yaml | 71 + tests/main/parallel-install-layout/task.yaml | 86 + tests/main/parallel-install-local/task.yaml | 45 + .../main/parallel-install-services/task.yaml | 63 + tests/main/parallel-install-store/task.yaml | 31 + tests/main/postrm-purge/task.yaml | 38 + tests/main/prefer/task.yaml | 36 + .../main/prepare-image-grub-core18/task.yaml | 50 + tests/main/prepare-image-grub/task.yaml | 87 + tests/main/prepare-image-uboot/task.yaml | 77 + tests/main/proxy-no-core/task.yaml | 49 + tests/main/proxy/task.yaml | 35 + tests/main/refresh-all-undo/task.yaml | 85 + tests/main/refresh-all/task.yaml | 71 + tests/main/refresh-amend/task.yaml | 28 + tests/main/refresh-delta-from-core/task.yaml | 30 + tests/main/refresh-delta/task.yaml | 29 + tests/main/refresh-devmode/task.yaml | 88 + tests/main/refresh-hold/task.yaml | 20 + tests/main/refresh-undo/task.yaml | 50 + tests/main/refresh/task.yaml | 145 + .../regression-home-snap-root-owned/task.yaml | 36 + tests/main/remove-errors/task.yaml | 20 + tests/main/retryable-error/task.yaml | 28 + tests/main/revert-devmode/task.yaml | 99 + tests/main/revert-sideload/task.yaml | 20 + tests/main/revert/task.yaml | 113 + tests/main/sanitycheck/task.yaml | 33 + tests/main/searching/task.yaml | 71 + tests/main/seccomp-statx/task.yaml | 16 + tests/main/security-apparmor/task.yaml | 25 + .../security-dev-input-event-denied/task.yaml | 108 + .../security-device-cgroups-classic/task.yaml | 39 + .../security-device-cgroups-devmode/task.yaml | 52 + .../task.yaml | 55 + .../task.yaml | 58 + .../security-device-cgroups-strict/task.yaml | 45 + tests/main/security-device-cgroups/task.yaml | 127 + tests/main/security-devpts/task.yaml | 31 + tests/main/security-private-tmp/task.yaml | 51 + .../main/security-private-tmp/tmp-create.exp | 15 + tests/main/security-profiles/task.yaml | 33 + tests/main/security-setuid-root/task.yaml | 44 + .../security-udev-input-subsystem/task.yaml | 87 + tests/main/selinux-snap-restorecon/task.yaml | 55 + tests/main/server-snap/task.yaml | 39 + tests/main/set-proxy-store/task.yaml | 80 + tests/main/snap-advise-command/task.yaml | 56 + .../snap-auto-import-asserts-spools/task.yaml | 55 + tests/main/snap-auto-import-asserts/task.yaml | 39 + tests/main/snap-auto-mount/task.yaml | 57 + tests/main/snap-confine-from-core/task.yaml | 69 + tests/main/snap-confine-privs/task.yaml | 72 + tests/main/snap-confine-privs/uids-and-gids.c | 40 + tests/main/snap-confine/task.yaml | 54 + tests/main/snap-connect/task.yaml | 66 + tests/main/snap-connectivity-check/task.yaml | 5 + tests/main/snap-core-fixup/task.yaml | 46 + tests/main/snap-core-fixup/test.img.xz | Bin 0 -> 203836 bytes tests/main/snap-core-symlinks/task.yaml | 12 + .../snap-debug-get-base-declaration/task.yaml | 12 + tests/main/snap-discard-ns/mount.py | 72 + tests/main/snap-discard-ns/mount.sh | 9 + tests/main/snap-discard-ns/task.yaml | 52 + tests/main/snap-disconnect/task.yaml | 45 + tests/main/snap-download/task.yaml | 37 + tests/main/snap-env/task.yaml | 85 + tests/main/snap-get/task.yaml | 106 + tests/main/snap-handle-link/task.yaml | 44 + tests/main/snap-info/check.py | 147 + tests/main/snap-info/task.yaml | 58 + .../snap-interface-network-core.yaml | 6 + .../snap-interface-network-snapd.yaml | 6 + tests/main/snap-interface/task.yaml | 18 + tests/main/snap-logs/task.yaml | 37 + tests/main/snap-mgmt/task.yaml | 90 + .../main/snap-multi-service-failing/task.yaml | 11 + tests/main/snap-network-errors/task.yaml | 32 + tests/main/snap-readme/task.yaml | 13 + tests/main/snap-remove-not-mounted/task.yaml | 16 + tests/main/snap-repair/task.yaml | 23 + tests/main/snap-run-alias/task.yaml | 39 + tests/main/snap-run-hook/task.yaml | 50 + tests/main/snap-run-symlink-error/task.yaml | 23 + tests/main/snap-run-symlink/task.yaml | 33 + .../main/snap-run-userdata-current/task.yaml | 42 + tests/main/snap-run/task.yaml | 69 + tests/main/snap-seccomp/task.yaml | 141 + .../task.yaml | 76 + .../main/snap-service-after-before/task.yaml | 23 + .../main/snap-service-refresh-mode/task.yaml | 39 + .../snap-service-stop-mode-sigkill/task.yaml | 46 + tests/main/snap-service-stop-mode/task.yaml | 70 + tests/main/snap-service-timer/task.yaml | 52 + tests/main/snap-service-watchdog/task.yaml | 59 + tests/main/snap-service/task.yaml | 29 + tests/main/snap-set-core-w-no-core/task.yaml | 24 + tests/main/snap-set/task.yaml | 58 + tests/main/snap-sign/create-key.exp | 17 + tests/main/snap-sign/sign-model.exp | 20 + tests/main/snap-sign/task.yaml | 49 + tests/main/snap-switch/task.yaml | 8 + tests/main/snap-system-env/task.yaml | 43 + tests/main/snap-system-key/task.yaml | 80 + tests/main/snap-update-ns/task.yaml | 87 + .../task.yaml | 36 + tests/main/snap-userd-reexec/task.yaml | 17 + tests/main/snap-wait/task.yaml | 27 + tests/main/snapctl-configure-core/task.yaml | 78 + tests/main/snapctl-from-snap/task.yaml | 103 + tests/main/snapctl-services/task.yaml | 89 + tests/main/snapctl/task.yaml | 48 + .../main/snapd-go-socket-activated/task.yaml | 39 + tests/main/snapd-notify/task.yaml | 37 + tests/main/snapd-reexec-snapd-snap/task.yaml | 26 + tests/main/snapd-reexec/task.yaml | 98 + tests/main/snapd-snap/task.yaml | 17 + tests/main/snapshot-basic/task.yaml | 47 + tests/main/snapshot-cross-revno/task.yaml | 59 + .../task.yaml | 45 + tests/main/stale-base-snap/task.yaml | 76 + tests/main/static/task.yaml | 7 + .../main/svcs-disable-install-hook/task.yaml | 11 + tests/main/system-core-alias/task.yaml | 20 + tests/main/systemd-service/task.yaml | 21 + tests/main/try-non-fatal/task.yaml | 19 + tests/main/try-snap-goes-away/task.yaml | 51 + tests/main/try-snap-is-optional/task.yaml | 12 + tests/main/try-twice-with-daemon/task.yaml | 36 + tests/main/try-with-hooks/task.yaml | 32 + tests/main/try/task.yaml | 85 + tests/main/ubuntu-core-apt/task.yaml | 11 + tests/main/ubuntu-core-classic/task.yaml | 54 + tests/main/ubuntu-core-create-user/task.yaml | 50 + .../manip_seed.py | 21 + .../prepare-device | 5 + .../task.yaml | 87 + .../manip_seed.py | 21 + .../prepare-device | 2 + .../ubuntu-core-custom-device-reg/task.yaml | 85 + tests/main/ubuntu-core-device-reg/task.yaml | 30 + tests/main/ubuntu-core-fan/task.yaml | 20 + .../manip_seed.py | 27 + .../task.yaml | 122 + tests/main/ubuntu-core-grub/task.yaml | 15 + .../main/ubuntu-core-network-config/task.yaml | 30 + tests/main/ubuntu-core-os-release/task.yaml | 7 + tests/main/ubuntu-core-reboot/task.yaml | 41 + tests/main/ubuntu-core-services/task.yaml | 15 + tests/main/ubuntu-core-uboot/task.yaml | 14 + tests/main/ubuntu-core-upgrade/task.yaml | 96 + .../main/ubuntu-core-writablepaths/task.yaml | 41 + tests/main/unhandled-task/task.yaml | 30 + tests/main/upgrade-from-2.15/task.yaml | 62 + tests/main/user-data-handling/task.yaml | 34 + tests/main/user-mounts/task.yaml | 53 + .../validate-container-failures/task.yaml | 32 + tests/main/whoami/task.yaml | 19 + tests/main/writable-areas/task.yaml | 37 + tests/main/xauth-migration/task.yaml | 88 + tests/main/xdg-open-compat/task.yaml | 108 + tests/main/xdg-open/task.yaml | 77 + tests/main/xdg-settings/task.yaml | 93 + tests/manual-tests.md | 236 + tests/nested/core-revert/task.yaml | 80 + tests/nested/extra-snaps-assertions/task.yaml | 31 + tests/nested/hot-plug/task.yaml | 63 + tests/nested/image-build/task.yaml | 27 + tests/nightly/docker/task.yaml | 38 + tests/nightly/unity/task.yaml | 37 + tests/regression/lp-1595444/task.yaml | 32 + tests/regression/lp-1597839/task.yaml | 18 + tests/regression/lp-1597842/task.yaml | 29 + tests/regression/lp-1599891/task.yaml | 11 + tests/regression/lp-1606277/task.yaml | 17 + tests/regression/lp-1607796/task.yaml | 16 + tests/regression/lp-1615113/task.yaml | 16 + tests/regression/lp-1618683/task.yaml | 31 + tests/regression/lp-1630479/task.yaml | 33 + tests/regression/lp-1641885/task.yaml | 33 + tests/regression/lp-1644439/task.yaml | 55 + tests/regression/lp-1665004/task.yaml | 19 + tests/regression/lp-1667385/task.yaml | 23 + tests/regression/lp-1693042/task.yaml | 18 + tests/regression/lp-1704860/snap-env-query.sh | 1 + tests/regression/lp-1704860/task.yaml | 29 + tests/regression/lp-1732555/task.yaml | 19 + tests/regression/lp-1764977/task.yaml | 30 + tests/regression/lp-1797556/task.yaml | 28 + tests/regression/lp-1800004/task.yaml | 11 + tests/regression/lp-1801955/task.yaml | 13 + tests/regression/lp-1802581/task.yaml | 81 + tests/regression/lp-1803535/task.yaml | 8 + tests/regression/lp-1803542/task.yaml | 53 + tests/regression/lp-1805485/task.yaml | 15 + tests/regression/lp-1805838/task.yaml | 26 + tests/regression/lp-1813963/task.yaml | 108 + tests/regression/lp-1815722/task.yaml | 13 + tests/regression/lp-1815869/hello.py | 1 + tests/regression/lp-1815869/task.yaml | 67 + tests/smoke/find-info/task.yaml | 9 + tests/smoke/install/task.yaml | 20 + tests/smoke/remove/task.yaml | 20 + tests/smoke/sandbox/task.yaml | 81 + tests/snapd-state.md | 5 + tests/unit/c-unit-tests-clang/task.yaml | 31 + tests/unit/c-unit-tests-gcc/task.yaml | 31 + tests/unit/go/task.yaml | 33 + tests/unit/spread-shellcheck/can-fail | 1 + tests/unit/spread-shellcheck/task.yaml | 15 + tests/upgrade/basic/task.yaml | 117 + tests/upgrade/snapd-xdg-open/task.yaml | 44 + tests/util/benchmark.sh | 21 + testutil/base.go | 51 + testutil/containschecker.go | 152 + testutil/containschecker_test.go | 223 + testutil/dbustest.go | 110 + testutil/exec.go | 154 + testutil/exec_test.go | 68 + testutil/export_test.go | 28 + testutil/filecontentchecker.go | 115 + testutil/filecontentchecker_test.go | 102 + testutil/filepresencechecker.go | 59 + testutil/filepresencechecker_test.go | 54 + testutil/intcheckers.go | 98 + testutil/intcheckers_test.go | 55 + testutil/lowlevel.go | 578 + testutil/lowlevel_test.go | 778 + testutil/paddedchecker.go | 147 + testutil/paddedchecker_test.go | 77 + testutil/syscallschecker.go | 89 + testutil/syscallschecker_test.go | 78 + testutil/testutil_test.go | 55 + timeout/timeout.go | 76 + timeout/timeout_test.go | 65 + timeutil/export_test.go | 34 + timeutil/human.go | 80 + timeutil/human_test.go | 93 + timeutil/schedule.go | 762 + timeutil/schedule_test.go | 1034 ++ update-pot | 74 + userd/autostart.go | 278 + userd/autostart_test.go | 325 + userd/export_test.go | 57 + userd/helpers.go | 109 + userd/helpers_test.go | 63 + userd/launcher.go | 187 + userd/launcher_test.go | 199 + userd/settings.go | 226 + userd/settings_test.go | 220 + userd/ui/kdialog.go | 68 + userd/ui/kdialog_test.go | 84 + userd/ui/ui.go | 94 + userd/ui/zenity.go | 64 + userd/ui/zenity_test.go | 102 + userd/userd.go | 116 + wrappers/binaries.go | 92 + wrappers/binaries_test.go | 165 + wrappers/core18.go | 208 + wrappers/core18_test.go | 158 + wrappers/desktop.go | 282 + wrappers/desktop_test.go | 512 + wrappers/export_test.go | 48 + wrappers/services.go | 725 + wrappers/services_gen_test.go | 703 + wrappers/services_test.go | 1053 ++ x11/xauth.go | 151 + x11/xauth_test.go | 74 + xdgopenproxy/export_test.go | 22 + xdgopenproxy/xdgopenproxy.go | 63 + xdgopenproxy/xdgopenproxy_test.go | 159 + 2477 files changed, 475679 insertions(+) create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 COPYING create mode 100644 HACKING.md create mode 100644 PULL_REQUEST_TEMPLATE.md create mode 100644 README.md create mode 100644 advisor/backend.go create mode 100644 advisor/cmdfinder.go create mode 100644 advisor/cmdfinder_test.go create mode 100644 advisor/export_test.go create mode 100644 advisor/finder.go create mode 100644 advisor/pkgfinder.go create mode 100644 advisor/pkgfinder_test.go create mode 100644 arch/arch.go create mode 100644 arch/arch_test.go create mode 100644 asserts/account.go create mode 100644 asserts/account_key.go create mode 100644 asserts/account_key_test.go create mode 100644 asserts/account_test.go create mode 100644 asserts/asserts.go create mode 100644 asserts/asserts_test.go create mode 100644 asserts/assertstest/assertstest.go create mode 100644 asserts/assertstest/assertstest_test.go create mode 100644 asserts/crypto.go create mode 100644 asserts/database.go create mode 100644 asserts/database_test.go create mode 100644 asserts/device_asserts.go create mode 100644 asserts/device_asserts_test.go create mode 100644 asserts/digest.go create mode 100644 asserts/digest_test.go create mode 100644 asserts/export_test.go create mode 100644 asserts/fetcher.go create mode 100644 asserts/fetcher_test.go create mode 100644 asserts/findwildcard.go create mode 100644 asserts/findwildcard_test.go create mode 100644 asserts/fsbackstore.go create mode 100644 asserts/fsbackstore_test.go create mode 100644 asserts/fsentryutils.go create mode 100644 asserts/fskeypairmgr.go create mode 100644 asserts/fskeypairmgr_test.go create mode 100644 asserts/gpgkeypairmgr.go create mode 100644 asserts/gpgkeypairmgr_test.go create mode 100644 asserts/header_checks.go create mode 100644 asserts/headers.go create mode 100644 asserts/headers_test.go create mode 100644 asserts/ifacedecls.go create mode 100644 asserts/ifacedecls_test.go create mode 100644 asserts/membackstore.go create mode 100644 asserts/membackstore_test.go create mode 100644 asserts/memkeypairmgr.go create mode 100644 asserts/memkeypairmgr_test.go create mode 100644 asserts/privkeys_for_test.go create mode 100644 asserts/repair.go create mode 100644 asserts/repair_test.go create mode 100644 asserts/signtool/sign.go create mode 100644 asserts/signtool/sign_test.go create mode 100644 asserts/snap_asserts.go create mode 100644 asserts/snap_asserts_test.go create mode 100644 asserts/snapasserts/snapasserts.go create mode 100644 asserts/snapasserts/snapasserts_test.go create mode 100644 asserts/store_asserts.go create mode 100644 asserts/store_asserts_test.go create mode 100644 asserts/sysdb/generic.go create mode 100644 asserts/sysdb/staging.go create mode 100644 asserts/sysdb/sysdb.go create mode 100644 asserts/sysdb/sysdb_test.go create mode 100644 asserts/sysdb/testkeys.go create mode 100644 asserts/sysdb/trusted.go create mode 100644 asserts/systestkeys/trusted.go create mode 100644 asserts/user.go create mode 100644 asserts/user_test.go create mode 100644 boot/boottest/mockbootloader.go create mode 100644 boot/kernel_os.go create mode 100644 boot/kernel_os_test.go create mode 100644 client/aliases.go create mode 100644 client/aliases_test.go create mode 100644 client/apps.go create mode 100644 client/apps_test.go create mode 100644 client/asserts.go create mode 100644 client/asserts_test.go create mode 100644 client/buy.go create mode 100644 client/change.go create mode 100644 client/change_test.go create mode 100644 client/client.go create mode 100644 client/client_test.go create mode 100644 client/conf.go create mode 100644 client/conf_test.go create mode 100644 client/export_test.go create mode 100644 client/icons.go create mode 100644 client/icons_test.go create mode 100644 client/interfaces.go create mode 100644 client/interfaces_test.go create mode 100644 client/login.go create mode 100644 client/login_test.go create mode 100644 client/packages.go create mode 100644 client/packages_test.go create mode 100644 client/snap_op.go create mode 100644 client/snap_op_test.go create mode 100644 client/snapctl.go create mode 100644 client/snapctl_test.go create mode 100644 client/snapshot.go create mode 100644 client/snapshot_test.go create mode 100644 client/warnings.go create mode 100644 client/warnings_test.go create mode 100644 cmd/.indent.pro create mode 100644 cmd/Makefile.am create mode 100644 cmd/appinfo.go create mode 100644 cmd/appinfo_test.go create mode 100755 cmd/autogen.sh create mode 100644 cmd/cmd_linux.go create mode 100644 cmd/cmd_linux_test.go create mode 100644 cmd/cmd_other.go create mode 100644 cmd/cmd_test.go create mode 100644 cmd/configure.ac create mode 100644 cmd/decode-mount-opts/decode-mount-opts.c create mode 100644 cmd/export_test.go create mode 100644 cmd/libsnap-confine-private/apparmor-support.c create mode 100644 cmd/libsnap-confine-private/apparmor-support.h create mode 100644 cmd/libsnap-confine-private/cgroup-freezer-support.c create mode 100644 cmd/libsnap-confine-private/cgroup-freezer-support.h create mode 100644 cmd/libsnap-confine-private/classic-test.c create mode 100644 cmd/libsnap-confine-private/classic.c create mode 100644 cmd/libsnap-confine-private/classic.h create mode 100644 cmd/libsnap-confine-private/cleanup-funcs-test.c create mode 100644 cmd/libsnap-confine-private/cleanup-funcs.c create mode 100644 cmd/libsnap-confine-private/cleanup-funcs.h create mode 100644 cmd/libsnap-confine-private/error-test.c create mode 100644 cmd/libsnap-confine-private/error.c create mode 100644 cmd/libsnap-confine-private/error.h create mode 100644 cmd/libsnap-confine-private/fault-injection-test.c create mode 100644 cmd/libsnap-confine-private/fault-injection.c create mode 100644 cmd/libsnap-confine-private/fault-injection.h create mode 100644 cmd/libsnap-confine-private/feature-test.c create mode 100644 cmd/libsnap-confine-private/feature.c create mode 100644 cmd/libsnap-confine-private/feature.h create mode 100644 cmd/libsnap-confine-private/locking-test.c create mode 100644 cmd/libsnap-confine-private/locking.c create mode 100644 cmd/libsnap-confine-private/locking.h create mode 100644 cmd/libsnap-confine-private/mount-opt-test.c create mode 100644 cmd/libsnap-confine-private/mount-opt.c create mode 100644 cmd/libsnap-confine-private/mount-opt.h create mode 100644 cmd/libsnap-confine-private/mountinfo-test.c create mode 100644 cmd/libsnap-confine-private/mountinfo.c create mode 100644 cmd/libsnap-confine-private/mountinfo.h create mode 100644 cmd/libsnap-confine-private/privs-test.c create mode 100644 cmd/libsnap-confine-private/privs.c create mode 100644 cmd/libsnap-confine-private/privs.h create mode 100644 cmd/libsnap-confine-private/secure-getenv-test.c create mode 100644 cmd/libsnap-confine-private/secure-getenv.c create mode 100644 cmd/libsnap-confine-private/secure-getenv.h create mode 100644 cmd/libsnap-confine-private/snap-test.c create mode 100644 cmd/libsnap-confine-private/snap.c create mode 100644 cmd/libsnap-confine-private/snap.h create mode 100644 cmd/libsnap-confine-private/string-utils-test.c create mode 100644 cmd/libsnap-confine-private/string-utils.c create mode 100644 cmd/libsnap-confine-private/string-utils.h create mode 100644 cmd/libsnap-confine-private/test-utils-test.c create mode 100644 cmd/libsnap-confine-private/test-utils.c create mode 100644 cmd/libsnap-confine-private/test-utils.h create mode 100644 cmd/libsnap-confine-private/tool.c create mode 100644 cmd/libsnap-confine-private/tool.h create mode 100644 cmd/libsnap-confine-private/unit-tests-main.c create mode 100644 cmd/libsnap-confine-private/unit-tests.c create mode 100644 cmd/libsnap-confine-private/unit-tests.h create mode 100644 cmd/libsnap-confine-private/utils-test.c create mode 100644 cmd/libsnap-confine-private/utils.c create mode 100644 cmd/libsnap-confine-private/utils.h create mode 100644 cmd/snap-confine/PORTING create mode 100644 cmd/snap-confine/README.mount_namespace create mode 100644 cmd/snap-confine/README.nvidia create mode 100644 cmd/snap-confine/README.syscalls create mode 100644 cmd/snap-confine/cookie-support-test.c create mode 100644 cmd/snap-confine/cookie-support.c create mode 100644 cmd/snap-confine/cookie-support.h create mode 100644 cmd/snap-confine/misc/0001-Add-printk-based-debugging-to-pivot_root.patch create mode 100644 cmd/snap-confine/mount-support-nvidia.c create mode 100644 cmd/snap-confine/mount-support-nvidia.h create mode 100644 cmd/snap-confine/mount-support-test.c create mode 100644 cmd/snap-confine/mount-support.c create mode 100644 cmd/snap-confine/mount-support.h create mode 100644 cmd/snap-confine/ns-support-test.c create mode 100644 cmd/snap-confine/ns-support.c create mode 100644 cmd/snap-confine/ns-support.h create mode 100644 cmd/snap-confine/seccomp-support-ext.c create mode 100644 cmd/snap-confine/seccomp-support-ext.h create mode 100644 cmd/snap-confine/seccomp-support.c create mode 100644 cmd/snap-confine/seccomp-support.h create mode 100644 cmd/snap-confine/snap-confine-args-test.c create mode 100644 cmd/snap-confine/snap-confine-args.c create mode 100644 cmd/snap-confine/snap-confine-args.h create mode 100644 cmd/snap-confine/snap-confine.apparmor.in create mode 100644 cmd/snap-confine/snap-confine.c create mode 100644 cmd/snap-confine/snap-confine.rst create mode 100755 cmd/snap-confine/snap-device-helper create mode 100644 cmd/snap-confine/snap-device-helper-test.c create mode 100644 cmd/snap-confine/spread-tests/data/apt-keys/README.md create mode 100644 cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.pub create mode 100644 cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.sec create mode 100644 cmd/snap-confine/spread-tests/distros/debian. create mode 100644 cmd/snap-confine/spread-tests/distros/debian.common create mode 100644 cmd/snap-confine/spread-tests/distros/ubuntu.14.04 create mode 100644 cmd/snap-confine/spread-tests/distros/ubuntu.16.04 create mode 100644 cmd/snap-confine/spread-tests/distros/ubuntu.16.10 create mode 100644 cmd/snap-confine/spread-tests/distros/ubuntu.common create mode 100644 cmd/snap-confine/spread-tests/main/cgroup-used/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/core-is-preferred/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/debug-flags/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/hostfs-created-on-demand/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/media-visible-in-devmode/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.amd64.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.i386.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.amd64.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.i386.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.amd64.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.i386.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.amd64.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.i386.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.linode.json create mode 100755 cmd/snap-confine/spread-tests/main/mount-ns-layout/process.py create mode 100755 cmd/snap-confine/spread-tests/main/mount-ns-layout/snap-arch.py create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-sharing/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-destination/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-source/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-missing-dst/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-missing-src/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-mount-tmpfs/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-ro-mount/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-rw-mount/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-usr-src/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/test-seccomp-compat/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/test-snap-runs/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/ubuntu-core-launcher-exists/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/user-data-dir-created/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/user-xdg-runtime-dir-created/task.yaml create mode 100644 cmd/snap-confine/spread-tests/regression/lp-1599608/task.yaml create mode 100755 cmd/snap-confine/spread-tests/release.sh create mode 100755 cmd/snap-confine/spread-tests/spread-prepare.sh create mode 100644 cmd/snap-confine/udev-support.c create mode 100644 cmd/snap-confine/udev-support.h create mode 100644 cmd/snap-confine/user-support.c create mode 100644 cmd/snap-confine/user-support.h create mode 100644 cmd/snap-discard-ns/snap-discard-ns.c create mode 100644 cmd/snap-discard-ns/snap-discard-ns.rst create mode 100644 cmd/snap-exec/export_test.go create mode 100644 cmd/snap-exec/main.go create mode 100644 cmd/snap-exec/main_test.go create mode 100644 cmd/snap-failure/cmd_snapd.go create mode 100644 cmd/snap-failure/cmd_snapd_test.go create mode 100644 cmd/snap-failure/export_test.go create mode 100644 cmd/snap-failure/main.go create mode 100644 cmd/snap-failure/main_test.go create mode 100644 cmd/snap-gdb-shim/snap-gdb-shim.c create mode 100644 cmd/snap-mgmt/snap-mgmt.sh.in create mode 100644 cmd/snap-repair/cmd_done_retry_skip.go create mode 100644 cmd/snap-repair/cmd_done_retry_skip_test.go create mode 100644 cmd/snap-repair/cmd_list.go create mode 100644 cmd/snap-repair/cmd_list_test.go create mode 100644 cmd/snap-repair/cmd_run.go create mode 100644 cmd/snap-repair/cmd_run_test.go create mode 100644 cmd/snap-repair/cmd_show.go create mode 100644 cmd/snap-repair/cmd_show_test.go create mode 100644 cmd/snap-repair/export_test.go create mode 100644 cmd/snap-repair/main.go create mode 100644 cmd/snap-repair/main_test.go create mode 100644 cmd/snap-repair/runner.go create mode 100644 cmd/snap-repair/runner_test.go create mode 100644 cmd/snap-repair/staging.go create mode 100644 cmd/snap-repair/trace.go create mode 100644 cmd/snap-repair/trace_test.go create mode 100644 cmd/snap-repair/trusted.go create mode 100644 cmd/snap-seccomp/export_test.go create mode 100644 cmd/snap-seccomp/main.go create mode 100644 cmd/snap-seccomp/main_ppc64le.go create mode 100644 cmd/snap-seccomp/main_test.go create mode 100644 cmd/snap-update-ns/bootstrap.c create mode 100644 cmd/snap-update-ns/bootstrap.go create mode 100644 cmd/snap-update-ns/bootstrap.h create mode 100644 cmd/snap-update-ns/bootstrap_ppc64le.go create mode 100644 cmd/snap-update-ns/bootstrap_test.go create mode 100644 cmd/snap-update-ns/change.go create mode 100644 cmd/snap-update-ns/change_test.go create mode 100644 cmd/snap-update-ns/export_test.go create mode 100644 cmd/snap-update-ns/freezer.go create mode 100644 cmd/snap-update-ns/freezer_test.go create mode 100644 cmd/snap-update-ns/main.go create mode 100644 cmd/snap-update-ns/main_test.go create mode 100644 cmd/snap-update-ns/secure_bindmount.go create mode 100644 cmd/snap-update-ns/secure_bindmount_test.go create mode 100644 cmd/snap-update-ns/sorting.go create mode 100644 cmd/snap-update-ns/sorting_test.go create mode 100644 cmd/snap-update-ns/trespassing.go create mode 100644 cmd/snap-update-ns/trespassing_test.go create mode 100644 cmd/snap-update-ns/utils.go create mode 100644 cmd/snap-update-ns/utils_test.go create mode 100644 cmd/snap/cmd_abort.go create mode 100644 cmd/snap/cmd_abort_test.go create mode 100644 cmd/snap/cmd_ack.go create mode 100644 cmd/snap/cmd_advise.go create mode 100644 cmd/snap/cmd_advise_test.go create mode 100644 cmd/snap/cmd_alias.go create mode 100644 cmd/snap/cmd_alias_test.go create mode 100644 cmd/snap/cmd_aliases.go create mode 100644 cmd/snap/cmd_aliases_test.go create mode 100644 cmd/snap/cmd_auto_import.go create mode 100644 cmd/snap/cmd_auto_import_test.go create mode 100644 cmd/snap/cmd_blame.go create mode 100644 cmd/snap/cmd_blame_generated.go create mode 100644 cmd/snap/cmd_booted.go create mode 100644 cmd/snap/cmd_buy.go create mode 100644 cmd/snap/cmd_buy_test.go create mode 100644 cmd/snap/cmd_can_manage_refreshes.go create mode 100644 cmd/snap/cmd_changes.go create mode 100644 cmd/snap/cmd_changes_test.go create mode 100644 cmd/snap/cmd_confinement.go create mode 100644 cmd/snap/cmd_confinement_test.go create mode 100644 cmd/snap/cmd_connect.go create mode 100644 cmd/snap/cmd_connect_test.go create mode 100644 cmd/snap/cmd_connectivity_check.go create mode 100644 cmd/snap/cmd_connectivity_check_test.go create mode 100644 cmd/snap/cmd_create_key.go create mode 100644 cmd/snap/cmd_create_key_test.go create mode 100644 cmd/snap/cmd_create_user.go create mode 100644 cmd/snap/cmd_create_user_test.go create mode 100644 cmd/snap/cmd_debug.go create mode 100644 cmd/snap/cmd_delete_key.go create mode 100644 cmd/snap/cmd_delete_key_test.go create mode 100644 cmd/snap/cmd_disconnect.go create mode 100644 cmd/snap/cmd_disconnect_test.go create mode 100644 cmd/snap/cmd_download.go create mode 100644 cmd/snap/cmd_ensure_state_soon.go create mode 100644 cmd/snap/cmd_ensure_state_soon_test.go create mode 100644 cmd/snap/cmd_export_key.go create mode 100644 cmd/snap/cmd_export_key_test.go create mode 100644 cmd/snap/cmd_find.go create mode 100644 cmd/snap/cmd_find_test.go create mode 100644 cmd/snap/cmd_first_boot.go create mode 100644 cmd/snap/cmd_get.go create mode 100644 cmd/snap/cmd_get_base_declaration.go create mode 100644 cmd/snap/cmd_get_base_declaration_test.go create mode 100644 cmd/snap/cmd_get_test.go create mode 100644 cmd/snap/cmd_handle_link.go create mode 100644 cmd/snap/cmd_help.go create mode 100644 cmd/snap/cmd_help_test.go create mode 100644 cmd/snap/cmd_info.go create mode 100644 cmd/snap/cmd_info_test.go create mode 100644 cmd/snap/cmd_interface.go create mode 100644 cmd/snap/cmd_interface_test.go create mode 100644 cmd/snap/cmd_interfaces.go create mode 100644 cmd/snap/cmd_interfaces_test.go create mode 100644 cmd/snap/cmd_keys.go create mode 100644 cmd/snap/cmd_keys_test.go create mode 100644 cmd/snap/cmd_known.go create mode 100644 cmd/snap/cmd_known_test.go create mode 100644 cmd/snap/cmd_list.go create mode 100644 cmd/snap/cmd_list_test.go create mode 100644 cmd/snap/cmd_login.go create mode 100644 cmd/snap/cmd_login_test.go create mode 100644 cmd/snap/cmd_logout.go create mode 100644 cmd/snap/cmd_managed.go create mode 100644 cmd/snap/cmd_managed_test.go create mode 100644 cmd/snap/cmd_pack.go create mode 100644 cmd/snap/cmd_pack_test.go create mode 100644 cmd/snap/cmd_paths.go create mode 100644 cmd/snap/cmd_paths_test.go create mode 100644 cmd/snap/cmd_prefer.go create mode 100644 cmd/snap/cmd_prefer_test.go create mode 100644 cmd/snap/cmd_prepare_image.go create mode 100644 cmd/snap/cmd_repair_repairs.go create mode 100644 cmd/snap/cmd_repair_repairs_test.go create mode 100644 cmd/snap/cmd_run.go create mode 100644 cmd/snap/cmd_run_test.go create mode 100644 cmd/snap/cmd_sandbox_features.go create mode 100644 cmd/snap/cmd_sandbox_features_test.go create mode 100644 cmd/snap/cmd_services.go create mode 100644 cmd/snap/cmd_services_test.go create mode 100644 cmd/snap/cmd_set.go create mode 100644 cmd/snap/cmd_set_test.go create mode 100644 cmd/snap/cmd_sign.go create mode 100644 cmd/snap/cmd_sign_build.go create mode 100644 cmd/snap/cmd_sign_build_test.go create mode 100644 cmd/snap/cmd_sign_test.go create mode 100644 cmd/snap/cmd_snap_op.go create mode 100644 cmd/snap/cmd_snap_op_test.go create mode 100644 cmd/snap/cmd_snapshot.go create mode 100644 cmd/snap/cmd_unalias.go create mode 100644 cmd/snap/cmd_unalias_test.go create mode 100644 cmd/snap/cmd_userd.go create mode 100644 cmd/snap/cmd_userd_test.go create mode 100644 cmd/snap/cmd_version.go create mode 100644 cmd/snap/cmd_version_linux.go create mode 100644 cmd/snap/cmd_version_other.go create mode 100644 cmd/snap/cmd_version_test.go create mode 100644 cmd/snap/cmd_wait.go create mode 100644 cmd/snap/cmd_wait_test.go create mode 100644 cmd/snap/cmd_warnings.go create mode 100644 cmd/snap/cmd_warnings_test.go create mode 100644 cmd/snap/cmd_watch.go create mode 100644 cmd/snap/cmd_watch_test.go create mode 100644 cmd/snap/cmd_whoami.go create mode 100644 cmd/snap/color.go create mode 100644 cmd/snap/color_test.go create mode 100644 cmd/snap/complete.go create mode 100644 cmd/snap/error.go create mode 100644 cmd/snap/export_test.go create mode 100644 cmd/snap/gnupg2_test.go create mode 100644 cmd/snap/interfaces_common.go create mode 100644 cmd/snap/interfaces_common_test.go create mode 100644 cmd/snap/last.go create mode 100644 cmd/snap/main.go create mode 100644 cmd/snap/main_test.go create mode 100644 cmd/snap/notes.go create mode 100644 cmd/snap/notes_test.go create mode 100644 cmd/snap/test-data/pubring.gpg create mode 100644 cmd/snap/test-data/secring.gpg create mode 100644 cmd/snap/test-data/trustdb.gpg create mode 100644 cmd/snap/times.go create mode 100644 cmd/snap/wait.go create mode 100644 cmd/snapctl/main.go create mode 100644 cmd/snapctl/main_test.go create mode 100755 cmd/snapd-apparmor/snapd-apparmor create mode 100644 cmd/snapd-env-generator/main.c create mode 100644 cmd/snapd-env-generator/snapd-env-generator.rst create mode 100644 cmd/snapd-generator/main.c create mode 100644 cmd/snapd/export_test.go create mode 100644 cmd/snapd/main.go create mode 100644 cmd/snapd/main_test.go create mode 100644 cmd/system-shutdown/system-shutdown-utils-test.c create mode 100644 cmd/system-shutdown/system-shutdown-utils.c create mode 100644 cmd/system-shutdown/system-shutdown-utils.h create mode 100644 cmd/system-shutdown/system-shutdown.c create mode 100644 cmd/version.go create mode 100644 daemon/api.go create mode 100644 daemon/api_json.go create mode 100644 daemon/api_mock_test.go create mode 100644 daemon/api_snapshots.go create mode 100644 daemon/api_snapshots_test.go create mode 100644 daemon/api_test.go create mode 100644 daemon/command_counter_test.go create mode 100644 daemon/daemon.go create mode 100644 daemon/daemon_test.go create mode 100644 daemon/export_snapshots_test.go create mode 100644 daemon/response.go create mode 100644 daemon/response_test.go create mode 100644 daemon/snap.go create mode 100644 daemon/ucrednet.go create mode 100644 daemon/ucrednet_test.go create mode 100644 data/Makefile create mode 100644 data/apt/20snapd.conf create mode 100644 data/completion/complete.sh create mode 100644 data/completion/etelpmoc.sh create mode 100644 data/completion/snap create mode 100644 data/dbus/Makefile create mode 100644 data/dbus/io.snapcraft.Launcher.service.in create mode 100644 data/dbus/io.snapcraft.Settings.service.in create mode 100644 data/desktop/Makefile create mode 100644 data/desktop/snap-handle-link.desktop.in create mode 100644 data/desktop/snap-userd-autostart.desktop.in create mode 100644 data/env/Makefile create mode 100644 data/env/snapd.sh.in create mode 100644 data/failure.txt create mode 100644 data/polkit/io.snapcraft.snapd.policy create mode 100644 data/selinux/COPYING create mode 100644 data/selinux/INSTALL.md create mode 100644 data/selinux/Makefile create mode 100644 data/selinux/README.md create mode 100644 data/selinux/snappy.fc create mode 100644 data/selinux/snappy.if create mode 100644 data/selinux/snappy.te create mode 100644 data/success.txt create mode 100644 data/sysctl/rhel7-snap.conf create mode 100644 data/systemd-env/990-snapd.conf.in create mode 100644 data/systemd-env/Makefile create mode 100644 data/systemd/Makefile create mode 100644 data/systemd/snapd.apparmor.service.in create mode 100644 data/systemd/snapd.autoimport.service.in create mode 100644 data/systemd/snapd.core-fixup.service.in create mode 100755 data/systemd/snapd.core-fixup.sh create mode 100644 data/systemd/snapd.failure.service.in create mode 100644 data/systemd/snapd.run-from-snap create mode 100644 data/systemd/snapd.seeded.service.in create mode 100644 data/systemd/snapd.service.in create mode 100644 data/systemd/snapd.snap-repair.service.in create mode 100644 data/systemd/snapd.snap-repair.timer create mode 100644 data/systemd/snapd.socket create mode 100644 data/systemd/snapd.system-shutdown.service.in create mode 100644 data/udev/rules.d/66-snapd-autoimport.rules create mode 120000 debian create mode 100644 dirs/dirs.go create mode 100644 dirs/dirs_test.go create mode 100644 dirs/export_test.go create mode 100644 docs/MOVED.md create mode 100644 errtracker/errtracker.go create mode 100644 errtracker/errtracker_test.go create mode 100644 errtracker/export_test.go create mode 100644 features/export_test.go create mode 100644 features/features.go create mode 100644 features/features_test.go create mode 100755 gen-coverage.sh create mode 100755 generate-packaging-dir create mode 100755 get-deps.sh create mode 100644 httputil/client.go create mode 100644 httputil/client_test.go create mode 100644 httputil/export_test.go create mode 100644 httputil/logger.go create mode 100644 httputil/logger_test.go create mode 100644 httputil/redirect17.go create mode 100644 httputil/redirect18.go create mode 100644 httputil/retry.go create mode 100644 httputil/retry_test.go create mode 100644 httputil/transport16.go create mode 100644 httputil/transport17.go create mode 100644 httputil/useragent.go create mode 100644 httputil/useragent_test.go create mode 100644 httputil/withtestkeys.go create mode 100644 i18n/i18n.go create mode 100644 i18n/i18n_test.go create mode 100644 i18n/xgettext-go/main.go create mode 100644 i18n/xgettext-go/main_test.go create mode 100644 image/export_test.go create mode 100644 image/helpers.go create mode 100644 image/image.go create mode 100644 image/image_test.go create mode 100644 interfaces/apparmor/apparmor.go create mode 100644 interfaces/apparmor/apparmor_test.go create mode 100644 interfaces/apparmor/backend.go create mode 100644 interfaces/apparmor/backend_test.go create mode 100644 interfaces/apparmor/export_test.go create mode 100644 interfaces/apparmor/spec.go create mode 100644 interfaces/apparmor/spec_test.go create mode 100644 interfaces/apparmor/template.go create mode 100644 interfaces/apparmor/template_vars.go create mode 100644 interfaces/backend.go create mode 100644 interfaces/backends/backends.go create mode 100644 interfaces/backends/backends_test.go create mode 100644 interfaces/backends/export_test.go create mode 100644 interfaces/builtin/account_control.go create mode 100644 interfaces/builtin/account_control_test.go create mode 100644 interfaces/builtin/accounts_service.go create mode 100644 interfaces/builtin/accounts_service_test.go create mode 100644 interfaces/builtin/adb_support.go create mode 100644 interfaces/builtin/adb_support_test.go create mode 100644 interfaces/builtin/all.go create mode 100644 interfaces/builtin/all_test.go create mode 100644 interfaces/builtin/alsa.go create mode 100644 interfaces/builtin/alsa_test.go create mode 100644 interfaces/builtin/autopilot.go create mode 100644 interfaces/builtin/autopilot_test.go create mode 100644 interfaces/builtin/avahi_control.go create mode 100644 interfaces/builtin/avahi_control_test.go create mode 100644 interfaces/builtin/avahi_observe.go create mode 100644 interfaces/builtin/avahi_observe_test.go create mode 100644 interfaces/builtin/block_devices.go create mode 100644 interfaces/builtin/block_devices_test.go create mode 100644 interfaces/builtin/bluetooth_control.go create mode 100644 interfaces/builtin/bluetooth_control_test.go create mode 100644 interfaces/builtin/bluez.go create mode 100644 interfaces/builtin/bluez_test.go create mode 100644 interfaces/builtin/bool_file.go create mode 100644 interfaces/builtin/bool_file_test.go create mode 100644 interfaces/builtin/broadcom_asic_control.go create mode 100644 interfaces/builtin/broadcom_asic_control_test.go create mode 100644 interfaces/builtin/browser_support.go create mode 100644 interfaces/builtin/browser_support_test.go create mode 100644 interfaces/builtin/calendar_service.go create mode 100644 interfaces/builtin/calendar_service_test.go create mode 100644 interfaces/builtin/camera.go create mode 100644 interfaces/builtin/camera_test.go create mode 100644 interfaces/builtin/can_bus.go create mode 100644 interfaces/builtin/can_bus_test.go create mode 100644 interfaces/builtin/cifs_mount.go create mode 100644 interfaces/builtin/cifs_mount_test.go create mode 100644 interfaces/builtin/classic_support.go create mode 100644 interfaces/builtin/classic_support_test.go create mode 100644 interfaces/builtin/common.go create mode 100644 interfaces/builtin/common_files.go create mode 100644 interfaces/builtin/common_test.go create mode 100644 interfaces/builtin/contacts_service.go create mode 100644 interfaces/builtin/contacts_service_test.go create mode 100644 interfaces/builtin/content.go create mode 100644 interfaces/builtin/content_test.go create mode 100644 interfaces/builtin/core_support.go create mode 100644 interfaces/builtin/core_support_test.go create mode 100644 interfaces/builtin/cpu_control.go create mode 100644 interfaces/builtin/cpu_control_test.go create mode 100644 interfaces/builtin/cups_control.go create mode 100644 interfaces/builtin/daemon_notify.go create mode 100644 interfaces/builtin/daemon_notify_test.go create mode 100644 interfaces/builtin/dbus.go create mode 100644 interfaces/builtin/dbus_test.go create mode 100644 interfaces/builtin/dcdbas_control.go create mode 100644 interfaces/builtin/dcdbas_control_test.go create mode 100644 interfaces/builtin/desktop.go create mode 100644 interfaces/builtin/desktop_legacy.go create mode 100644 interfaces/builtin/desktop_legacy_test.go create mode 100644 interfaces/builtin/desktop_test.go create mode 100644 interfaces/builtin/device_buttons.go create mode 100644 interfaces/builtin/device_buttons_test.go create mode 100644 interfaces/builtin/display_control.go create mode 100644 interfaces/builtin/display_control_test.go create mode 100644 interfaces/builtin/docker.go create mode 100644 interfaces/builtin/docker_support.go create mode 100644 interfaces/builtin/docker_support_test.go create mode 100644 interfaces/builtin/docker_test.go create mode 100644 interfaces/builtin/dummy.go create mode 100644 interfaces/builtin/dvb.go create mode 100644 interfaces/builtin/dvb_test.go create mode 100644 interfaces/builtin/export_test.go create mode 100644 interfaces/builtin/firewall_control.go create mode 100644 interfaces/builtin/firewall_control_test.go create mode 100644 interfaces/builtin/framebuffer.go create mode 100644 interfaces/builtin/framebuffer_test.go create mode 100644 interfaces/builtin/fuse_support.go create mode 100644 interfaces/builtin/fuse_support_test.go create mode 100644 interfaces/builtin/fwupd.go create mode 100644 interfaces/builtin/fwupd_test.go create mode 100644 interfaces/builtin/gpg_keys.go create mode 100644 interfaces/builtin/gpg_keys_test.go create mode 100644 interfaces/builtin/gpg_public_keys.go create mode 100644 interfaces/builtin/gpg_public_keys_test.go create mode 100644 interfaces/builtin/gpio.go create mode 100644 interfaces/builtin/gpio_memory_control.go create mode 100644 interfaces/builtin/gpio_memory_control_test.go create mode 100644 interfaces/builtin/gpio_test.go create mode 100644 interfaces/builtin/greengrass_support.go create mode 100644 interfaces/builtin/greengrass_support_test.go create mode 100644 interfaces/builtin/gsettings.go create mode 100644 interfaces/builtin/gsettings_test.go create mode 100644 interfaces/builtin/hardware_observe.go create mode 100644 interfaces/builtin/hardware_observe_test.go create mode 100644 interfaces/builtin/hardware_random_control.go create mode 100644 interfaces/builtin/hardware_random_control_test.go create mode 100644 interfaces/builtin/hardware_random_observe.go create mode 100644 interfaces/builtin/hardware_random_observe_test.go create mode 100644 interfaces/builtin/hidraw.go create mode 100644 interfaces/builtin/hidraw_test.go create mode 100644 interfaces/builtin/home.go create mode 100644 interfaces/builtin/home_test.go create mode 100644 interfaces/builtin/hostname_control.go create mode 100644 interfaces/builtin/hostname_control_test.go create mode 100644 interfaces/builtin/i2c.go create mode 100644 interfaces/builtin/i2c_test.go create mode 100644 interfaces/builtin/iio.go create mode 100644 interfaces/builtin/iio_test.go create mode 100644 interfaces/builtin/io_ports_control.go create mode 100644 interfaces/builtin/io_ports_control_test.go create mode 100644 interfaces/builtin/joystick.go create mode 100644 interfaces/builtin/joystick_test.go create mode 100644 interfaces/builtin/juju_client_observe.go create mode 100644 interfaces/builtin/juju_client_observe_test.go create mode 100644 interfaces/builtin/kernel_module_control.go create mode 100644 interfaces/builtin/kernel_module_control_test.go create mode 100644 interfaces/builtin/kernel_module_observe.go create mode 100644 interfaces/builtin/kernel_module_observe_test.go create mode 100644 interfaces/builtin/kubernetes_support.go create mode 100644 interfaces/builtin/kubernetes_support_test.go create mode 100644 interfaces/builtin/kvm.go create mode 100644 interfaces/builtin/kvm_test.go create mode 100644 interfaces/builtin/libvirt.go create mode 100644 interfaces/builtin/libvirt_test.go create mode 100644 interfaces/builtin/locale_control.go create mode 100644 interfaces/builtin/locale_control_test.go create mode 100644 interfaces/builtin/location_control.go create mode 100644 interfaces/builtin/location_control_test.go create mode 100644 interfaces/builtin/location_observe.go create mode 100644 interfaces/builtin/location_observe_test.go create mode 100644 interfaces/builtin/log_observe.go create mode 100644 interfaces/builtin/log_observe_test.go create mode 100644 interfaces/builtin/lxd.go create mode 100644 interfaces/builtin/lxd_support.go create mode 100644 interfaces/builtin/lxd_support_test.go create mode 100644 interfaces/builtin/lxd_test.go create mode 100644 interfaces/builtin/maliit.go create mode 100644 interfaces/builtin/maliit_test.go create mode 100644 interfaces/builtin/media_hub.go create mode 100644 interfaces/builtin/media_hub_test.go create mode 100644 interfaces/builtin/mir.go create mode 100644 interfaces/builtin/mir_test.go create mode 100644 interfaces/builtin/modem_manager.go create mode 100644 interfaces/builtin/modem_manager_test.go create mode 100644 interfaces/builtin/mount_observe.go create mode 100644 interfaces/builtin/mount_observe_test.go create mode 100644 interfaces/builtin/mpris.go create mode 100644 interfaces/builtin/mpris_test.go create mode 100644 interfaces/builtin/netlink_audit.go create mode 100644 interfaces/builtin/netlink_audit_test.go create mode 100644 interfaces/builtin/netlink_connector.go create mode 100644 interfaces/builtin/netlink_connector_test.go create mode 100644 interfaces/builtin/network.go create mode 100644 interfaces/builtin/network_bind.go create mode 100644 interfaces/builtin/network_bind_test.go create mode 100644 interfaces/builtin/network_control.go create mode 100644 interfaces/builtin/network_control_test.go create mode 100644 interfaces/builtin/network_manager.go create mode 100644 interfaces/builtin/network_manager_test.go create mode 100644 interfaces/builtin/network_observe.go create mode 100644 interfaces/builtin/network_observe_test.go create mode 100644 interfaces/builtin/network_setup_control.go create mode 100644 interfaces/builtin/network_setup_control_test.go create mode 100644 interfaces/builtin/network_setup_observe.go create mode 100644 interfaces/builtin/network_setup_observe_test.go create mode 100644 interfaces/builtin/network_status.go create mode 100644 interfaces/builtin/network_status_test.go create mode 100644 interfaces/builtin/network_test.go create mode 100644 interfaces/builtin/ofono.go create mode 100644 interfaces/builtin/ofono_test.go create mode 100644 interfaces/builtin/online_accounts_service.go create mode 100644 interfaces/builtin/online_accounts_service_test.go create mode 100644 interfaces/builtin/opengl.go create mode 100644 interfaces/builtin/opengl_test.go create mode 100644 interfaces/builtin/openvswitch.go create mode 100644 interfaces/builtin/openvswitch_support.go create mode 100644 interfaces/builtin/openvswitch_support_test.go create mode 100644 interfaces/builtin/openvswitch_test.go create mode 100644 interfaces/builtin/optical_drive.go create mode 100644 interfaces/builtin/optical_drive_test.go create mode 100644 interfaces/builtin/password_manager_service.go create mode 100644 interfaces/builtin/password_manager_service_test.go create mode 100644 interfaces/builtin/personal_files.go create mode 100644 interfaces/builtin/personal_files_test.go create mode 100644 interfaces/builtin/physical_memory_control.go create mode 100644 interfaces/builtin/physical_memory_control_test.go create mode 100644 interfaces/builtin/physical_memory_observe.go create mode 100644 interfaces/builtin/physical_memory_observe_test.go create mode 100644 interfaces/builtin/ppp.go create mode 100644 interfaces/builtin/ppp_test.go create mode 100644 interfaces/builtin/process_control.go create mode 100644 interfaces/builtin/process_control_test.go create mode 100644 interfaces/builtin/pulseaudio.go create mode 100644 interfaces/builtin/pulseaudio_test.go create mode 100644 interfaces/builtin/raw_usb.go create mode 100644 interfaces/builtin/raw_usb_test.go create mode 100644 interfaces/builtin/removable_media.go create mode 100644 interfaces/builtin/removable_media_test.go create mode 100644 interfaces/builtin/screen_inhibit_control.go create mode 100644 interfaces/builtin/screen_inhibit_control_test.go create mode 100644 interfaces/builtin/screencast_legacy.go create mode 100644 interfaces/builtin/screencast_legacy_test.go create mode 100644 interfaces/builtin/serial_port.go create mode 100644 interfaces/builtin/serial_port_test.go create mode 100644 interfaces/builtin/shutdown.go create mode 100644 interfaces/builtin/shutdown_test.go create mode 100644 interfaces/builtin/snapd_control.go create mode 100644 interfaces/builtin/snapd_control_test.go create mode 100644 interfaces/builtin/spi.go create mode 100644 interfaces/builtin/spi_test.go create mode 100644 interfaces/builtin/ssh_keys.go create mode 100644 interfaces/builtin/ssh_keys_test.go create mode 100644 interfaces/builtin/ssh_public_keys.go create mode 100644 interfaces/builtin/ssh_public_keys_test.go create mode 100644 interfaces/builtin/storage_framework_service.go create mode 100644 interfaces/builtin/storage_framework_service_test.go create mode 100644 interfaces/builtin/system_files.go create mode 100644 interfaces/builtin/system_files_test.go create mode 100644 interfaces/builtin/system_observe.go create mode 100644 interfaces/builtin/system_observe_test.go create mode 100644 interfaces/builtin/system_trace.go create mode 100644 interfaces/builtin/system_trace_test.go create mode 100644 interfaces/builtin/thumbnailer_service.go create mode 100644 interfaces/builtin/thumbnailer_service_test.go create mode 100644 interfaces/builtin/time_control.go create mode 100644 interfaces/builtin/time_control_test.go create mode 100644 interfaces/builtin/timeserver_control.go create mode 100644 interfaces/builtin/timeserver_control_test.go create mode 100644 interfaces/builtin/timezone_control.go create mode 100644 interfaces/builtin/timezone_control_test.go create mode 100644 interfaces/builtin/tpm.go create mode 100644 interfaces/builtin/tpm_test.go create mode 100644 interfaces/builtin/u2f_devices.go create mode 100644 interfaces/builtin/u2f_devices_test.go create mode 100644 interfaces/builtin/ubuntu_download_manager.go create mode 100644 interfaces/builtin/ubuntu_download_manager_test.go create mode 100644 interfaces/builtin/udisks2.go create mode 100644 interfaces/builtin/udisks2_test.go create mode 100644 interfaces/builtin/uhid.go create mode 100644 interfaces/builtin/uhid_test.go create mode 100644 interfaces/builtin/unity7.go create mode 100644 interfaces/builtin/unity7_test.go create mode 100644 interfaces/builtin/unity8.go create mode 100644 interfaces/builtin/unity8_calendar.go create mode 100644 interfaces/builtin/unity8_calendar_test.go create mode 100644 interfaces/builtin/unity8_contacts.go create mode 100644 interfaces/builtin/unity8_contacts_test.go create mode 100644 interfaces/builtin/unity8_pim_common.go create mode 100644 interfaces/builtin/unity8_test.go create mode 100644 interfaces/builtin/upower_observe.go create mode 100644 interfaces/builtin/upower_observe_test.go create mode 100644 interfaces/builtin/utils.go create mode 100644 interfaces/builtin/utils_test.go create mode 100644 interfaces/builtin/wayland.go create mode 100644 interfaces/builtin/wayland_test.go create mode 100644 interfaces/builtin/x11.go create mode 100644 interfaces/builtin/x11_test.go create mode 100644 interfaces/connection.go create mode 100644 interfaces/connection_test.go create mode 100644 interfaces/core.go create mode 100644 interfaces/core_test.go create mode 100644 interfaces/dbus/backend.go create mode 100644 interfaces/dbus/backend_test.go create mode 100644 interfaces/dbus/dbus.go create mode 100644 interfaces/dbus/dbus_test.go create mode 100644 interfaces/dbus/export_test.go create mode 100644 interfaces/dbus/spec.go create mode 100644 interfaces/dbus/spec_test.go create mode 100644 interfaces/dbus/template.go create mode 100644 interfaces/export_test.go create mode 100644 interfaces/hotplug/deviceinfo.go create mode 100644 interfaces/hotplug/deviceinfo_test.go create mode 100644 interfaces/hotplug/spec.go create mode 100644 interfaces/hotplug/spec_test.go create mode 100644 interfaces/hotplug/udevadm.go create mode 100644 interfaces/hotplug/udevadm_test.go create mode 100644 interfaces/ifacetest/backend.go create mode 100644 interfaces/ifacetest/backendtest.go create mode 100644 interfaces/ifacetest/ifacetest_test.go create mode 100644 interfaces/ifacetest/spec.go create mode 100644 interfaces/ifacetest/spec_test.go create mode 100644 interfaces/ifacetest/testiface.go create mode 100644 interfaces/ifacetest/testiface_test.go create mode 100644 interfaces/kmod/backend.go create mode 100644 interfaces/kmod/backend_test.go create mode 100644 interfaces/kmod/export_test.go create mode 100644 interfaces/kmod/kmod.go create mode 100644 interfaces/kmod/kmod_test.go create mode 100644 interfaces/kmod/spec.go create mode 100644 interfaces/kmod/spec_test.go create mode 100644 interfaces/mount/backend.go create mode 100644 interfaces/mount/backend_test.go create mode 100644 interfaces/mount/lock.go create mode 100644 interfaces/mount/lock_test.go create mode 100644 interfaces/mount/ns.go create mode 100644 interfaces/mount/ns_test.go create mode 100644 interfaces/mount/spec.go create mode 100644 interfaces/mount/spec_test.go create mode 100644 interfaces/naming.go create mode 100644 interfaces/naming_test.go create mode 100644 interfaces/policy/basedeclaration.go create mode 100644 interfaces/policy/basedeclaration_test.go create mode 100644 interfaces/policy/export_test.go create mode 100644 interfaces/policy/helpers.go create mode 100644 interfaces/policy/helpers_test.go create mode 100644 interfaces/policy/policy.go create mode 100644 interfaces/policy/policy_test.go create mode 100644 interfaces/repo.go create mode 100644 interfaces/repo_test.go create mode 100644 interfaces/seccomp/backend.go create mode 100644 interfaces/seccomp/backend_test.go create mode 100644 interfaces/seccomp/export_test.go create mode 100644 interfaces/seccomp/seccomp_test.go create mode 100644 interfaces/seccomp/spec.go create mode 100644 interfaces/seccomp/spec_test.go create mode 100644 interfaces/seccomp/template.go create mode 100644 interfaces/sorting.go create mode 100644 interfaces/sorting_test.go create mode 100644 interfaces/system_key.go create mode 100644 interfaces/system_key_test.go create mode 100644 interfaces/systemd/backend.go create mode 100644 interfaces/systemd/backend_test.go create mode 100644 interfaces/systemd/service.go create mode 100644 interfaces/systemd/service_test.go create mode 100644 interfaces/systemd/spec.go create mode 100644 interfaces/systemd/spec_test.go create mode 100644 interfaces/systemd/systemd_test.go create mode 100644 interfaces/udev/backend.go create mode 100644 interfaces/udev/backend_test.go create mode 100644 interfaces/udev/spec.go create mode 100644 interfaces/udev/spec_test.go create mode 100644 interfaces/udev/udev.go create mode 100644 interfaces/udev/udev_test.go create mode 100644 interfaces/utils/utils.go create mode 100644 interfaces/utils/utils_test.go create mode 100644 jsonutil/json.go create mode 100644 jsonutil/json_test.go create mode 100644 jsonutil/safejson/safejson.go create mode 100644 jsonutil/safejson/safejson_test.go create mode 100644 logger/export_test.go create mode 100644 logger/logger.go create mode 100644 logger/logger_test.go create mode 100755 mdlint.py create mode 100755 mkauthors.sh create mode 100755 mkversion.sh create mode 100644 netutil/metered.go create mode 100644 osutil/bootid.go create mode 100644 osutil/bootid_test.go create mode 100644 osutil/buildid.go create mode 100644 osutil/buildid_test.go create mode 100644 osutil/chattr.go create mode 100644 osutil/chattr_32.go create mode 100644 osutil/chattr_64.go create mode 100644 osutil/chdir.go create mode 100644 osutil/chdir_test.go create mode 100644 osutil/cmp.go create mode 100644 osutil/cmp_test.go create mode 100644 osutil/context.go create mode 100644 osutil/context_test.go create mode 100644 osutil/cp.go create mode 100644 osutil/cp_linux.go create mode 100644 osutil/cp_linux_test.go create mode 100644 osutil/cp_other.go create mode 100644 osutil/cp_test.go create mode 100644 osutil/digest.go create mode 100644 osutil/digest_test.go create mode 100644 osutil/env.go create mode 100644 osutil/env_test.go create mode 100644 osutil/exec.go create mode 100644 osutil/exec_test.go create mode 100644 osutil/exitcode.go create mode 100644 osutil/exitcode_test.go create mode 100644 osutil/export_test.go create mode 100644 osutil/flock.go create mode 100644 osutil/flock_test.go create mode 100644 osutil/fshelpers.go create mode 100644 osutil/fshelpers_test.go create mode 100644 osutil/group.go create mode 100644 osutil/io.go create mode 100644 osutil/io_test.go create mode 100644 osutil/mkdirallchown.go create mode 100644 osutil/mkdirallchown_test.go create mode 100644 osutil/mockable.go create mode 100644 osutil/mount_darwin.go create mode 100644 osutil/mount_linux.go create mode 100644 osutil/mount_linux_test.go create mode 100644 osutil/mountentry_linux.go create mode 100644 osutil/mountentry_linux_test.go create mode 100644 osutil/mountinfo_linux.go create mode 100644 osutil/mountinfo_linux_test.go create mode 100644 osutil/mountprofile_linux.go create mode 100644 osutil/mountprofile_linux_test.go create mode 100644 osutil/nfs_darwin.go create mode 100644 osutil/nfs_linux.go create mode 100644 osutil/nfs_linux_test.go create mode 100644 osutil/osutil_darwin.go create mode 100644 osutil/osutil_test.go create mode 100644 osutil/outputerr.go create mode 100644 osutil/outputerr_test.go create mode 100644 osutil/overlay_darwin.go create mode 100644 osutil/overlay_linux.go create mode 100644 osutil/overlay_linux_test.go create mode 100644 osutil/squashfs/fstype.go create mode 100644 osutil/stat.go create mode 100644 osutil/stat_test.go create mode 100644 osutil/strace/export_test.go create mode 100644 osutil/strace/strace.go create mode 100644 osutil/strace/strace_test.go create mode 100644 osutil/strace/timing.go create mode 100644 osutil/strace/timing_test.go create mode 100644 osutil/syncdir.go create mode 100644 osutil/syncdir_test.go create mode 100644 osutil/sys/syscall.go create mode 100644 osutil/sys/sysnum_16_linux.go create mode 100644 osutil/sys/sysnum_32_linux.go create mode 100644 osutil/sys/sysnum_darwin.go create mode 100644 osutil/sys/sysnum_linux.go create mode 100644 osutil/sys_linux.go create mode 100644 osutil/sys_linux_test.go create mode 100644 osutil/udev/.travis.yml create mode 100644 osutil/udev/LICENSE create mode 100644 osutil/udev/README.md create mode 100644 osutil/udev/crawler/device.go create mode 100644 osutil/udev/main.go.sample create mode 100644 osutil/udev/matcher.sample create mode 100644 osutil/udev/netlink/conn.go create mode 100644 osutil/udev/netlink/conn_test.go create mode 100644 osutil/udev/netlink/matcher.go create mode 100644 osutil/udev/netlink/matcher_test.go create mode 100644 osutil/udev/netlink/uevent.go create mode 100644 osutil/udev/netlink/uevent_test.go create mode 100644 osutil/uname.go create mode 100644 osutil/uname_darwin.go create mode 100644 osutil/uname_linux.go create mode 100644 osutil/uname_linux_test.go create mode 100644 osutil/unlink.go create mode 100644 osutil/unlink_darwin.go create mode 100644 osutil/unlink_linux.go create mode 100644 osutil/unlink_test.go create mode 100644 osutil/user.go create mode 100644 osutil/user_test.go create mode 100644 osutil/winsize.go create mode 100644 overlord/assertstate/assertmgr.go create mode 100644 overlord/assertstate/assertstate.go create mode 100644 overlord/assertstate/assertstate_test.go create mode 100644 overlord/assertstate/export_test.go create mode 100644 overlord/assertstate/helpers.go create mode 100644 overlord/auth/auth.go create mode 100644 overlord/auth/auth_test.go create mode 100644 overlord/backend.go create mode 100644 overlord/cmdstate/cmdmgr.go create mode 100644 overlord/cmdstate/cmdstate.go create mode 100644 overlord/cmdstate/cmdstate_test.go create mode 100644 overlord/cmdstate/export_test.go create mode 100644 overlord/configstate/config/helpers.go create mode 100644 overlord/configstate/config/helpers_test.go create mode 100644 overlord/configstate/config/transaction.go create mode 100644 overlord/configstate/config/transaction_test.go create mode 100644 overlord/configstate/configcore/cloud.go create mode 100644 overlord/configstate/configcore/cloud_test.go create mode 100644 overlord/configstate/configcore/corecfg.go create mode 100644 overlord/configstate/configcore/corecfg_test.go create mode 100644 overlord/configstate/configcore/experimental.go create mode 100644 overlord/configstate/configcore/experimental_test.go create mode 100644 overlord/configstate/configcore/export_test.go create mode 100644 overlord/configstate/configcore/network.go create mode 100644 overlord/configstate/configcore/network_test.go create mode 100644 overlord/configstate/configcore/picfg.go create mode 100644 overlord/configstate/configcore/picfg_test.go create mode 100644 overlord/configstate/configcore/powerbtn.go create mode 100644 overlord/configstate/configcore/powerbtn_test.go create mode 100644 overlord/configstate/configcore/proxy.go create mode 100644 overlord/configstate/configcore/proxy_test.go create mode 100644 overlord/configstate/configcore/refresh.go create mode 100644 overlord/configstate/configcore/refresh_test.go create mode 100644 overlord/configstate/configcore/services.go create mode 100644 overlord/configstate/configcore/services_test.go create mode 100644 overlord/configstate/configcore/utils.go create mode 100644 overlord/configstate/configcore/utils_test.go create mode 100644 overlord/configstate/configcore/watchdog.go create mode 100644 overlord/configstate/configcore/watchdog_test.go create mode 100644 overlord/configstate/configmgr.go create mode 100644 overlord/configstate/configstate.go create mode 100644 overlord/configstate/configstate_test.go create mode 100644 overlord/configstate/export_test.go create mode 100644 overlord/configstate/handler_test.go create mode 100644 overlord/configstate/hooks.go create mode 100644 overlord/configstate/proxyconf/proxy.go create mode 100644 overlord/configstate/proxyconf/proxy_test.go create mode 100644 overlord/configstate/settings/settings.go create mode 100644 overlord/configstate/settings/settings_test.go create mode 100644 overlord/devicestate/crypto.go create mode 100644 overlord/devicestate/devicemgr.go create mode 100644 overlord/devicestate/devicestate.go create mode 100644 overlord/devicestate/devicestate_test.go create mode 100644 overlord/devicestate/export_test.go create mode 100644 overlord/devicestate/firstboot.go create mode 100644 overlord/devicestate/firstboot_test.go create mode 100644 overlord/devicestate/handlers.go create mode 100644 overlord/export_test.go create mode 100644 overlord/hookstate/context.go create mode 100644 overlord/hookstate/context_test.go create mode 100644 overlord/hookstate/ctlcmd/ctlcmd.go create mode 100644 overlord/hookstate/ctlcmd/ctlcmd_test.go create mode 100644 overlord/hookstate/ctlcmd/export_test.go create mode 100644 overlord/hookstate/ctlcmd/get.go create mode 100644 overlord/hookstate/ctlcmd/get_test.go create mode 100644 overlord/hookstate/ctlcmd/helpers.go create mode 100644 overlord/hookstate/ctlcmd/restart.go create mode 100644 overlord/hookstate/ctlcmd/services.go create mode 100644 overlord/hookstate/ctlcmd/services_test.go create mode 100644 overlord/hookstate/ctlcmd/set.go create mode 100644 overlord/hookstate/ctlcmd/set_test.go create mode 100644 overlord/hookstate/ctlcmd/start.go create mode 100644 overlord/hookstate/ctlcmd/stop.go create mode 100644 overlord/hookstate/export_test.go create mode 100644 overlord/hookstate/hookmgr.go create mode 100644 overlord/hookstate/hooks.go create mode 100644 overlord/hookstate/hookstate.go create mode 100644 overlord/hookstate/hookstate_test.go create mode 100644 overlord/hookstate/hooktest/handler.go create mode 100644 overlord/hookstate/hooktest/handler_test.go create mode 100644 overlord/hookstate/repository.go create mode 100644 overlord/hookstate/repository_test.go create mode 100644 overlord/ifacestate/export_test.go create mode 100644 overlord/ifacestate/handlers.go create mode 100644 overlord/ifacestate/handlers_test.go create mode 100644 overlord/ifacestate/helpers.go create mode 100644 overlord/ifacestate/helpers_test.go create mode 100644 overlord/ifacestate/hooks.go create mode 100644 overlord/ifacestate/hotplug.go create mode 100644 overlord/ifacestate/hotplug_test.go create mode 100644 overlord/ifacestate/ifacemgr.go create mode 100644 overlord/ifacestate/ifacerepo/repo.go create mode 100644 overlord/ifacestate/ifacerepo/repo_test.go create mode 100644 overlord/ifacestate/ifacestate.go create mode 100644 overlord/ifacestate/ifacestate_test.go create mode 100644 overlord/ifacestate/implicit.go create mode 100644 overlord/ifacestate/implicit_test.go create mode 100644 overlord/ifacestate/udevmonitor/udevmon.go create mode 100644 overlord/ifacestate/udevmonitor/udevmon_test.go create mode 100644 overlord/managers_test.go create mode 100644 overlord/overlord.go create mode 100644 overlord/overlord_test.go create mode 100644 overlord/patch/export_test.go create mode 100644 overlord/patch/patch.go create mode 100644 overlord/patch/patch1.go create mode 100644 overlord/patch/patch1_test.go create mode 100644 overlord/patch/patch2.go create mode 100644 overlord/patch/patch2_test.go create mode 100644 overlord/patch/patch3.go create mode 100644 overlord/patch/patch3_test.go create mode 100644 overlord/patch/patch4.go create mode 100644 overlord/patch/patch4_test.go create mode 100644 overlord/patch/patch5.go create mode 100644 overlord/patch/patch6.go create mode 100644 overlord/patch/patch6_1.go create mode 100644 overlord/patch/patch6_1_test.go create mode 100644 overlord/patch/patch6_test.go create mode 100644 overlord/patch/patch_test.go create mode 100644 overlord/servicestate/servicestate.go create mode 100644 overlord/snapshotstate/backend/backend.go create mode 100644 overlord/snapshotstate/backend/backend_test.go create mode 100644 overlord/snapshotstate/backend/export_test.go create mode 100644 overlord/snapshotstate/backend/helpers.go create mode 100644 overlord/snapshotstate/backend/reader.go create mode 100644 overlord/snapshotstate/backend/restorestate.go create mode 100644 overlord/snapshotstate/backend/sizer.go create mode 100644 overlord/snapshotstate/export_test.go create mode 100644 overlord/snapshotstate/snapshotmgr.go create mode 100644 overlord/snapshotstate/snapshotmgr_test.go create mode 100644 overlord/snapshotstate/snapshotstate.go create mode 100644 overlord/snapshotstate/snapshotstate_test.go create mode 100644 overlord/snapstate/aliasesv2.go create mode 100644 overlord/snapstate/aliasesv2_test.go create mode 100644 overlord/snapstate/autorefresh.go create mode 100644 overlord/snapstate/autorefresh_test.go create mode 100644 overlord/snapstate/backend.go create mode 100644 overlord/snapstate/backend/aliases.go create mode 100644 overlord/snapstate/backend/aliases_test.go create mode 100644 overlord/snapstate/backend/backend.go create mode 100644 overlord/snapstate/backend/backend_test.go create mode 100644 overlord/snapstate/backend/copydata.go create mode 100644 overlord/snapstate/backend/copydata_test.go create mode 100644 overlord/snapstate/backend/export_test.go create mode 100644 overlord/snapstate/backend/fontconfig.go create mode 100644 overlord/snapstate/backend/link.go create mode 100644 overlord/snapstate/backend/link_test.go create mode 100644 overlord/snapstate/backend/mountns.go create mode 100644 overlord/snapstate/backend/mountunit.go create mode 100644 overlord/snapstate/backend/mountunit_test.go create mode 100644 overlord/snapstate/backend/setup.go create mode 100644 overlord/snapstate/backend/setup_test.go create mode 100644 overlord/snapstate/backend/snapdata.go create mode 100644 overlord/snapstate/backend/snapdata_test.go create mode 100644 overlord/snapstate/backend/utils.go create mode 100644 overlord/snapstate/backend_test.go create mode 100644 overlord/snapstate/booted.go create mode 100644 overlord/snapstate/booted_test.go create mode 100644 overlord/snapstate/catalogrefresh.go create mode 100644 overlord/snapstate/catalogrefresh_test.go create mode 100644 overlord/snapstate/check_snap.go create mode 100644 overlord/snapstate/check_snap_test.go create mode 100644 overlord/snapstate/conflict.go create mode 100644 overlord/snapstate/cookies.go create mode 100644 overlord/snapstate/cookies_test.go create mode 100644 overlord/snapstate/export_test.go create mode 100644 overlord/snapstate/flags.go create mode 100644 overlord/snapstate/handlers.go create mode 100644 overlord/snapstate/handlers_aliasesv2_test.go create mode 100644 overlord/snapstate/handlers_discard_test.go create mode 100644 overlord/snapstate/handlers_download_test.go create mode 100644 overlord/snapstate/handlers_link_test.go create mode 100644 overlord/snapstate/handlers_mount_test.go create mode 100644 overlord/snapstate/handlers_prepare_test.go create mode 100644 overlord/snapstate/handlers_prereq_test.go create mode 100644 overlord/snapstate/progress.go create mode 100644 overlord/snapstate/progress_test.go create mode 100644 overlord/snapstate/readme.go create mode 100644 overlord/snapstate/readme_test.go create mode 100644 overlord/snapstate/refreshhints.go create mode 100644 overlord/snapstate/refreshhints_test.go create mode 100644 overlord/snapstate/snapmgr.go create mode 100644 overlord/snapstate/snapstate.go create mode 100644 overlord/snapstate/snapstate_test.go create mode 100644 overlord/snapstate/storehelpers.go create mode 100644 overlord/standby/export_test.go create mode 100644 overlord/standby/standby.go create mode 100644 overlord/standby/standby_test.go create mode 100644 overlord/state/change.go create mode 100644 overlord/state/change_test.go create mode 100644 overlord/state/export_test.go create mode 100644 overlord/state/state.go create mode 100644 overlord/state/state_test.go create mode 100644 overlord/state/task.go create mode 100644 overlord/state/task_test.go create mode 100644 overlord/state/taskrunner.go create mode 100644 overlord/state/taskrunner_test.go create mode 100644 overlord/state/warning.go create mode 100644 overlord/state/warning_test.go create mode 100644 overlord/stateengine.go create mode 100644 overlord/stateengine_test.go create mode 100644 overlord/unknowntask.go create mode 120000 packaging/amzn-2 create mode 100644 packaging/arch/PKGBUILD create mode 100644 packaging/arch/snapd.install create mode 100755 packaging/build-tools/go create mode 120000 packaging/centos-7 create mode 120000 packaging/fedora-25 create mode 120000 packaging/fedora-26 create mode 120000 packaging/fedora-27 create mode 120000 packaging/fedora-28 create mode 120000 packaging/fedora-29 create mode 120000 packaging/fedora-rawhide create mode 100644 packaging/fedora/snapd.spec create mode 120000 packaging/opensuse-15.0 create mode 120000 packaging/opensuse-42.1 create mode 120000 packaging/opensuse-42.2 create mode 120000 packaging/opensuse-42.3 create mode 120000 packaging/opensuse-tumbleweed create mode 100644 packaging/opensuse/permissions create mode 100644 packaging/opensuse/permissions.easy create mode 100644 packaging/opensuse/permissions.paranoid create mode 100644 packaging/opensuse/permissions.secure create mode 100644 packaging/opensuse/snapd-rpmlintrc create mode 100644 packaging/opensuse/snapd.changes create mode 100644 packaging/opensuse/snapd.spec create mode 100644 packaging/ubuntu-14.04/changelog create mode 120000 packaging/ubuntu-14.04/compat create mode 100644 packaging/ubuntu-14.04/control create mode 100644 packaging/ubuntu-14.04/copyright create mode 120000 packaging/ubuntu-14.04/golang-github-snapcore-snapd-dev.install create mode 100755 packaging/ubuntu-14.04/rules create mode 120000 packaging/ubuntu-14.04/snap-confine.maintscript create mode 100644 packaging/ubuntu-14.04/snap.mount.service create mode 120000 packaging/ubuntu-14.04/snapd.autoimport.udev create mode 100644 packaging/ubuntu-14.04/snapd.dirs create mode 100644 packaging/ubuntu-14.04/snapd.install create mode 100644 packaging/ubuntu-14.04/snapd.links create mode 120000 packaging/ubuntu-14.04/snapd.maintscript create mode 120000 packaging/ubuntu-14.04/snapd.manpages create mode 100644 packaging/ubuntu-14.04/snapd.postinst create mode 100644 packaging/ubuntu-14.04/snapd.postrm create mode 100644 packaging/ubuntu-14.04/snapd.prerm create mode 120000 packaging/ubuntu-14.04/source create mode 120000 packaging/ubuntu-14.04/tests/README.md create mode 100644 packaging/ubuntu-14.04/tests/control create mode 100644 packaging/ubuntu-14.04/tests/integrationtests create mode 120000 packaging/ubuntu-14.04/tests/testconfig.json create mode 100644 packaging/ubuntu-14.04/ubuntu-snappy-cli.dirs create mode 100644 packaging/ubuntu-16.04/changelog create mode 100644 packaging/ubuntu-16.04/compat create mode 100644 packaging/ubuntu-16.04/control create mode 100644 packaging/ubuntu-16.04/copyright create mode 100644 packaging/ubuntu-16.04/gbp.conf create mode 100644 packaging/ubuntu-16.04/golang-github-snapcore-snapd-dev.install create mode 100755 packaging/ubuntu-16.04/rules create mode 100644 packaging/ubuntu-16.04/snap-confine.maintscript create mode 100644 packaging/ubuntu-16.04/snapd.autoimport.udev create mode 100644 packaging/ubuntu-16.04/snapd.dirs create mode 100644 packaging/ubuntu-16.04/snapd.install create mode 100644 packaging/ubuntu-16.04/snapd.links create mode 100644 packaging/ubuntu-16.04/snapd.maintscript create mode 100644 packaging/ubuntu-16.04/snapd.manpages create mode 100644 packaging/ubuntu-16.04/snapd.postinst create mode 100644 packaging/ubuntu-16.04/snapd.postrm create mode 100644 packaging/ubuntu-16.04/source/format create mode 100644 packaging/ubuntu-16.04/source/options create mode 100644 packaging/ubuntu-16.04/tests/README.md create mode 100644 packaging/ubuntu-16.04/tests/control create mode 100644 packaging/ubuntu-16.04/tests/integrationtests create mode 100644 packaging/ubuntu-16.04/tests/testconfig.json create mode 100644 packaging/ubuntu-16.04/ubuntu-snappy-cli.dirs create mode 120000 packaging/ubuntu-16.10 create mode 120000 packaging/ubuntu-17.04 create mode 100644 partition/androidboot.go create mode 100644 partition/androidboot_test.go create mode 100644 partition/androidbootenv/androidbootenv.go create mode 100644 partition/androidbootenv/androidbootenv_test.go create mode 100644 partition/bootloader.go create mode 100644 partition/bootloader_test.go create mode 100644 partition/export_test.go create mode 100644 partition/grub.go create mode 100644 partition/grub_test.go create mode 100644 partition/grubenv/grubenv.go create mode 100644 partition/grubenv/grubenv_test.go create mode 100644 partition/uboot.go create mode 100644 partition/uboot_test.go create mode 100644 partition/ubootenv/env.go create mode 100644 partition/ubootenv/env_test.go create mode 100644 partition/ubootenv/export_test.go create mode 100644 parts/plugins/x_builddeb.py create mode 100644 po/af.po create mode 100644 po/am.po create mode 100644 po/ar.po create mode 100644 po/bs.po create mode 100644 po/ca.po create mode 100644 po/cs.po create mode 100644 po/cy.po create mode 100644 po/da.po create mode 100644 po/de.po create mode 100644 po/el.po create mode 100644 po/en_GB.po create mode 100644 po/eo.po create mode 100644 po/es.po create mode 100644 po/fi.po create mode 100644 po/fr.po create mode 100644 po/gl.po create mode 100644 po/hr.po create mode 100644 po/hu.po create mode 100644 po/ia.po create mode 100644 po/id.po create mode 100644 po/it.po create mode 100644 po/its/polkit.its create mode 100644 po/ja.po create mode 100644 po/km.po create mode 100644 po/ko.po create mode 100644 po/lt.po create mode 100644 po/ms.po create mode 100644 po/nb.po create mode 100644 po/nl.po create mode 100644 po/oc.po create mode 100644 po/pl.po create mode 100644 po/pt.po create mode 100644 po/pt_BR.po create mode 100644 po/ro.po create mode 100644 po/ru.po create mode 100644 po/sq.po create mode 100644 po/sv.po create mode 100644 po/th.po create mode 100644 po/tr.po create mode 100644 po/ug.po create mode 100644 po/uk.po create mode 100644 po/zh_CN.po create mode 100644 po/zh_TW.po create mode 100644 polkit/authority.go create mode 100644 polkit/pid_start_time.go create mode 100644 polkit/pid_start_time_test.go create mode 100644 progress/ansimeter.go create mode 100644 progress/ansimeter_test.go create mode 100644 progress/export_test.go create mode 100644 progress/progress.go create mode 100644 progress/progress_test.go create mode 100644 progress/progresstest/progresstest.go create mode 100755 release-tools/repack-debian-tarball.sh create mode 100644 release/apparmor.go create mode 100644 release/apparmor_test.go create mode 100644 release/export_test.go create mode 100644 release/release.go create mode 100644 release/release_test.go create mode 100644 release/seccomp.go create mode 100644 release/seccomp_test.go create mode 100644 release/selinux.go create mode 100644 release/selinux_test.go create mode 100755 run-checks create mode 100644 sanity/apparmor_lxd.go create mode 100644 sanity/apparmor_lxd_test.go create mode 100644 sanity/check.go create mode 100644 sanity/check_test.go create mode 100644 sanity/export_test.go create mode 100644 sanity/squashfs.go create mode 100644 sanity/squashfs_test.go create mode 100644 sanity/version.go create mode 100644 sanity/version_test.go create mode 100644 sanity/wsl.go create mode 100644 sanity/wsl_test.go create mode 100644 selinux/export_test.go create mode 100644 selinux/label.go create mode 100644 selinux/label_darwin.go create mode 100644 selinux/label_linux.go create mode 100644 selinux/label_linux_test.go create mode 100644 selinux/selinux_darwin.go create mode 100644 selinux/selinux_linux.go create mode 100644 selinux/selinux_linux_test.go create mode 100644 snap/broken.go create mode 100644 snap/broken_test.go create mode 100644 snap/channel.go create mode 100644 snap/channel_test.go create mode 100644 snap/container.go create mode 100644 snap/container_test.go create mode 100644 snap/epoch.go create mode 100644 snap/epoch_test.go create mode 100644 snap/errors.go create mode 100644 snap/export_test.go create mode 100644 snap/gadget.go create mode 100644 snap/gadget_test.go create mode 100644 snap/hooktypes.go create mode 100644 snap/implicit.go create mode 100644 snap/implicit_test.go create mode 100644 snap/info.go create mode 100644 snap/info_snap_yaml.go create mode 100644 snap/info_snap_yaml_test.go create mode 100644 snap/info_test.go create mode 100644 snap/pack/export_test.go create mode 100644 snap/pack/pack.go create mode 100644 snap/pack/pack_test.go create mode 100644 snap/restartcond.go create mode 100644 snap/restartcond_test.go create mode 100644 snap/revision.go create mode 100644 snap/revision_test.go create mode 100644 snap/seed_yaml.go create mode 100644 snap/seed_yaml_test.go create mode 100644 snap/snapdir/snapdir.go create mode 100644 snap/snapdir/snapdir_test.go create mode 100644 snap/snapenv/snapenv.go create mode 100644 snap/snapenv/snapenv_test.go create mode 100644 snap/snaptest/snaptest.go create mode 100644 snap/snaptest/snaptest_test.go create mode 100644 snap/squashfs/export_test.go create mode 100644 snap/squashfs/squashfs.go create mode 100644 snap/squashfs/squashfs_test.go create mode 100644 snap/squashfs/stat.go create mode 100644 snap/squashfs/stat_test.go create mode 100644 snap/types.go create mode 100644 snap/types_test.go create mode 100644 snap/validate.go create mode 100644 snap/validate_test.go create mode 100644 snapcraft.yaml create mode 100644 spdx/licenses.go create mode 100644 spdx/parser.go create mode 100644 spdx/parser_test.go create mode 100644 spdx/scanner.go create mode 100644 spdx/scanner_test.go create mode 100644 spdx/validate.go create mode 100755 spread-shellcheck create mode 100644 spread.yaml create mode 100644 store/auth.go create mode 100644 store/auth_test.go create mode 100644 store/cache.go create mode 100644 store/cache_test.go create mode 100644 store/details.go create mode 100644 store/details_v2.go create mode 100644 store/details_v2_test.go create mode 100644 store/download_test.go create mode 100644 store/errors.go create mode 100644 store/export_test.go create mode 100644 store/store.go create mode 100644 store/store_test.go create mode 100644 store/storetest/storetest.go create mode 100644 store/stringlist_test.go create mode 100644 store/userinfo.go create mode 100644 store/userinfo_test.go create mode 100644 strutil/chrorder.go create mode 100644 strutil/chrorder/main.go create mode 100644 strutil/ctrl16.go create mode 100644 strutil/ctrl17.go create mode 100644 strutil/limbuffer.go create mode 100644 strutil/limbuffer_test.go create mode 100644 strutil/map.go create mode 100644 strutil/map_test.go create mode 100644 strutil/matchcounter.go create mode 100644 strutil/matchcounter_benchmark_test.go create mode 100644 strutil/matchcounter_test.go create mode 100644 strutil/pathiter.go create mode 100644 strutil/pathiter_test.go create mode 100644 strutil/quantity/example_test.go create mode 100644 strutil/quantity/quantity.go create mode 100644 strutil/shlex/shlex.go create mode 100644 strutil/shlex/shlex_test.go create mode 100644 strutil/strutil.go create mode 100644 strutil/strutil_test.go create mode 100644 strutil/version.go create mode 100644 strutil/version_benchmark_test.go create mode 100644 strutil/version_test.go create mode 100644 systemd/escape.go create mode 100644 systemd/escape_test.go create mode 100644 systemd/export_test.go create mode 100644 systemd/journal.go create mode 100644 systemd/journal_test.go create mode 100644 systemd/sdnotify.go create mode 100644 systemd/sdnotify_test.go create mode 100644 systemd/systemd.go create mode 100644 systemd/systemd_test.go create mode 100644 tests/completion/data/files/a/a_thing.txt create mode 100644 tests/completion/data/files/b/b_thing.txt create mode 100644 tests/completion/data/files/b/c/b_c_thing.txt create mode 100644 tests/completion/data/files/d/d_thing.txt create mode 100644 tests/completion/data/files/thing.txt create mode 100644 tests/completion/data/hosts.txt create mode 100644 tests/completion/data/twisted/.just a hidden file create mode 100644 tests/completion/data/twisted/this is a file with spaces in it.doc create mode 100644 tests/completion/data/twisted/this isn't.innit create mode 100644 tests/completion/dirs.complete create mode 100644 tests/completion/dirs.sh create mode 100644 tests/completion/dirs.vars create mode 100644 tests/completion/files.complete create mode 100644 tests/completion/files.sh create mode 100644 tests/completion/files.vars create mode 100644 tests/completion/func.complete create mode 100644 tests/completion/func.sh create mode 100644 tests/completion/func.vars create mode 100644 tests/completion/funcarg.complete create mode 100644 tests/completion/funcarg.sh create mode 100644 tests/completion/funcarg.vars create mode 100644 tests/completion/funky.complete create mode 100644 tests/completion/funky.sh create mode 100644 tests/completion/funky.vars create mode 100644 tests/completion/funkyfunc.complete create mode 100644 tests/completion/funkyfunc.sh create mode 100644 tests/completion/funkyfunc.vars create mode 100644 tests/completion/hosts.complete create mode 100644 tests/completion/hosts.sh create mode 100644 tests/completion/hosts.vars create mode 100644 tests/completion/hosts_n_dirs.complete create mode 100644 tests/completion/hosts_n_dirs.sh create mode 100644 tests/completion/hosts_n_dirs.vars create mode 100644 tests/completion/indirect/task.exp create mode 100644 tests/completion/indirect/task.yaml create mode 100644 tests/completion/lib.exp0 create mode 100644 tests/completion/plain.complete create mode 100644 tests/completion/plain.sh create mode 100644 tests/completion/plain.vars create mode 100644 tests/completion/plain_plusdirs.complete create mode 100644 tests/completion/plain_plusdirs.sh create mode 100644 tests/completion/plain_plusdirs.vars create mode 100644 tests/completion/simple/task.exp create mode 100644 tests/completion/simple/task.yaml create mode 100644 tests/completion/snippets/task.exp create mode 100644 tests/completion/snippets/task.yaml create mode 100644 tests/completion/twisted.complete create mode 100644 tests/completion/twisted.sh create mode 100644 tests/completion/twisted.vars create mode 100644 tests/core18/basic/task.yaml create mode 100644 tests/core18/compat/task.yaml create mode 100644 tests/core18/kernel/task.yaml create mode 100644 tests/core18/remove/task.yaml create mode 100644 tests/core18/snapd-failover/task.yaml create mode 100644 tests/core18/snapd-refresh/task.yaml create mode 100644 tests/cross/go-build/task.yaml create mode 100644 tests/external-backend.md create mode 100644 tests/lib/assertions/auto-import.assert create mode 100644 tests/lib/assertions/auto-import.assert.json create mode 100644 tests/lib/assertions/developer1-my-classic-w-gadget.model create mode 100644 tests/lib/assertions/developer1-my-classic.model create mode 100644 tests/lib/assertions/developer1-pc-w-config.model create mode 100644 tests/lib/assertions/developer1-pc.model create mode 100644 tests/lib/assertions/developer1.account create mode 100644 tests/lib/assertions/developer1.account-key create mode 100644 tests/lib/assertions/fake.store create mode 100644 tests/lib/assertions/nested-amd64.model create mode 100644 tests/lib/assertions/nested-amd64.model.json create mode 100644 tests/lib/assertions/nested-i386.model create mode 100644 tests/lib/assertions/nested-i386.model.json create mode 100644 tests/lib/assertions/pc-production.model create mode 100644 tests/lib/assertions/pc-staging.model create mode 100644 tests/lib/assertions/pi2.model create mode 100644 tests/lib/assertions/pi2.model.json create mode 100644 tests/lib/assertions/testrootorg-store.account-key create mode 100644 tests/lib/assertions/ubuntu-core-18-amd64.model create mode 100755 tests/lib/best_golang.py create mode 100644 tests/lib/boot.sh create mode 100755 tests/lib/changes.sh create mode 100755 tests/lib/cla_check.py create mode 100755 tests/lib/dbus.sh create mode 100644 tests/lib/dirs.sh create mode 100755 tests/lib/external/prepare-ssh.sh create mode 100644 tests/lib/fakedevicesvc/main.go create mode 100755 tests/lib/fakegpio/fake-gpio.py create mode 100644 tests/lib/fakestore/cmd/fakestore/cmd_make_refreshable.go create mode 100644 tests/lib/fakestore/cmd/fakestore/cmd_new_snap_decl.go create mode 100644 tests/lib/fakestore/cmd/fakestore/cmd_new_snap_rev.go create mode 100644 tests/lib/fakestore/cmd/fakestore/cmd_run.go create mode 100644 tests/lib/fakestore/cmd/fakestore/main.go create mode 100644 tests/lib/fakestore/refresh/refresh.go create mode 100644 tests/lib/fakestore/refresh/snap_asserts.go create mode 100644 tests/lib/fakestore/store/store.go create mode 100644 tests/lib/fakestore/store/store_test.go create mode 100644 tests/lib/files.sh create mode 100644 tests/lib/journalctl.sh create mode 100644 tests/lib/list-interfaces.go create mode 100755 tests/lib/mkpinentry.sh create mode 100644 tests/lib/names.sh create mode 100644 tests/lib/nested.sh create mode 100644 tests/lib/network.sh create mode 100644 tests/lib/os-release.16 create mode 100755 tests/lib/pinentry-fake.sh create mode 100755 tests/lib/pkgdb.sh create mode 100755 tests/lib/prepare-restore.sh create mode 100755 tests/lib/prepare.sh create mode 100644 tests/lib/quiet.sh create mode 100644 tests/lib/ramdisk.sh create mode 100644 tests/lib/random.sh create mode 100755 tests/lib/reset.sh create mode 100644 tests/lib/snaps.sh create mode 100755 tests/lib/snaps/account-control-consumer-core18/bin/chpasswd create mode 100755 tests/lib/snaps/account-control-consumer-core18/bin/deluser create mode 100755 tests/lib/snaps/account-control-consumer-core18/bin/useradd create mode 100644 tests/lib/snaps/account-control-consumer-core18/meta/snap.yaml create mode 100755 tests/lib/snaps/account-control-consumer/bin/chpasswd create mode 100755 tests/lib/snaps/account-control-consumer/bin/deluser create mode 100755 tests/lib/snaps/account-control-consumer/bin/useradd create mode 100644 tests/lib/snaps/account-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/aliases/bin/cmd1 create mode 100755 tests/lib/snaps/aliases/bin/cmd2 create mode 100644 tests/lib/snaps/aliases/meta/icon.png create mode 100644 tests/lib/snaps/aliases/meta/snap.yaml create mode 100755 tests/lib/snaps/basic-desktop/bin/echo create mode 100644 tests/lib/snaps/basic-desktop/meta/gui/echo.desktop create mode 100644 tests/lib/snaps/basic-desktop/meta/gui/icon.png create mode 100644 tests/lib/snaps/basic-desktop/meta/gui/io.snapcraft.echoecho.desktop create mode 100644 tests/lib/snaps/basic-desktop/meta/snap.yaml create mode 100755 tests/lib/snaps/basic-hooks/meta/hooks/configure create mode 100755 tests/lib/snaps/basic-hooks/meta/hooks/invalid-hook create mode 100644 tests/lib/snaps/basic-hooks/meta/icon.png create mode 100644 tests/lib/snaps/basic-hooks/meta/snap.yaml create mode 100755 tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/configure create mode 100755 tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/connect-plug-consumer create mode 100755 tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/disconnect-plug-consumer create mode 100755 tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/prepare-plug-consumer create mode 100755 tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/unprepare-plug-consumer create mode 100644 tests/lib/snaps/basic-iface-hooks-consumer/meta/icon.png create mode 100644 tests/lib/snaps/basic-iface-hooks-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/configure create mode 100755 tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/connect-slot-producer create mode 100755 tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/disconnect-slot-producer create mode 100755 tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/prepare-slot-producer create mode 100755 tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/unprepare-slot-producer create mode 100644 tests/lib/snaps/basic-iface-hooks-producer/meta/icon.png create mode 100644 tests/lib/snaps/basic-iface-hooks-producer/meta/snap.yaml create mode 100755 tests/lib/snaps/basic-run/bin/echo create mode 100644 tests/lib/snaps/basic-run/meta/snap.yaml create mode 100644 tests/lib/snaps/basic/meta/icon.png create mode 100644 tests/lib/snaps/basic/meta/snap.yaml create mode 100755 tests/lib/snaps/browser-support-consumer/bin/cmd create mode 100644 tests/lib/snaps/browser-support-consumer/meta/snap.yaml.in create mode 100644 tests/lib/snaps/classic-gadget/meta/gadget.yaml create mode 100755 tests/lib/snaps/classic-gadget/meta/hooks/prepare-device create mode 100644 tests/lib/snaps/classic-gadget/meta/icon.png create mode 100644 tests/lib/snaps/classic-gadget/meta/snap.yaml create mode 100755 tests/lib/snaps/command-chain/chain1 create mode 100755 tests/lib/snaps/command-chain/chain2 create mode 100755 tests/lib/snaps/command-chain/hello create mode 100755 tests/lib/snaps/command-chain/meta/hooks/configure create mode 100644 tests/lib/snaps/command-chain/meta/icon.png create mode 100644 tests/lib/snaps/command-chain/meta/snap.yaml create mode 100755 tests/lib/snaps/config-versions-v2/bin/sh create mode 100755 tests/lib/snaps/config-versions-v2/meta/hooks/configure create mode 100644 tests/lib/snaps/config-versions-v2/meta/icon.png create mode 100644 tests/lib/snaps/config-versions-v2/meta/snap.yaml create mode 100755 tests/lib/snaps/config-versions/bin/sh create mode 100755 tests/lib/snaps/config-versions/meta/hooks/configure create mode 100644 tests/lib/snaps/config-versions/meta/icon.png create mode 100644 tests/lib/snaps/config-versions/meta/snap.yaml create mode 100755 tests/lib/snaps/data-writer/bin/write-data create mode 100644 tests/lib/snaps/data-writer/meta/icon.png create mode 100644 tests/lib/snaps/data-writer/meta/snap.yaml create mode 100755 tests/lib/snaps/failing-config-hooks/meta/hooks/configure create mode 100644 tests/lib/snaps/failing-config-hooks/meta/icon.png create mode 100644 tests/lib/snaps/failing-config-hooks/meta/snap.yaml create mode 100755 tests/lib/snaps/firewall-control-consumer/bin/consumer create mode 100644 tests/lib/snaps/firewall-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/generic-consumer/bin/cmd create mode 100644 tests/lib/snaps/generic-consumer/meta/snap.yaml.in create mode 100755 tests/lib/snaps/gpio-consumer/bin/read create mode 100644 tests/lib/snaps/gpio-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/hardware-observe-consumer/bin/consumer create mode 100644 tests/lib/snaps/hardware-observe-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/home-consumer/bin/reader create mode 100755 tests/lib/snaps/home-consumer/bin/writer create mode 100644 tests/lib/snaps/home-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/iio-consumer/bin/read create mode 100755 tests/lib/snaps/iio-consumer/bin/write create mode 100644 tests/lib/snaps/iio-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/locale-control-consumer/bin/get create mode 100755 tests/lib/snaps/locale-control-consumer/bin/set create mode 100644 tests/lib/snaps/locale-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/log-observe-consumer/bin/cmd create mode 100755 tests/lib/snaps/log-observe-consumer/bin/consumer create mode 100644 tests/lib/snaps/log-observe-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/modem-manager-consumer/bin/consumer create mode 100644 tests/lib/snaps/modem-manager-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/mount-observe-consumer/bin/consumer create mode 100644 tests/lib/snaps/mount-observe-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/network-bind-consumer/bin/consumer create mode 100644 tests/lib/snaps/network-bind-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/network-consumer/bin/consumer create mode 100644 tests/lib/snaps/network-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/network-control-consumer/bin/cmd create mode 100644 tests/lib/snaps/network-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/network-observe-consumer/bin/consumer create mode 100644 tests/lib/snaps/network-observe-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/process-control-consumer/bin/signal create mode 100644 tests/lib/snaps/process-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/shutdown-introspection-consumer/bin/consumer create mode 100644 tests/lib/snaps/shutdown-introspection-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/snap-hooks-bad-install/meta/hooks/install create mode 100644 tests/lib/snaps/snap-hooks-bad-install/meta/snap.yaml create mode 100755 tests/lib/snaps/snap-hooks-bad-install/true create mode 100755 tests/lib/snaps/snap-hooks/meta/hooks/configure create mode 100755 tests/lib/snaps/snap-hooks/meta/hooks/install create mode 100755 tests/lib/snaps/snap-hooks/meta/hooks/post-refresh create mode 100755 tests/lib/snaps/snap-hooks/meta/hooks/pre-refresh create mode 100755 tests/lib/snaps/snap-hooks/meta/hooks/remove create mode 100644 tests/lib/snaps/snap-hooks/meta/snap.yaml create mode 100755 tests/lib/snaps/snap-hooks/true create mode 100755 tests/lib/snaps/snap-install-hook-broken/meta/hooks/install create mode 100644 tests/lib/snaps/snap-install-hook-broken/meta/snap.yaml create mode 100755 tests/lib/snaps/snap-store/bin/snap-store create mode 100644 tests/lib/snaps/snap-store/meta/snap.yaml create mode 100755 tests/lib/snaps/snapctl-from-snap-core18/bin/snapctl-get create mode 100755 tests/lib/snaps/snapctl-from-snap-core18/bin/snapctl-set create mode 100755 tests/lib/snaps/snapctl-from-snap-core18/meta/hooks/configure create mode 100644 tests/lib/snaps/snapctl-from-snap-core18/meta/snap.yaml create mode 100755 tests/lib/snaps/snapctl-from-snap/bin/snapctl-get create mode 100755 tests/lib/snaps/snapctl-from-snap/bin/snapctl-set create mode 100755 tests/lib/snaps/snapctl-from-snap/meta/hooks/configure create mode 100644 tests/lib/snaps/snapctl-from-snap/meta/snap.yaml create mode 100755 tests/lib/snaps/snapctl-hooks-v2/meta/hooks/configure create mode 100644 tests/lib/snaps/snapctl-hooks-v2/meta/icon.png create mode 100644 tests/lib/snaps/snapctl-hooks-v2/meta/snap.yaml create mode 100755 tests/lib/snaps/snapctl-hooks/meta/hooks/configure create mode 100644 tests/lib/snaps/snapctl-hooks/meta/icon.png create mode 100644 tests/lib/snaps/snapctl-hooks/meta/snap.yaml create mode 100755 tests/lib/snaps/socket-activation/bin/sleep create mode 100644 tests/lib/snaps/socket-activation/meta/snap.yaml create mode 100755 tests/lib/snaps/test-classic-cgroup/bin/read-fb create mode 100755 tests/lib/snaps/test-classic-cgroup/bin/read-kmsg create mode 100644 tests/lib/snaps/test-classic-cgroup/meta/snap.yaml create mode 100755 tests/lib/snaps/test-devmode-cgroup/bin/read-dev create mode 100644 tests/lib/snaps/test-devmode-cgroup/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-accounts-service/list-accounts.c create mode 100644 tests/lib/snaps/test-snapd-accounts-service/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-adb-support/bin/sh create mode 100644 tests/lib/snaps/test-snapd-adb-support/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-after-before-service/bin/start create mode 100644 tests/lib/snaps/test-snapd-after-before-service/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-appstreamid/bin/run create mode 100644 tests/lib/snaps/test-snapd-appstreamid/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-auto-aliases/bin/wellknown1 create mode 100755 tests/lib/snaps/test-snapd-auto-aliases/bin/wellknown2 create mode 100644 tests/lib/snaps/test-snapd-auto-aliases/meta/icon.png create mode 100644 tests/lib/snaps/test-snapd-auto-aliases/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-autopilot-consumer/consumer create mode 100644 tests/lib/snaps/test-snapd-autopilot-consumer/provider.py create mode 100644 tests/lib/snaps/test-snapd-autopilot-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-autopilot-consumer/wrapper create mode 100644 tests/lib/snaps/test-snapd-base-bare/Makefile create mode 100644 tests/lib/snaps/test-snapd-base-bare/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-base/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-base/random-file create mode 100644 tests/lib/snaps/test-snapd-busybox-static/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-classic-confinement/bin/classic-confinement create mode 100755 tests/lib/snaps/test-snapd-classic-confinement/bin/recurse create mode 100755 tests/lib/snaps/test-snapd-classic-confinement/bin/sh create mode 100644 tests/lib/snaps/test-snapd-classic-confinement/meta/icon.png create mode 100644 tests/lib/snaps/test-snapd-classic-confinement/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-complexion/bin/test-snapd-complexion create mode 100644 tests/lib/snaps/test-snapd-complexion/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-complexion/test-snapd-complexion.bash-completer create mode 100755 tests/lib/snaps/test-snapd-content-advanced-plug/bin/sh create mode 100644 tests/lib/snaps/test-snapd-content-advanced-plug/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-content-advanced-slot/bin/sh create mode 100644 tests/lib/snaps/test-snapd-content-advanced-slot/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-advanced-slot/source/canary create mode 100755 tests/lib/snaps/test-snapd-content-circular1/bin/content-plug create mode 100644 tests/lib/snaps/test-snapd-content-circular1/import/.placeholder create mode 100644 tests/lib/snaps/test-snapd-content-circular1/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-content-circular2/bin/content-plug create mode 100644 tests/lib/snaps/test-snapd-content-circular2/import/.placeholder create mode 100644 tests/lib/snaps/test-snapd-content-circular2/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-content-mimic-plug/bin/sh create mode 100644 tests/lib/snaps/test-snapd-content-mimic-plug/dir/stuff-in-dir create mode 100644 tests/lib/snaps/test-snapd-content-mimic-plug/file create mode 100644 tests/lib/snaps/test-snapd-content-mimic-plug/meta/snap.yaml create mode 120000 tests/lib/snaps/test-snapd-content-mimic-plug/symlink create mode 100644 tests/lib/snaps/test-snapd-content-mimic-plug/symlink-target create mode 100755 tests/lib/snaps/test-snapd-content-mimic-slot/bin/sh create mode 100644 tests/lib/snaps/test-snapd-content-mimic-slot/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-mimic-slot/source/canary create mode 100755 tests/lib/snaps/test-snapd-content-plug-no-content-attr/bin/content-plug create mode 100644 tests/lib/snaps/test-snapd-content-plug-no-content-attr/import/.placeholder create mode 100644 tests/lib/snaps/test-snapd-content-plug-no-content-attr/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-content-plug/bin/content-plug create mode 100644 tests/lib/snaps/test-snapd-content-plug/import/.placeholder create mode 100644 tests/lib/snaps/test-snapd-content-plug/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-slot-no-content-attr/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-slot-no-content-attr/shared-content create mode 100644 tests/lib/snaps/test-snapd-content-slot/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-slot/shared-content create mode 100644 tests/lib/snaps/test-snapd-content-slot2/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-slot2/shared-content create mode 100755 tests/lib/snaps/test-snapd-control-consumer/bin/install create mode 100755 tests/lib/snaps/test-snapd-control-consumer/bin/list create mode 100644 tests/lib/snaps/test-snapd-control-consumer/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-cups-control-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-daemon-notify/bin/notify create mode 100644 tests/lib/snaps/test-snapd-daemon-notify/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-dbus-consumer/consumer.py create mode 100644 tests/lib/snaps/test-snapd-dbus-consumer/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-dbus-provider/provider.py create mode 100644 tests/lib/snaps/test-snapd-dbus-provider/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-dbus-provider/wrapper create mode 100755 tests/lib/snaps/test-snapd-desktop/bin/check-dirs create mode 100755 tests/lib/snaps/test-snapd-desktop/bin/check-files create mode 100644 tests/lib/snaps/test-snapd-desktop/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-devmode/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-devmode/true create mode 100755 tests/lib/snaps/test-snapd-devpts/bin/openpty create mode 100755 tests/lib/snaps/test-snapd-devpts/bin/useptmx create mode 100644 tests/lib/snaps/test-snapd-devpts/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-eds/calendar.c create mode 100644 tests/lib/snaps/test-snapd-eds/contacts.c create mode 100644 tests/lib/snaps/test-snapd-eds/meson.build create mode 100644 tests/lib/snaps/test-snapd-eds/snap/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-epoch-1/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-epoch-2/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-event/bin/read-evdev-device create mode 100644 tests/lib/snaps/test-snapd-event/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-framebuffer/bin/read create mode 100755 tests/lib/snaps/test-snapd-framebuffer/bin/write create mode 100644 tests/lib/snaps/test-snapd-framebuffer/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-fuse-consumer/Makefile create mode 100644 tests/lib/snaps/test-snapd-fuse-consumer/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-go-webserver/main.go create mode 100644 tests/lib/snaps/test-snapd-go-webserver/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-gpio-memory-control/Makefile create mode 100644 tests/lib/snaps/test-snapd-gpio-memory-control/gpiomem.c create mode 100644 tests/lib/snaps/test-snapd-gpio-memory-control/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-hardware-random-control/bin/check create mode 100644 tests/lib/snaps/test-snapd-hardware-random-control/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-hardware-random-observe/bin/check create mode 100644 tests/lib/snaps/test-snapd-hardware-random-observe/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-hello-classic/Makefile create mode 100644 tests/lib/snaps/test-snapd-hello-classic/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-hello-classic/test-snapd-hello-classic.c create mode 100755 tests/lib/snaps/test-snapd-just-beta/snap-name create mode 100644 tests/lib/snaps/test-snapd-just-beta/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-just-edge/snap-name create mode 100644 tests/lib/snaps/test-snapd-just-edge/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-kernel-module-control-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-layout/bin/sh create mode 100644 tests/lib/snaps/test-snapd-layout/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-layout/opt/demo/file create mode 100644 tests/lib/snaps/test-snapd-layout/usr/share/demo/file create mode 100755 tests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-down create mode 100755 tests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-up create mode 100644 tests/lib/snaps/test-snapd-libvirt-consumer/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-libvirt-consumer/vm/ping-unikernel.xml create mode 100755 tests/lib/snaps/test-snapd-location-control-provider/consumer create mode 100644 tests/lib/snaps/test-snapd-location-control-provider/provider.py create mode 100644 tests/lib/snaps/test-snapd-location-control-provider/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-location-control-provider/wrapper create mode 100755 tests/lib/snaps/test-snapd-lp-1803535/bin/sh create mode 100644 tests/lib/snaps/test-snapd-lp-1803535/etc/OpenCL/vendors/foo.icd create mode 100644 tests/lib/snaps/test-snapd-lp-1803535/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-multi-service/bin/start create mode 100644 tests/lib/snaps/test-snapd-multi-service/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-netlink-audit/bin/bind create mode 100644 tests/lib/snaps/test-snapd-netlink-audit/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-netlink-connector/bin/bind create mode 100644 tests/lib/snaps/test-snapd-netlink-connector/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-network-status-provider/consumer create mode 100644 tests/lib/snaps/test-snapd-network-status-provider/provider.py create mode 100644 tests/lib/snaps/test-snapd-network-status-provider/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-network-status-provider/wrapper create mode 100644 tests/lib/snaps/test-snapd-number-version/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-openvswitch-consumer/bin/ovs-vsctl create mode 100644 tests/lib/snaps/test-snapd-openvswitch-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-openvswitch-support/random-uuid create mode 100644 tests/lib/snaps/test-snapd-openvswitch-support/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-password-manager-service-consumer/bin/secret-tool create mode 100644 tests/lib/snaps/test-snapd-password-manager-service-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-physical-memory-observe/bin/head-mem create mode 100644 tests/lib/snaps/test-snapd-physical-memory-observe/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-policy-app-consumer/bin/run create mode 100644 tests/lib/snaps/test-snapd-policy-app-consumer/meta/gui/test-desktop.desktop create mode 100644 tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-policy-app-provider-classic/bin/run create mode 100644 tests/lib/snaps/test-snapd-policy-app-provider-classic/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-policy-app-provider-core/bin/run create mode 100644 tests/lib/snaps/test-snapd-policy-app-provider-core/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-private/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-public/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-python-webserver/index.html create mode 100755 tests/lib/snaps/test-snapd-python-webserver/server.py create mode 100644 tests/lib/snaps/test-snapd-python-webserver/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-requires-base-bare/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-requires-base/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-rsync/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-service-try-v1/bin/service create mode 100644 tests/lib/snaps/test-snapd-service-try-v1/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-service-try-v2/bin/service create mode 100644 tests/lib/snaps/test-snapd-service-try-v2/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-service-v1-good/bin/good create mode 100644 tests/lib/snaps/test-snapd-service-v1-good/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-service-v2-bad/bin/bad create mode 100644 tests/lib/snaps/test-snapd-service-v2-bad/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-service-watchdog/bin/direct create mode 100644 tests/lib/snaps/test-snapd-service-watchdog/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-service/bin/reload create mode 100755 tests/lib/snaps/test-snapd-service/bin/start create mode 100755 tests/lib/snaps/test-snapd-service/bin/start-other create mode 100755 tests/lib/snaps/test-snapd-service/bin/start-stop-mode create mode 100755 tests/lib/snaps/test-snapd-service/bin/start-stop-mode-sigterm create mode 100755 tests/lib/snaps/test-snapd-service/bin/stop create mode 100755 tests/lib/snaps/test-snapd-service/bin/stop-stop-mode create mode 100755 tests/lib/snaps/test-snapd-service/meta/hooks/configure create mode 100644 tests/lib/snaps/test-snapd-service/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-sh-core16/bin/sh create mode 100644 tests/lib/snaps/test-snapd-sh-core16/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-sh/bin/sh create mode 100644 tests/lib/snaps/test-snapd-sh/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-simple-service/bin/service create mode 100644 tests/lib/snaps/test-snapd-simple-service/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-snapctl-core18/bin/service create mode 100755 tests/lib/snaps/test-snapd-snapctl-core18/meta/hooks/install create mode 100644 tests/lib/snaps/test-snapd-snapctl-core18/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-statx/bin/statx.py create mode 100644 tests/lib/snaps/test-snapd-statx/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-svcs-disable-install-hook/bin/forking.sh create mode 100755 tests/lib/snaps/test-snapd-svcs-disable-install-hook/bin/simple.sh create mode 100755 tests/lib/snaps/test-snapd-svcs-disable-install-hook/meta/hooks/install create mode 100644 tests/lib/snaps/test-snapd-svcs-disable-install-hook/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-system-observe-consumer/consumer.py create mode 100755 tests/lib/snaps/test-snapd-system-observe-consumer/dbus-introspect.py create mode 100644 tests/lib/snaps/test-snapd-system-observe-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-timedate-control-consumer/bin/hwclock create mode 100755 tests/lib/snaps/test-snapd-timedate-control-consumer/bin/timedatectl create mode 100644 tests/lib/snaps/test-snapd-timedate-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-timer-service/bin/loop create mode 100644 tests/lib/snaps/test-snapd-timer-service/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-tools-core18/bin/block create mode 100755 tests/lib/snaps/test-snapd-tools-core18/bin/cat create mode 100755 tests/lib/snaps/test-snapd-tools-core18/bin/cmd create mode 100755 tests/lib/snaps/test-snapd-tools-core18/bin/echo create mode 100755 tests/lib/snaps/test-snapd-tools-core18/bin/env create mode 100755 tests/lib/snaps/test-snapd-tools-core18/bin/fail create mode 100755 tests/lib/snaps/test-snapd-tools-core18/bin/head create mode 100755 tests/lib/snaps/test-snapd-tools-core18/bin/sh create mode 100755 tests/lib/snaps/test-snapd-tools-core18/bin/success create mode 100644 tests/lib/snaps/test-snapd-tools-core18/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-tools/bin/block create mode 100755 tests/lib/snaps/test-snapd-tools/bin/cat create mode 100755 tests/lib/snaps/test-snapd-tools/bin/cmd create mode 100755 tests/lib/snaps/test-snapd-tools/bin/echo create mode 100755 tests/lib/snaps/test-snapd-tools/bin/env create mode 100755 tests/lib/snaps/test-snapd-tools/bin/fail create mode 100755 tests/lib/snaps/test-snapd-tools/bin/head create mode 100755 tests/lib/snaps/test-snapd-tools/bin/sh create mode 100755 tests/lib/snaps/test-snapd-tools/bin/success create mode 100644 tests/lib/snaps/test-snapd-tools/meta/icon.png create mode 100644 tests/lib/snaps/test-snapd-tools/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-tuntap/bin/tuntap.py create mode 100644 tests/lib/snaps/test-snapd-tuntap/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-udev-input-subsystem/bin/read-evdev-kbd create mode 100644 tests/lib/snaps/test-snapd-udev-input-subsystem/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-udisks2/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-udisks2/udisksctl create mode 100644 tests/lib/snaps/test-snapd-uhid/Makefile create mode 100644 tests/lib/snaps/test-snapd-uhid/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-uhid/uhid-test.c create mode 100755 tests/lib/snaps/test-snapd-unknown-interfaces/bin/sh create mode 100644 tests/lib/snaps/test-snapd-unknown-interfaces/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-upower-observe-consumer/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-validate-container-failures/bin/bar create mode 100755 tests/lib/snaps/test-snapd-validate-container-failures/bin/foo create mode 100644 tests/lib/snaps/test-snapd-validate-container-failures/comp.sh create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/bar create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/bar -> baz create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/bar -> baz -> qux create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/bar -> qux create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/baz create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/baz -> qux create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/foo create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/foo -> bar create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/foo -> bar -> baz create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/foo -> bar -> qux create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/foo -> baz create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/foo -> baz -> qux create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/foo -> qux create mode 120000 tests/lib/snaps/test-snapd-validate-container-failures/hell/qux create mode 100644 tests/lib/snaps/test-snapd-validate-container-failures/meta/hooks/what create mode 100644 tests/lib/snaps/test-snapd-validate-container-failures/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-validate-container-failures/meta/unreadable create mode 100755 tests/lib/snaps/test-snapd-with-configure-core18/meta/hooks/configure create mode 100644 tests/lib/snaps/test-snapd-with-configure-core18/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-with-configure-nc/meta/hooks/configure create mode 100644 tests/lib/snaps/test-snapd-with-configure-nc/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-with-configure/meta/hooks/configure create mode 100644 tests/lib/snaps/test-snapd-with-configure/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-xdg-autostart/bin/foobar create mode 100644 tests/lib/snaps/test-snapd-xdg-autostart/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-xdg-settings/bin/browser create mode 100755 tests/lib/snaps/test-snapd-xdg-settings/bin/set-default-web-browser create mode 100644 tests/lib/snaps/test-snapd-xdg-settings/meta/gui/browser.desktop create mode 100644 tests/lib/snaps/test-snapd-xdg-settings/meta/snap.yaml create mode 100755 tests/lib/snaps/test-strict-cgroup/bin/read-dev create mode 100644 tests/lib/snaps/test-strict-cgroup/meta/snap.yaml create mode 100644 tests/lib/spread-funcs.sh create mode 100755 tests/lib/state.sh create mode 100644 tests/lib/store.sh create mode 100644 tests/lib/strings.sh create mode 100644 tests/lib/successful_login.exp create mode 100644 tests/lib/systemd-escape/main.go create mode 100644 tests/lib/systemd.sh create mode 100644 tests/lib/systems.sh create mode 100644 tests/lib/tinyproxy/tinyproxy.py create mode 100644 tests/main/abort/task.yaml create mode 100644 tests/main/ack/alice.account create mode 100644 tests/main/ack/alice.account-key create mode 100644 tests/main/ack/bob.assertions create mode 100644 tests/main/ack/task.yaml create mode 100644 tests/main/alias/task.yaml create mode 100644 tests/main/appstream-id/task.yaml create mode 100644 tests/main/apt-hooks/task.yaml create mode 100644 tests/main/auth-errors/task.yaml create mode 100644 tests/main/auto-aliases/task.yaml create mode 100644 tests/main/auto-refresh-private/expired_macaroons.sh create mode 100644 tests/main/auto-refresh-private/successful_login.exp create mode 100644 tests/main/auto-refresh-private/task.yaml create mode 100644 tests/main/auto-refresh/task.yaml create mode 100644 tests/main/base-snaps-refresh/task.yaml create mode 100644 tests/main/base-snaps/task.yaml create mode 100644 tests/main/canonical-livepatch/task.yaml create mode 100644 tests/main/catalog-update/task.yaml create mode 100644 tests/main/cgroup-freezer/task.yaml create mode 100644 tests/main/change-errors/task.yaml create mode 100644 tests/main/chattr/task.yaml create mode 100644 tests/main/chattr/toggle.go create mode 100644 tests/main/classic-confinement-not-supported/task.yaml create mode 100644 tests/main/classic-confinement/task.yaml create mode 100644 tests/main/classic-custom-device-reg/task.yaml create mode 100644 tests/main/classic-firstboot/task.yaml create mode 100644 tests/main/classic-ubuntu-core-transition-auth/task.yaml create mode 100644 tests/main/classic-ubuntu-core-transition-two-cores/task.yaml create mode 100644 tests/main/classic-ubuntu-core-transition/task.yaml create mode 100644 tests/main/cloud-init/task.yaml create mode 100644 tests/main/cmdline/task.yaml create mode 100644 tests/main/command-chain/task.yaml create mode 100644 tests/main/completion/abort.exp create mode 100644 tests/main/completion/ack.exp create mode 100644 tests/main/completion/alias.exp create mode 100644 tests/main/completion/buy.exp create mode 100644 tests/main/completion/change.exp create mode 100644 tests/main/completion/delete-key.exp create mode 100644 tests/main/completion/disable.exp create mode 100644 tests/main/completion/download.exp create mode 100644 tests/main/completion/enable.exp create mode 100644 tests/main/completion/export-key.exp create mode 100644 tests/main/completion/get.exp create mode 100644 tests/main/completion/info.exp create mode 100644 tests/main/completion/install.exp create mode 100644 tests/main/completion/key.exp0 create mode 120000 tests/main/completion/lib.exp0 create mode 100644 tests/main/completion/list.exp create mode 100644 tests/main/completion/refresh.exp create mode 100644 tests/main/completion/remove.exp create mode 100644 tests/main/completion/revert.exp create mode 100644 tests/main/completion/set.exp create mode 100644 tests/main/completion/sign-build.exp create mode 100644 tests/main/completion/sign.exp create mode 100644 tests/main/completion/task.yaml create mode 100644 tests/main/completion/toplevel.exp create mode 100644 tests/main/completion/try.exp create mode 100644 tests/main/completion/watch.exp create mode 100644 tests/main/config-versions/task.yaml create mode 100644 tests/main/configure-hook-with-network-control/task.yaml create mode 100644 tests/main/confinement-classic/task.yaml create mode 100644 tests/main/core-snap-not-test-test/task.yaml create mode 100644 tests/main/core-snap-refresh-on-core/task.yaml create mode 100644 tests/main/core-snap-refresh/task.yaml create mode 100644 tests/main/core-watchdog/task.yaml create mode 100644 tests/main/core16-base/task.yaml create mode 100644 tests/main/core18-configure-hook/task.yaml create mode 100644 tests/main/core18-with-hooks/task.yaml create mode 100644 tests/main/create-key/passphrase_mismatch.exp create mode 100644 tests/main/create-key/successful_default.exp create mode 100644 tests/main/create-key/successful_non_default.exp create mode 100644 tests/main/create-key/task.yaml create mode 100644 tests/main/create-user/task.yaml create mode 100644 tests/main/debs-have-built-using/task.yaml create mode 100644 tests/main/debug-confinement/task.yaml create mode 100644 tests/main/debug-paths/task.yaml create mode 100644 tests/main/debug-sandbox/task.yaml create mode 100644 tests/main/degraded/task.yaml create mode 100644 tests/main/dirs-not-shared-with-host/task.yaml create mode 100644 tests/main/disable-autoconnect/task.yaml create mode 100755 tests/main/document-portal-activation/fake-document-portal.py create mode 100644 tests/main/document-portal-activation/task.yaml create mode 100644 tests/main/econnreset/task.yaml create mode 100644 tests/main/enable-disable-units-gpio/task.yaml create mode 100644 tests/main/enable-disable/task.yaml create mode 100644 tests/main/experimental-features/task.yaml create mode 100644 tests/main/failover/task.yaml create mode 100644 tests/main/fakestore-install/task.yaml create mode 100644 tests/main/fedora-base-smoke/task.yaml create mode 100644 tests/main/find-private/task.yaml create mode 100644 tests/main/generic-classic-reg/task.yaml create mode 100644 tests/main/help/task.yaml create mode 100644 tests/main/high-user-handling/task.yaml create mode 100644 tests/main/high-user-handling/test.go create mode 100644 tests/main/i18n/task.yaml create mode 100644 tests/main/install-cache/task.yaml create mode 100644 tests/main/install-closed-channel/task.yaml create mode 100644 tests/main/install-errors/task.yaml create mode 100644 tests/main/install-refresh-private/task.yaml create mode 100644 tests/main/install-refresh-remove-hooks/task.yaml create mode 100644 tests/main/install-remove-multi/task.yaml create mode 100644 tests/main/install-sideload-epochs/task.yaml create mode 100644 tests/main/install-sideload/task.yaml create mode 100644 tests/main/install-snaps/task.yaml create mode 100644 tests/main/install-socket-activation/task.yaml create mode 100644 tests/main/install-store-laaaarge/task.yaml create mode 100644 tests/main/install-store/task.yaml create mode 100644 tests/main/install/task.yaml create mode 100644 tests/main/interfaces-account-control/task.yaml create mode 100644 tests/main/interfaces-accounts-service/task.yaml create mode 100644 tests/main/interfaces-adb-support/task.yaml create mode 100644 tests/main/interfaces-alsa/task.yaml create mode 100644 tests/main/interfaces-autopilot-introspection/task.yaml create mode 100644 tests/main/interfaces-avahi-observe/task.yaml create mode 100644 tests/main/interfaces-bluetooth-control/task.yaml create mode 100644 tests/main/interfaces-bluez/task.yaml create mode 100644 tests/main/interfaces-broadcom-asic-control/task.yaml create mode 100644 tests/main/interfaces-browser-support/task.yaml create mode 100644 tests/main/interfaces-calendar-service/task.yaml create mode 100644 tests/main/interfaces-cli/task.yaml create mode 100644 tests/main/interfaces-contacts-service/task.yaml create mode 100644 tests/main/interfaces-content-circular/task.yaml create mode 100644 tests/main/interfaces-content-default-provider/task.yaml create mode 100644 tests/main/interfaces-content-empty-content-attr/task.yaml create mode 100644 tests/main/interfaces-content-mimic/task.yaml create mode 100644 tests/main/interfaces-content-mkdir-writable/task.yaml create mode 100644 tests/main/interfaces-content/task.yaml create mode 100644 tests/main/interfaces-cups-control/task.yaml create mode 100644 tests/main/interfaces-daemon-notify/task.yaml create mode 100644 tests/main/interfaces-dbus/task.yaml create mode 100644 tests/main/interfaces-desktop-document-portal/task.yaml create mode 100644 tests/main/interfaces-desktop-host-fonts/task.yaml create mode 100644 tests/main/interfaces-desktop/task.yaml create mode 100644 tests/main/interfaces-device-buttons/task.yaml create mode 100644 tests/main/interfaces-dvb/task.yaml create mode 100644 tests/main/interfaces-firewall-control/task.yaml create mode 100644 tests/main/interfaces-framebuffer/task.yaml create mode 100644 tests/main/interfaces-fuse_support/task.yaml create mode 100644 tests/main/interfaces-gpg-keys/task.yaml create mode 100644 tests/main/interfaces-gpg-public-keys/task.yaml create mode 100644 tests/main/interfaces-gpio-memory-control/task.yaml create mode 100644 tests/main/interfaces-hardware-observe/task.yaml create mode 100644 tests/main/interfaces-hardware-random-control/task.yaml create mode 100644 tests/main/interfaces-hardware-random-observe/task.yaml create mode 100644 tests/main/interfaces-home/task.yaml create mode 100644 tests/main/interfaces-hooks-misbehaving/task.yaml create mode 100644 tests/main/interfaces-hooks/task.yaml create mode 100644 tests/main/interfaces-hostname-control/task.yaml create mode 100644 tests/main/interfaces-iio/task.yaml create mode 100644 tests/main/interfaces-joystick/task.yaml create mode 100644 tests/main/interfaces-juju-client-observe/task.yaml create mode 100644 tests/main/interfaces-kernel-module-control/task.yaml create mode 100644 tests/main/interfaces-kvm/task.yaml create mode 100644 tests/main/interfaces-libvirt/task.yaml create mode 100644 tests/main/interfaces-locale-control/task.yaml create mode 100644 tests/main/interfaces-location-control/task.yaml create mode 100644 tests/main/interfaces-log-observe/task.yaml create mode 100644 tests/main/interfaces-many/task.yaml create mode 100644 tests/main/interfaces-mount-observe/task.yaml create mode 100644 tests/main/interfaces-netlink-audit/task.yaml create mode 100644 tests/main/interfaces-netlink-connector/task.yaml create mode 100644 tests/main/interfaces-network-bind/task.yaml create mode 100644 tests/main/interfaces-network-control-ip-netns/task.yaml create mode 100644 tests/main/interfaces-network-control-tuntap/task.yaml create mode 100644 tests/main/interfaces-network-control/task.yaml create mode 100644 tests/main/interfaces-network-manager/task.yaml create mode 100644 tests/main/interfaces-network-observe/task.yaml create mode 100644 tests/main/interfaces-network-setup-control/task.yaml create mode 100644 tests/main/interfaces-network-setup-observe/task.yaml create mode 100644 tests/main/interfaces-network-status/task.yaml create mode 100644 tests/main/interfaces-network/task.yaml create mode 100644 tests/main/interfaces-opengl-nvidia/task.yaml create mode 100644 tests/main/interfaces-openvswitch-support/task.yaml create mode 100644 tests/main/interfaces-openvswitch/task.yaml create mode 100644 tests/main/interfaces-password-manager-service/task.yaml create mode 100644 tests/main/interfaces-personal-files/task.yaml create mode 100644 tests/main/interfaces-physical-memory-observe/task.yaml create mode 100644 tests/main/interfaces-process-control/task.yaml create mode 100644 tests/main/interfaces-raw-usb/task.yaml create mode 100644 tests/main/interfaces-removable-media/task.yaml create mode 100644 tests/main/interfaces-shutdown-introspection/task.yaml create mode 100644 tests/main/interfaces-snapd-control-with-manage/task.yaml create mode 100644 tests/main/interfaces-snapd-control/task.yaml create mode 100644 tests/main/interfaces-ssh-keys/task.yaml create mode 100644 tests/main/interfaces-ssh-public-keys/task.yaml create mode 100644 tests/main/interfaces-system-files/task.yaml create mode 100644 tests/main/interfaces-system-observe/task.yaml create mode 100644 tests/main/interfaces-time-control/task.yaml create mode 100644 tests/main/interfaces-timeserver-control/task.yaml create mode 100644 tests/main/interfaces-timezone-control/task.yaml create mode 100644 tests/main/interfaces-udev/task.yaml create mode 100644 tests/main/interfaces-udisks2/task.yaml create mode 100644 tests/main/interfaces-uhid/task.yaml create mode 100644 tests/main/interfaces-upower-observe/task.yaml create mode 100644 tests/main/interfaces-wayland/task.yaml create mode 100644 tests/main/kernel-snap-refresh-on-core/task.yaml create mode 100644 tests/main/known-remote/task.yaml create mode 100644 tests/main/known/task.yaml create mode 100755 tests/main/layout-symlink-bind-revert/app.v1/bin/app create mode 100644 tests/main/layout-symlink-bind-revert/app.v1/meta/snap.yaml create mode 100644 tests/main/layout-symlink-bind-revert/app.v1/runtime/.keep create mode 100755 tests/main/layout-symlink-bind-revert/app.v2/bin/app create mode 100644 tests/main/layout-symlink-bind-revert/app.v2/meta/snap.yaml create mode 100644 tests/main/layout-symlink-bind-revert/app.v2/runtime/.keep create mode 100644 tests/main/layout-symlink-bind-revert/runtime/meta/snap.yaml create mode 100755 tests/main/layout-symlink-bind-revert/runtime/opt/runtime/runner create mode 100644 tests/main/layout-symlink-bind-revert/task.yaml create mode 100644 tests/main/layout/task.yaml create mode 100644 tests/main/listing/task.yaml create mode 100644 tests/main/local-install-w-metadata/digest.go create mode 100644 tests/main/local-install-w-metadata/task.yaml create mode 100644 tests/main/login/missing_email_error.exp create mode 100644 tests/main/login/task.yaml create mode 100644 tests/main/login/unsuccessful_login.exp create mode 100644 tests/main/lxd/task.yaml create mode 100644 tests/main/manpages/task.yaml create mode 100644 tests/main/media-sharing/task.yaml create mode 100644 tests/main/mount-protocol-error/task.yaml create mode 100644 tests/main/network-retry/task.yaml create mode 100644 tests/main/nfs-support/task.yaml create mode 100644 tests/main/op-install-failed-undone/task.yaml create mode 100644 tests/main/op-remove-retry/task.yaml create mode 100644 tests/main/op-remove/task.yaml create mode 100644 tests/main/parallel-install-aliases/task.yaml create mode 100644 tests/main/parallel-install-auto-aliases/task.yaml create mode 100644 tests/main/parallel-install-basic/task.yaml create mode 100644 tests/main/parallel-install-common-dirs-undo/task.yaml create mode 100644 tests/main/parallel-install-common-dirs/task.yaml create mode 100644 tests/main/parallel-install-desktop/task.yaml create mode 100644 tests/main/parallel-install-interfaces-content/task.yaml create mode 100644 tests/main/parallel-install-interfaces/task.yaml create mode 100644 tests/main/parallel-install-layout/task.yaml create mode 100644 tests/main/parallel-install-local/task.yaml create mode 100644 tests/main/parallel-install-services/task.yaml create mode 100644 tests/main/parallel-install-store/task.yaml create mode 100644 tests/main/postrm-purge/task.yaml create mode 100644 tests/main/prefer/task.yaml create mode 100644 tests/main/prepare-image-grub-core18/task.yaml create mode 100644 tests/main/prepare-image-grub/task.yaml create mode 100644 tests/main/prepare-image-uboot/task.yaml create mode 100644 tests/main/proxy-no-core/task.yaml create mode 100644 tests/main/proxy/task.yaml create mode 100644 tests/main/refresh-all-undo/task.yaml create mode 100644 tests/main/refresh-all/task.yaml create mode 100644 tests/main/refresh-amend/task.yaml create mode 100644 tests/main/refresh-delta-from-core/task.yaml create mode 100644 tests/main/refresh-delta/task.yaml create mode 100644 tests/main/refresh-devmode/task.yaml create mode 100644 tests/main/refresh-hold/task.yaml create mode 100644 tests/main/refresh-undo/task.yaml create mode 100644 tests/main/refresh/task.yaml create mode 100644 tests/main/regression-home-snap-root-owned/task.yaml create mode 100644 tests/main/remove-errors/task.yaml create mode 100644 tests/main/retryable-error/task.yaml create mode 100644 tests/main/revert-devmode/task.yaml create mode 100644 tests/main/revert-sideload/task.yaml create mode 100644 tests/main/revert/task.yaml create mode 100644 tests/main/sanitycheck/task.yaml create mode 100644 tests/main/searching/task.yaml create mode 100644 tests/main/seccomp-statx/task.yaml create mode 100644 tests/main/security-apparmor/task.yaml create mode 100644 tests/main/security-dev-input-event-denied/task.yaml create mode 100644 tests/main/security-device-cgroups-classic/task.yaml create mode 100644 tests/main/security-device-cgroups-devmode/task.yaml create mode 100644 tests/main/security-device-cgroups-jailmode/task.yaml create mode 100644 tests/main/security-device-cgroups-serial-port/task.yaml create mode 100644 tests/main/security-device-cgroups-strict/task.yaml create mode 100644 tests/main/security-device-cgroups/task.yaml create mode 100644 tests/main/security-devpts/task.yaml create mode 100644 tests/main/security-private-tmp/task.yaml create mode 100644 tests/main/security-private-tmp/tmp-create.exp create mode 100644 tests/main/security-profiles/task.yaml create mode 100644 tests/main/security-setuid-root/task.yaml create mode 100644 tests/main/security-udev-input-subsystem/task.yaml create mode 100644 tests/main/selinux-snap-restorecon/task.yaml create mode 100644 tests/main/server-snap/task.yaml create mode 100644 tests/main/set-proxy-store/task.yaml create mode 100644 tests/main/snap-advise-command/task.yaml create mode 100644 tests/main/snap-auto-import-asserts-spools/task.yaml create mode 100644 tests/main/snap-auto-import-asserts/task.yaml create mode 100644 tests/main/snap-auto-mount/task.yaml create mode 100644 tests/main/snap-confine-from-core/task.yaml create mode 100644 tests/main/snap-confine-privs/task.yaml create mode 100644 tests/main/snap-confine-privs/uids-and-gids.c create mode 100644 tests/main/snap-confine/task.yaml create mode 100644 tests/main/snap-connect/task.yaml create mode 100644 tests/main/snap-connectivity-check/task.yaml create mode 100644 tests/main/snap-core-fixup/task.yaml create mode 100644 tests/main/snap-core-fixup/test.img.xz create mode 100644 tests/main/snap-core-symlinks/task.yaml create mode 100644 tests/main/snap-debug-get-base-declaration/task.yaml create mode 100644 tests/main/snap-discard-ns/mount.py create mode 100755 tests/main/snap-discard-ns/mount.sh create mode 100644 tests/main/snap-discard-ns/task.yaml create mode 100644 tests/main/snap-disconnect/task.yaml create mode 100644 tests/main/snap-download/task.yaml create mode 100644 tests/main/snap-env/task.yaml create mode 100644 tests/main/snap-get/task.yaml create mode 100644 tests/main/snap-handle-link/task.yaml create mode 100644 tests/main/snap-info/check.py create mode 100644 tests/main/snap-info/task.yaml create mode 100644 tests/main/snap-interface/snap-interface-network-core.yaml create mode 100644 tests/main/snap-interface/snap-interface-network-snapd.yaml create mode 100644 tests/main/snap-interface/task.yaml create mode 100644 tests/main/snap-logs/task.yaml create mode 100644 tests/main/snap-mgmt/task.yaml create mode 100644 tests/main/snap-multi-service-failing/task.yaml create mode 100644 tests/main/snap-network-errors/task.yaml create mode 100644 tests/main/snap-readme/task.yaml create mode 100644 tests/main/snap-remove-not-mounted/task.yaml create mode 100644 tests/main/snap-repair/task.yaml create mode 100644 tests/main/snap-run-alias/task.yaml create mode 100644 tests/main/snap-run-hook/task.yaml create mode 100644 tests/main/snap-run-symlink-error/task.yaml create mode 100644 tests/main/snap-run-symlink/task.yaml create mode 100644 tests/main/snap-run-userdata-current/task.yaml create mode 100644 tests/main/snap-run/task.yaml create mode 100644 tests/main/snap-seccomp/task.yaml create mode 100644 tests/main/snap-service-after-before-install/task.yaml create mode 100644 tests/main/snap-service-after-before/task.yaml create mode 100644 tests/main/snap-service-refresh-mode/task.yaml create mode 100644 tests/main/snap-service-stop-mode-sigkill/task.yaml create mode 100644 tests/main/snap-service-stop-mode/task.yaml create mode 100644 tests/main/snap-service-timer/task.yaml create mode 100644 tests/main/snap-service-watchdog/task.yaml create mode 100644 tests/main/snap-service/task.yaml create mode 100644 tests/main/snap-set-core-w-no-core/task.yaml create mode 100644 tests/main/snap-set/task.yaml create mode 100644 tests/main/snap-sign/create-key.exp create mode 100644 tests/main/snap-sign/sign-model.exp create mode 100644 tests/main/snap-sign/task.yaml create mode 100644 tests/main/snap-switch/task.yaml create mode 100644 tests/main/snap-system-env/task.yaml create mode 100644 tests/main/snap-system-key/task.yaml create mode 100644 tests/main/snap-update-ns/task.yaml create mode 100644 tests/main/snap-userd-desktop-app-autostart/task.yaml create mode 100644 tests/main/snap-userd-reexec/task.yaml create mode 100644 tests/main/snap-wait/task.yaml create mode 100644 tests/main/snapctl-configure-core/task.yaml create mode 100644 tests/main/snapctl-from-snap/task.yaml create mode 100644 tests/main/snapctl-services/task.yaml create mode 100644 tests/main/snapctl/task.yaml create mode 100644 tests/main/snapd-go-socket-activated/task.yaml create mode 100644 tests/main/snapd-notify/task.yaml create mode 100644 tests/main/snapd-reexec-snapd-snap/task.yaml create mode 100644 tests/main/snapd-reexec/task.yaml create mode 100644 tests/main/snapd-snap/task.yaml create mode 100644 tests/main/snapshot-basic/task.yaml create mode 100644 tests/main/snapshot-cross-revno/task.yaml create mode 100644 tests/main/special-home-can-run-classic-snaps/task.yaml create mode 100644 tests/main/stale-base-snap/task.yaml create mode 100644 tests/main/static/task.yaml create mode 100644 tests/main/svcs-disable-install-hook/task.yaml create mode 100644 tests/main/system-core-alias/task.yaml create mode 100644 tests/main/systemd-service/task.yaml create mode 100644 tests/main/try-non-fatal/task.yaml create mode 100644 tests/main/try-snap-goes-away/task.yaml create mode 100644 tests/main/try-snap-is-optional/task.yaml create mode 100644 tests/main/try-twice-with-daemon/task.yaml create mode 100644 tests/main/try-with-hooks/task.yaml create mode 100644 tests/main/try/task.yaml create mode 100644 tests/main/ubuntu-core-apt/task.yaml create mode 100644 tests/main/ubuntu-core-classic/task.yaml create mode 100644 tests/main/ubuntu-core-create-user/task.yaml create mode 100644 tests/main/ubuntu-core-custom-device-reg-extras/manip_seed.py create mode 100755 tests/main/ubuntu-core-custom-device-reg-extras/prepare-device create mode 100644 tests/main/ubuntu-core-custom-device-reg-extras/task.yaml create mode 100644 tests/main/ubuntu-core-custom-device-reg/manip_seed.py create mode 100755 tests/main/ubuntu-core-custom-device-reg/prepare-device create mode 100644 tests/main/ubuntu-core-custom-device-reg/task.yaml create mode 100644 tests/main/ubuntu-core-device-reg/task.yaml create mode 100644 tests/main/ubuntu-core-fan/task.yaml create mode 100644 tests/main/ubuntu-core-gadget-config-defaults/manip_seed.py create mode 100644 tests/main/ubuntu-core-gadget-config-defaults/task.yaml create mode 100644 tests/main/ubuntu-core-grub/task.yaml create mode 100644 tests/main/ubuntu-core-network-config/task.yaml create mode 100644 tests/main/ubuntu-core-os-release/task.yaml create mode 100644 tests/main/ubuntu-core-reboot/task.yaml create mode 100644 tests/main/ubuntu-core-services/task.yaml create mode 100644 tests/main/ubuntu-core-uboot/task.yaml create mode 100644 tests/main/ubuntu-core-upgrade/task.yaml create mode 100644 tests/main/ubuntu-core-writablepaths/task.yaml create mode 100644 tests/main/unhandled-task/task.yaml create mode 100644 tests/main/upgrade-from-2.15/task.yaml create mode 100644 tests/main/user-data-handling/task.yaml create mode 100644 tests/main/user-mounts/task.yaml create mode 100644 tests/main/validate-container-failures/task.yaml create mode 100644 tests/main/whoami/task.yaml create mode 100644 tests/main/writable-areas/task.yaml create mode 100644 tests/main/xauth-migration/task.yaml create mode 100644 tests/main/xdg-open-compat/task.yaml create mode 100644 tests/main/xdg-open/task.yaml create mode 100644 tests/main/xdg-settings/task.yaml create mode 100644 tests/manual-tests.md create mode 100644 tests/nested/core-revert/task.yaml create mode 100644 tests/nested/extra-snaps-assertions/task.yaml create mode 100644 tests/nested/hot-plug/task.yaml create mode 100644 tests/nested/image-build/task.yaml create mode 100644 tests/nightly/docker/task.yaml create mode 100644 tests/nightly/unity/task.yaml create mode 100644 tests/regression/lp-1595444/task.yaml create mode 100644 tests/regression/lp-1597839/task.yaml create mode 100644 tests/regression/lp-1597842/task.yaml create mode 100644 tests/regression/lp-1599891/task.yaml create mode 100644 tests/regression/lp-1606277/task.yaml create mode 100644 tests/regression/lp-1607796/task.yaml create mode 100644 tests/regression/lp-1615113/task.yaml create mode 100644 tests/regression/lp-1618683/task.yaml create mode 100644 tests/regression/lp-1630479/task.yaml create mode 100644 tests/regression/lp-1641885/task.yaml create mode 100644 tests/regression/lp-1644439/task.yaml create mode 100644 tests/regression/lp-1665004/task.yaml create mode 100644 tests/regression/lp-1667385/task.yaml create mode 100644 tests/regression/lp-1693042/task.yaml create mode 100755 tests/regression/lp-1704860/snap-env-query.sh create mode 100644 tests/regression/lp-1704860/task.yaml create mode 100644 tests/regression/lp-1732555/task.yaml create mode 100644 tests/regression/lp-1764977/task.yaml create mode 100644 tests/regression/lp-1797556/task.yaml create mode 100644 tests/regression/lp-1800004/task.yaml create mode 100644 tests/regression/lp-1801955/task.yaml create mode 100644 tests/regression/lp-1802581/task.yaml create mode 100644 tests/regression/lp-1803535/task.yaml create mode 100644 tests/regression/lp-1803542/task.yaml create mode 100644 tests/regression/lp-1805485/task.yaml create mode 100644 tests/regression/lp-1805838/task.yaml create mode 100644 tests/regression/lp-1813963/task.yaml create mode 100644 tests/regression/lp-1815722/task.yaml create mode 100644 tests/regression/lp-1815869/hello.py create mode 100644 tests/regression/lp-1815869/task.yaml create mode 100644 tests/smoke/find-info/task.yaml create mode 100644 tests/smoke/install/task.yaml create mode 100644 tests/smoke/remove/task.yaml create mode 100644 tests/smoke/sandbox/task.yaml create mode 100644 tests/snapd-state.md create mode 100644 tests/unit/c-unit-tests-clang/task.yaml create mode 100644 tests/unit/c-unit-tests-gcc/task.yaml create mode 100644 tests/unit/go/task.yaml create mode 100644 tests/unit/spread-shellcheck/can-fail create mode 100644 tests/unit/spread-shellcheck/task.yaml create mode 100644 tests/upgrade/basic/task.yaml create mode 100644 tests/upgrade/snapd-xdg-open/task.yaml create mode 100755 tests/util/benchmark.sh create mode 100644 testutil/base.go create mode 100644 testutil/containschecker.go create mode 100644 testutil/containschecker_test.go create mode 100644 testutil/dbustest.go create mode 100644 testutil/exec.go create mode 100644 testutil/exec_test.go create mode 100644 testutil/export_test.go create mode 100644 testutil/filecontentchecker.go create mode 100644 testutil/filecontentchecker_test.go create mode 100644 testutil/filepresencechecker.go create mode 100644 testutil/filepresencechecker_test.go create mode 100644 testutil/intcheckers.go create mode 100644 testutil/intcheckers_test.go create mode 100644 testutil/lowlevel.go create mode 100644 testutil/lowlevel_test.go create mode 100644 testutil/paddedchecker.go create mode 100644 testutil/paddedchecker_test.go create mode 100644 testutil/syscallschecker.go create mode 100644 testutil/syscallschecker_test.go create mode 100644 testutil/testutil_test.go create mode 100644 timeout/timeout.go create mode 100644 timeout/timeout_test.go create mode 100644 timeutil/export_test.go create mode 100644 timeutil/human.go create mode 100644 timeutil/human_test.go create mode 100644 timeutil/schedule.go create mode 100644 timeutil/schedule_test.go create mode 100755 update-pot create mode 100644 userd/autostart.go create mode 100644 userd/autostart_test.go create mode 100644 userd/export_test.go create mode 100644 userd/helpers.go create mode 100644 userd/helpers_test.go create mode 100644 userd/launcher.go create mode 100644 userd/launcher_test.go create mode 100644 userd/settings.go create mode 100644 userd/settings_test.go create mode 100644 userd/ui/kdialog.go create mode 100644 userd/ui/kdialog_test.go create mode 100644 userd/ui/ui.go create mode 100644 userd/ui/zenity.go create mode 100644 userd/ui/zenity_test.go create mode 100644 userd/userd.go create mode 100644 wrappers/binaries.go create mode 100644 wrappers/binaries_test.go create mode 100644 wrappers/core18.go create mode 100644 wrappers/core18_test.go create mode 100644 wrappers/desktop.go create mode 100644 wrappers/desktop_test.go create mode 100644 wrappers/export_test.go create mode 100644 wrappers/services.go create mode 100644 wrappers/services_gen_test.go create mode 100644 wrappers/services_test.go create mode 100644 x11/xauth.go create mode 100644 x11/xauth_test.go create mode 100644 xdgopenproxy/export_test.go create mode 100644 xdgopenproxy/xdgopenproxy.go create mode 100644 xdgopenproxy/xdgopenproxy_test.go diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..b4a0578b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,68 @@ +language: go +git: + quiet: true +jobs: + include: + - stage: quick + name: go 1.6/xenial static and unit test suites + dist: xenial + go: "1.6" + before_install: + - sudo apt --quiet -o Dpkg::Progress-Fancy=false update + install: + - sudo apt --quiet -o Dpkg::Progress-Fancy=false build-dep snapd + - ./get-deps.sh + script: + - set -e + - ./run-checks --static + - ./run-checks --short-unit + - stage: quick + go: "1.10" + name: OSX build and minimal runtime sanity check + os: osx + addons: + homebrew: + packages: [squashfs] + install: + - ./get-deps.sh + # extra dependency on darwin: + - go get golang.org/x/sys/unix + before_script: + - ./mkversion.sh + - go build -o /tmp/snp ./cmd/snap + script: + - /tmp/snp download hello + - /tmp/snp version + - /tmp/snp pack tests/lib/snaps/test-snapd-tools/ /tmp + - stage: quick + name: CLA check + if: type = pull_request + language: bash + addons: + apt: + packages: + python-launchpadlib + script: + - ./tests/lib/cla_check.py + - stage: integration + name: spread + os: linux + addons: + apt: + packages: + - xdelta3 + install: + # override the default install for language:go + - true + script: + - ./run-checks --spread +env: + global: + # SPREAD_LINODE_KEY + - secure: "bzALrfNSLwM0bjceal1PU5rFErvqVhi00Sygx8jruo6htpZay3hrC2sHCKCQKPn1kvCfHidrHX1vnomg5N+B9o25GZEYSjKSGxuvdNDfCZYqPNjMbz5y7xXYfKWgyo+xtrKRM85Nqy121SfRz3KLDvrOLwwreb+pZv8DG1WraFTd7D6rK7nLnnYNUyw665XBMFVnM8ue3Zu9496Ih/TfQXhnNpsZY8xFWte4+cH7JvVCVTs8snjoGVZi3972PzinNkfBgJa24cUzxFMfiN/AwSBXJQKdVv+FsbB4uRgXAqTNwuus7PptiPNxpWWojuhm1Qgbk0XhGIdJxyUYkmNA4UrZ3C29nIRWbuAiHJ6ZWd1ur3dqphqOcgFInltSHkpfEdlL3YK4dCa2SmJESzotUGnyowCUUCXkWdDaZmFTwyK0Y6He9oyXDK5f+/U7SFlPvok0caJCvB9HbTQR1kYdh048I/R+Ht5QrFOZPk21DYWDOYhn7SzthBDZLsaL6n5gX7Y547SsL4B35YVbpaeHzccG6Mox8rI4bqlGFvP1U5i8uXD4uQjJChlVxpmozUEMok9T5RVediJs540p5uc8DQl48Nke02tXzC/XpGAvpnXT7eiiRNW67zOj2QcIV+ni3lBj3HvZeB9cgjzLNrZSl/t9vseqnNwQWpl3V6nd/bU=" + # SPREAD_STORE_USER + - secure: "LjqfvJ2xz/7cxt1Cywaw5l8gaj5jOhUsf502UeaH+rOnj+9tCdWTtyP8U4nOjjQwiJ0xuygba+FgdnXEyxV+THeXHOF69SRF/1N8JIc3i9G6JK/CqDfFTRMqiRaCf5u7KuOrYZ0ssYNBXyZ8X4Ahls3uFu2DgEuAim1J6wOVSgIoUkduLVrbsn6uB9G5Uuc+C4NMA3TH21IJ6ct35t3T+/EjvoGUHcKtoOsPXdBZvz96xw5mKGIBaLpZdy5WxmhPUsz3MIlZgvi4DR3YIa/9u+QoGNU05f8upJRhwdwkuu9vJwqekXNXDJi/ZGlpkkAPx0feJbyhtz68551Pn1TtmA3TS5JtuMeMZWxCL9SudA7/C3oBRNGnKI3LwvP20pPjdlEYMOCq/oHlxoJylGVdpynZXTtaFS+s4Qhnr+WuNcG3zFa9bJvXPyy1vxPKcjI2DojneTrCTW/L6zg7tBIVQGzTxmC7QWsbTvOQzu+YICyeeS3g+iJ+QyP6+/oTyER3a3vmZCtXqsBJTznesS0SL5AkK+8moBGct96S6kT55XCDVgThWV0OGH6l4LwVSOjPioNzXNhVLZ8GKkXrMZXKSaWAeYptzWl4Gfz0Y4nFCu3aqIOyie7janPPgeEL0E2ZjndIs+ZigtN1LCol+GJN7fXzUFy8Fichqhhwvb3YLyE=" + # SPREAD_STORE_PASSWORD + - secure: "Le4CMhklfadi4aBQIEaEMbsFIB608GOvSHjVUxkDxkkUAVwl/Ov4Dni5d0Tn4e/xcxPkcm+pPg64dn0Jxzwx6XfWlxhWC10vYh+/GjpZW1znahtb/Gf9CNZOJJEy5LSeI7/uJ3LYcFd0FU0EJSerNeQJc5d8jmJH8UnuqObHOk29YD//XILiLRa1XALEimwXeQyGQePBmDTxPQQv1VLFjgfaJa5Xy55Us7AKTML2V7lhaeKCSEIp3x9liLAtnKlJhyXaXO/e4b3ZJTgXwYh+vENK1E2pxalpjBNPaJNkvtbsjFtYNXJoXca+hBVs5Sq1PCBhkEGxqFUsD8VLQd+MEXp4MYOF5fBhxIa3qOSjtuR+WmZ9G6fEysEBV6Y3F3D6HYWTpNkcHNXJCwdtOM+n92zNEBDIrufwzTPpyJXpoxZCCXrk3HHRdyDktvJYLrHdn1bM19mgYguesMZHTC5xMD6ifwdRoylmApjImXOvVxf2HdQiNvNLDqvaHgmYwNfl0+KbaVz+O2EDPCRnT5wOCpSeSUet47EPITdjr5OnTwLpOVaY+iSvn90EUB/8+ZU01TRYgc+6VNPHokLVjuiQJSrE4yTx/c2MnY9eRaOosVXngYfoS/L3XwDwZiQoeLZs04bScvxzGQIGCJ+CBzNPENtZ4AUh55Yl/vVNReZJeaY=" + # SPREAD_GOOGLE_KEY + - secure: "dIA2HrartowFL2Gl5jXiVMd9hIJyIeummYwxeBL9MzO48E/BIJyIGHudEOo8oCnZ5a0yb8TqYgND2FCgJU1V5I2LyxH6T9kizHjtmIGgeM4qlEGKRlptb2v7DFkaHeW4Mpp4gLk8hYIeWyq9OR+SlK6f0Jj049LLKfQoX6GzTPug5+MMEQOJs55OJ6f6gvCv2o3oj6WFybaohMCO4GbNYQSPLwheyTSkT0efnW9QqTN0w62pDMqscVURO90/CUeZyCcXw2uOBegwPNTBoo/+4+nZsfSNeupV8wX4vVYL0ZFL6IO3mViDoZBD4SGTNF/9x8Lc1WeKm9HlELzy5krdLqsvdV/fQSWhBzwkdykKVA3Aae5dAMIGRt7e5bJaUg+/HdtOgA5jr+qey/c/BN11MyaSOMNPNGjRuv9NAcEjxoN2JkiDXfpA3lE9kjd7TBTexGe4RJGJLJjT9s8XxdKufBfruC/yhVGdVkRoc2tsAJPZ72Ds9qH0FH28zNFAgAitCLDfInjhPMPvZJhb3Bqx5P/0DE5zUbduE9kYK0iiZRJ4AaytQy+R4nJCXE42mWv5cxoE84opVqO9cBu1TPCC8gTRQFWpJt1rP+DvwjaFiswvptG8obxNpHmkhcItPGmRVN9P9Yjd9nHvegS83tsbrd2KOyMmCk3/1KWhLufisHE=" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..712c273e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +Before contributing you should sign [Canonical's contributor agreement][1], +it’s the easiest way for you to give us permission to use your contributions. + +## Pull Requests and tests + +We need to verify that the code functionality and quality is not degraded +by additions before merging any changes to snapd's codebase. For each PR +we run checks in three different groups: static, unit and spread. + +Static test use several code analysis tools present in the GoLang ecosystem +(go vet, go lint and go fmt) to make sure that the code always aligns with +the standards. They also check the markdown format of documentation files. +All the existing unit tests are also executed, and the coverage info is +reported to coveralls. Regarding [spread](https://github.com/snapcore/spread), +we use it to verify the integrity of the product exercising it as a whole, +both from an end user standpoint (eg. all kind of interactions with the +snap tool from the command line) and from a more systemic approach (testing +upgrades, for instance). + +We do not set as a requirement the addition of spread and unit tests for a PR +to be merged, but encourage the contributors to add them so that the expected +behaviour is explained and verified through the tests and the review process +can be made on the solid base of a working system after the addition of the +changes. If any tests need to be added for a PR to be merged it will be denoted +during the review process. + +[1]: http://www.ubuntu.com/legal/contributors diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + 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..59ca14a9 --- /dev/null +++ b/HACKING.md @@ -0,0 +1,212 @@ +# Hacking on snapd + +Hacking on snapd is fun and straightforward. 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 + +### Supported Go versions + +snapd is supported on Go 1.6 onwards. + +### 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" + +(note `$GOPATH` can actually point to multiple locations, like `$PATH`, so if +your `$GOPATH` is more complex than a single entry you'll need to adjust the +above). + +### 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 + +Go 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 + +Other dependencies are handled via distribution packages and you should ensure +that dependencies for your distribution are installed. For example, on Ubuntu, +run: + + sudo apt-get build-dep ./ + +### 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/cmd/snap/... + +to have it available in `$GOPATH/bin` + +Similarly, to build the `snapd` REST API daemon, you can run + + go build -o /tmp/snapd github.com/snapcore/snapd/cmd/snapd + +### Contributing + +Contributions are always welcome! Please make sure that you sign the +Canonical contributor license 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. + +The source format follows the `gofmt -s` formating. Please run this on your sources files if `run-checks` complains about the format. + +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 SNAPD_DEBUG=1 SNAPD_DEBUG_HTTP=3 ./snapd + +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. + +(make hack: In case you get some security profiles errors when trying to install or refresh a snap, +maybe you need to replace system installed snap-seccomp with the one aligned to the snapd that +you are testing. To do this, simply backup /usr/lib/snapd/snap-seccomp and overwrite it with +the testing one. Don't forget to rollback to the original when finish testing) + +# 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/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..f37c6d41 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,2 @@ +Thanks for helping us make a better snapd! +Have you signed the [license agreement](https://www.ubuntu.com/legal/contributors) and read the [contribution guide](https://github.com/snapcore/snapd/blob/master/CONTRIBUTING.md)? diff --git a/README.md b/README.md new file mode 100644 index 00000000..705869b1 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +[![Build Status][travis-image]][travis-url] +[![Go Report Card][goreportcard-image]][goreportcard-url] +[![codecov][codecov-image]][codecov-url] + +## Snaps + +Package any app for every Linux desktop, server, cloud or device. + +Snaps are faster to install, easier to create, safer to run, and they update +automatically and transactionally so your app is always fresh and never +broken. You can bring your own build infrastructure or use ours. + +Head over to [snapcraft.io](https://snapcraft.io) to get started. + +## 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 forums](https://forum.snapcraft.io/). + +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 + +[codecov-url]: https://codecov.io/gh/snapcore/snapd +[codecov-image]: https://codecov.io/gh/snapcore/snapd/branch/master/graph/badge.svg diff --git a/advisor/backend.go b/advisor/backend.go new file mode 100644 index 00000000..d74aa14f --- /dev/null +++ b/advisor/backend.go @@ -0,0 +1,300 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 advisor + +import ( + "encoding/json" + "os" + "path/filepath" + "time" + + "github.com/snapcore/bolt" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/strutil" +) + +var ( + cmdBucketKey = []byte("Commands") + pkgBucketKey = []byte("Snaps") +) + +type writer struct { + fn string + db *bolt.DB + tx *bolt.Tx + cmdBucket *bolt.Bucket + pkgBucket *bolt.Bucket +} + +type CommandDB interface { + // AddSnap adds the entries for commands pointing to the given + // snap name to the commands database. + AddSnap(snapName, version, summary string, commands []string) error + // Commit persist the changes, and closes the database. If the + // database has already been committed/rollbacked, does nothing. + Commit() error + // Rollback aborts the changes, and closes the database. If the + // database has already been committed/rollbacked, does nothing. + Rollback() error +} + +// Create opens the commands database for writing, and starts a +// transaction that drops and recreates the buckets. You should then +// call AddSnap with each snap you wish to add, and them Commit the +// results to make the changes live, or Rollback to abort; either of +// these closes the database again. +func Create() (CommandDB, error) { + var err error + t := &writer{ + fn: dirs.SnapCommandsDB + "." + strutil.MakeRandomString(12) + "~", + } + + t.db, err = bolt.Open(t.fn, 0644, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return nil, err + } + + t.tx, err = t.db.Begin(true) + if err == nil { + t.cmdBucket, err = t.tx.CreateBucket(cmdBucketKey) + if err == nil { + t.pkgBucket, err = t.tx.CreateBucket(pkgBucketKey) + } + + if err != nil { + t.tx.Rollback() + } + } + + if err != nil { + t.db.Close() + return nil, err + } + + return t, nil +} + +func (t *writer) AddSnap(snapName, version, summary string, commands []string) error { + for _, cmd := range commands { + var sil []Package + + bcmd := []byte(cmd) + row := t.cmdBucket.Get(bcmd) + if row != nil { + if err := json.Unmarshal(row, &sil); err != nil { + return err + } + } + // For the mapping of command->snap we do not need the summary, nothing is using that. + sil = append(sil, Package{Snap: snapName, Version: version}) + row, err := json.Marshal(sil) + if err != nil { + return err + } + if err := t.cmdBucket.Put(bcmd, row); err != nil { + return err + } + } + + // TODO: use json here as well and put the version information here + bj, err := json.Marshal(Package{ + Snap: snapName, + Version: version, + Summary: summary, + }) + if err != nil { + return err + } + if err := t.pkgBucket.Put([]byte(snapName), bj); err != nil { + return err + } + + return nil +} + +func (t *writer) Commit() error { + // either everything worked, and therefore this will fail, or something + // will fail, and that error is more important than this one if this one + // then fails as well. So, ignore the error. + defer os.Remove(t.fn) + + if err := t.done(true); err != nil { + return err + } + + dir, err := os.Open(filepath.Dir(dirs.SnapCommandsDB)) + if err != nil { + return err + } + defer dir.Close() + + if err := os.Rename(t.fn, dirs.SnapCommandsDB); err != nil { + return err + } + + return dir.Sync() +} + +func (t *writer) Rollback() error { + e1 := t.done(false) + e2 := os.Remove(t.fn) + if e1 == nil { + return e2 + } + return e1 +} + +func (t *writer) done(commit bool) error { + var e1, e2 error + + t.cmdBucket = nil + t.pkgBucket = nil + if t.tx != nil { + if commit { + e1 = t.tx.Commit() + } else { + e1 = t.tx.Rollback() + } + t.tx = nil + } + if t.db != nil { + e2 = t.db.Close() + t.db = nil + } + if e1 == nil { + return e2 + } + return e1 +} + +// DumpCommands returns the whole database as a map. For use in +// testing and debugging. +func DumpCommands() (map[string]string, error) { + db, err := bolt.Open(dirs.SnapCommandsDB, 0644, &bolt.Options{ + ReadOnly: true, + Timeout: 1 * time.Second, + }) + if err != nil { + return nil, err + } + defer db.Close() + + tx, err := db.Begin(false) + if err != nil { + return nil, err + } + defer tx.Rollback() + + b := tx.Bucket(cmdBucketKey) + if b == nil { + return nil, nil + } + + m := map[string]string{} + c := b.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + m[string(k)] = string(v) + } + + return m, nil +} + +type boltFinder struct { + *bolt.DB +} + +// Open the database for reading. +func Open() (Finder, error) { + // Check for missing file manually to workaround bug in bolt. + // bolt.Open() is using os.OpenFile(.., os.O_RDONLY | + // os.O_CREATE) even if ReadOnly mode is used. So we would get + // a misleading "permission denied" error without this check. + if !osutil.FileExists(dirs.SnapCommandsDB) { + return nil, os.ErrNotExist + } + db, err := bolt.Open(dirs.SnapCommandsDB, 0644, &bolt.Options{ + ReadOnly: true, + Timeout: 1 * time.Second, + }) + if err != nil { + return nil, err + } + + return &boltFinder{db}, nil +} + +func (f *boltFinder) FindCommand(command string) ([]Command, error) { + tx, err := f.Begin(false) + if err != nil { + return nil, err + } + defer tx.Rollback() + + b := tx.Bucket(cmdBucketKey) + if b == nil { + return nil, nil + } + + buf := b.Get([]byte(command)) + if buf == nil { + return nil, nil + } + var sil []Package + if err := json.Unmarshal(buf, &sil); err != nil { + return nil, err + } + cmds := make([]Command, len(sil)) + for i, si := range sil { + cmds[i] = Command{ + Snap: si.Snap, + Version: si.Version, + Command: command, + } + } + + return cmds, nil +} + +func (f *boltFinder) FindPackage(pkgName string) (*Package, error) { + tx, err := f.Begin(false) + if err != nil { + return nil, err + } + defer tx.Rollback() + + b := tx.Bucket(pkgBucketKey) + if b == nil { + return nil, nil + } + + bj := b.Get([]byte(pkgName)) + if bj == nil { + return nil, nil + } + var si Package + err = json.Unmarshal(bj, &si) + if err != nil { + return nil, err + } + + return &Package{Snap: pkgName, Version: si.Version, Summary: si.Summary}, nil +} diff --git a/advisor/cmdfinder.go b/advisor/cmdfinder.go new file mode 100644 index 00000000..7cfadb5c --- /dev/null +++ b/advisor/cmdfinder.go @@ -0,0 +1,110 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 advisor + +import ( + "os" +) + +type Command struct { + Snap string + Version string `json:"Version,omitempty"` + Command string +} + +func FindCommand(command string) ([]Command, error) { + finder, err := newFinder() + if err != nil && os.IsNotExist(err) { + return nil, nil + } + if err != nil { + return nil, err + } + defer finder.Close() + + return finder.FindCommand(command) +} + +const ( + minLen = 3 + maxLen = 256 +) + +// based on CommandNotFound.py:similar_words.py +func similarWords(word string) []string { + const alphabet = "abcdefghijklmnopqrstuvwxyz-_0123456789" + similar := make(map[string]bool, 2*len(word)+2*len(word)*len(alphabet)) + + // deletes + for i := range word { + similar[word[:i]+word[i+1:]] = true + } + // transpose + for i := 0; i < len(word)-1; i++ { + similar[word[:i]+word[i+1:i+2]+word[i:i+1]+word[i+2:]] = true + } + // replaces + for i := range word { + for _, r := range alphabet { + similar[word[:i]+string(r)+word[i+1:]] = true + } + } + // inserts + for i := range word { + for _, r := range alphabet { + similar[word[:i]+string(r)+word[i:]] = true + } + } + + // convert for output + ret := make([]string, 0, len(similar)) + for w := range similar { + ret = append(ret, w) + } + + return ret +} + +func FindMisspelledCommand(command string) ([]Command, error) { + if len(command) < minLen || len(command) > maxLen { + return nil, nil + } + finder, err := newFinder() + if err != nil && os.IsNotExist(err) { + return nil, nil + } + if err != nil { + return nil, err + } + defer finder.Close() + + alternatives := make([]Command, 0, 32) + for _, w := range similarWords(command) { + res, err := finder.FindCommand(w) + if err != nil { + return nil, err + } + if len(res) > 0 { + alternatives = append(alternatives, res...) + } + } + + return alternatives, nil +} diff --git a/advisor/cmdfinder_test.go b/advisor/cmdfinder_test.go new file mode 100644 index 00000000..5a83e540 --- /dev/null +++ b/advisor/cmdfinder_test.go @@ -0,0 +1,153 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 advisor_test + +import ( + "os" + "sort" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/advisor" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type cmdfinderSuite struct{} + +var _ = Suite(&cmdfinderSuite{}) + +func (s *cmdfinderSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) + c.Assert(os.MkdirAll(dirs.SnapCacheDir, 0755), IsNil) + + db, err := advisor.Create() + c.Assert(err, IsNil) + c.Assert(db.AddSnap("foo", "1.0", "foo summary", []string{"foo", "meh"}), IsNil) + c.Assert(db.AddSnap("bar", "2.0", "bar summary", []string{"bar", "meh"}), IsNil) + c.Assert(db.Commit(), IsNil) +} + +func (s *cmdfinderSuite) TestFindSimilarWordsCnf(c *C) { + words := advisor.SimilarWords("123") + sort.Strings(words) + c.Check(words, DeepEquals, []string{ + // calculated using CommandNotFound.py:similar_words("123") + "-123", "-23", "0123", "023", "1-23", "1-3", "1023", + "103", "1123", "113", "12", "12-", "12-3", "120", + "1203", "121", "1213", "122", "1223", "123", "1233", + "124", "1243", "125", "1253", "126", "1263", "127", + "1273", "128", "1283", "129", "1293", "12_", "12_3", + "12a", "12a3", "12b", "12b3", "12c", "12c3", "12d", + "12d3", "12e", "12e3", "12f", "12f3", "12g", "12g3", + "12h", "12h3", "12i", "12i3", "12j", "12j3", "12k", + "12k3", "12l", "12l3", "12m", "12m3", "12n", "12n3", + "12o", "12o3", "12p", "12p3", "12q", "12q3", "12r", + "12r3", "12s", "12s3", "12t", "12t3", "12u", "12u3", + "12v", "12v3", "12w", "12w3", "12x", "12x3", "12y", + "12y3", "12z", "12z3", "13", "132", "1323", "133", + "1423", "143", "1523", "153", "1623", "163", "1723", + "173", "1823", "183", "1923", "193", "1_23", "1_3", + "1a23", "1a3", "1b23", "1b3", "1c23", "1c3", "1d23", + "1d3", "1e23", "1e3", "1f23", "1f3", "1g23", "1g3", + "1h23", "1h3", "1i23", "1i3", "1j23", "1j3", "1k23", + "1k3", "1l23", "1l3", "1m23", "1m3", "1n23", "1n3", + "1o23", "1o3", "1p23", "1p3", "1q23", "1q3", "1r23", + "1r3", "1s23", "1s3", "1t23", "1t3", "1u23", "1u3", + "1v23", "1v3", "1w23", "1w3", "1x23", "1x3", "1y23", + "1y3", "1z23", "1z3", "2123", "213", "223", "23", + "3123", "323", "4123", "423", "5123", "523", "6123", + "623", "7123", "723", "8123", "823", "9123", "923", + "_123", "_23", "a123", "a23", "b123", "b23", "c123", + "c23", "d123", "d23", "e123", "e23", "f123", "f23", + "g123", "g23", "h123", "h23", "i123", "i23", "j123", + "j23", "k123", "k23", "l123", "l23", "m123", "m23", + "n123", "n23", "o123", "o23", "p123", "p23", "q123", + "q23", "r123", "r23", "s123", "s23", "t123", "t23", + "u123", "u23", "v123", "v23", "w123", "w23", "x123", + "x23", "y123", "y23", "z123", "z23", + }) +} + +func (s *cmdfinderSuite) TestFindSimilarWordsTrivial(c *C) { + words := advisor.SimilarWords("hella") + c.Check(words, testutil.Contains, "hello") +} + +func (s *cmdfinderSuite) TestFindCommandHit(c *C) { + cmds, err := advisor.FindCommand("meh") + c.Assert(err, IsNil) + c.Check(cmds, DeepEquals, []advisor.Command{ + {Snap: "foo", Version: "1.0", Command: "meh"}, + {Snap: "bar", Version: "2.0", Command: "meh"}, + }) +} + +func (s *cmdfinderSuite) TestFindCommandMiss(c *C) { + cmds, err := advisor.FindCommand("moh") + c.Assert(err, IsNil) + c.Check(cmds, HasLen, 0) +} + +func (s *cmdfinderSuite) TestFindMisspelledCommandHit(c *C) { + cmds, err := advisor.FindMisspelledCommand("moh") + c.Assert(err, IsNil) + c.Check(cmds, DeepEquals, []advisor.Command{ + {Snap: "foo", Version: "1.0", Command: "meh"}, + {Snap: "bar", Version: "2.0", Command: "meh"}, + }) +} + +func (s *cmdfinderSuite) TestFindMisspelledCommandMiss(c *C) { + cmds, err := advisor.FindMisspelledCommand("hello") + c.Assert(err, IsNil) + c.Check(cmds, HasLen, 0) +} + +func (s *cmdfinderSuite) TestDumpCommands(c *C) { + cmds, err := advisor.DumpCommands() + c.Assert(err, IsNil) + c.Check(cmds, DeepEquals, map[string]string{ + "foo": `[{"snap":"foo","version":"1.0"}]`, + "bar": `[{"snap":"bar","version":"2.0"}]`, + "meh": `[{"snap":"foo","version":"1.0"},{"snap":"bar","version":"2.0"}]`, + }) +} + +func (s *cmdfinderSuite) TestFindMissingCommandsDB(c *C) { + err := os.Remove(dirs.SnapCommandsDB) + c.Assert(err, IsNil) + + cmds, err := advisor.FindMisspelledCommand("hello") + c.Assert(err, IsNil) + c.Check(cmds, HasLen, 0) + + cmds, err = advisor.FindCommand("hello") + c.Assert(err, IsNil) + c.Check(cmds, HasLen, 0) + + pkg, err := advisor.FindPackage("hello") + c.Assert(err, IsNil) + c.Check(pkg, IsNil) +} diff --git a/advisor/export_test.go b/advisor/export_test.go new file mode 100644 index 00000000..20b4ce15 --- /dev/null +++ b/advisor/export_test.go @@ -0,0 +1,22 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 advisor + +var SimilarWords = similarWords diff --git a/advisor/finder.go b/advisor/finder.go new file mode 100644 index 00000000..ca3b9982 --- /dev/null +++ b/advisor/finder.go @@ -0,0 +1,36 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 advisor + +var newFinder = Open + +type Finder interface { + FindCommand(command string) ([]Command, error) + FindPackage(pkgName string) (*Package, error) + Close() error +} + +func ReplaceCommandsFinder(constructor func() (Finder, error)) (restore func()) { + old := newFinder + newFinder = constructor + return func() { + newFinder = old + } +} diff --git a/advisor/pkgfinder.go b/advisor/pkgfinder.go new file mode 100644 index 00000000..bae4f820 --- /dev/null +++ b/advisor/pkgfinder.go @@ -0,0 +1,43 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 advisor + +import ( + "os" +) + +type Package struct { + Snap string `json:"snap"` + Version string `json:"version"` + Summary string `json:"summary,omitempty"` +} + +func FindPackage(pkgName string) (*Package, error) { + finder, err := newFinder() + if err != nil && os.IsNotExist(err) { + return nil, nil + } + if err != nil { + return nil, err + } + defer finder.Close() + + return finder.FindPackage(pkgName) +} diff --git a/advisor/pkgfinder_test.go b/advisor/pkgfinder_test.go new file mode 100644 index 00000000..fd08017b --- /dev/null +++ b/advisor/pkgfinder_test.go @@ -0,0 +1,40 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 advisor_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/advisor" +) + +func (s *cmdfinderSuite) TestFindPackageHit(c *C) { + pkg, err := advisor.FindPackage("foo") + c.Assert(err, IsNil) + c.Check(pkg, DeepEquals, &advisor.Package{ + Snap: "foo", Version: "1.0", Summary: "foo summary", + }) +} + +func (s *cmdfinderSuite) TestFindPackageMiss(c *C) { + pkg, err := advisor.FindPackage("moh") + c.Assert(err, IsNil) + c.Check(pkg, IsNil) +} diff --git a/arch/arch.go b/arch/arch.go new file mode 100644 index 00000000..d9813947 --- /dev/null +++ b/arch/arch.go @@ -0,0 +1,136 @@ +// -*- 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" + + "github.com/snapcore/snapd/osutil" +) + +// 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 +} + +// FIXME: rename all Ubuntu*Architecture() to SnapdArchitecture() +// (or DpkgArchitecture) + +// 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", + // available in debian and other distros + "ppc64": "ppc64", + } + + // If we are running on an ARM platform we need to have a + // closer look if we are on armhf or armel. If we're not + // on a armv6 platform we can continue to use the Go + // arch mapping. The Go arch sadly doesn't map this out + // for us so we have to fallback to uname here. + if goarch == "arm" { + if osutil.MachineName() == "armv6l" { + return "armel" + } + } + + ubuntuArch := goArchMapping[goarch] + if ubuntuArch == "" { + log.Panicf("unknown goarch %q", goarch) + } + + return ubuntuArch +} + +// UbuntuKernelArchitecture return the debian equivalent architecture +// for the current running kernel. This is usually the same as the +// UbuntuArchitecture - however there maybe cases that you run e.g. +// a snapd:i386 on an amd64 kernel. +func UbuntuKernelArchitecture() string { + return ubuntuArchFromKernelArch(osutil.MachineName()) +} + +// ubuntuArchFromkernelArch maps the kernel architecture as reported +// via uname() to the dpkg architecture +func ubuntuArchFromKernelArch(utsMachine string) string { + kernelArchMapping := map[string]string{ + // kernel ubuntu + "i686": "i386", + "x86_64": "amd64", + "armv7l": "armhf", + "armv8l": "arm64", + "aarch64": "arm64", + "ppc64le": "ppc64el", + "s390x": "s390x", + "ppc": "powerpc", + // available in debian and other distros + "ppc64": "ppc64", + } + + ubuntuArch := kernelArchMapping[utsMachine] + if ubuntuArch == "" { + log.Panicf("unknown kernel arch %q", utsMachine) + } + + 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..b08a1200 --- /dev/null +++ b/arch/arch_test.go @@ -0,0 +1,61 @@ +// -*- 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") + c.Check(ubuntuArchFromGoArch("ppc64"), Equals, "ppc64") + c.Check(ubuntuArchFromGoArch("s390x"), Equals, "s390x") +} + +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..a1017b3c --- /dev/null +++ b/asserts/account.go @@ -0,0 +1,113 @@ +// -*- 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" + "time" +) + +var ( + // account ids look like snap-ids or a nice identifier + validAccountID = regexp.MustCompile("^(?:[a-z0-9A-Z]{32}|[-a-z0-9]{2,28})$") +) + +// 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 + validation string + 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") +} + +// Validation returns the level of confidence of the authority in the +// account's identity, expected to be "unproven" or "verified", and +// for forward compatibility any value != "unproven" can be considered +// at least "verified". +func (acc *Account) Validation() string { + return acc.validation +} + +// 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 + } + + validation, err := checkNotEmptyString(assert.headers, "validation") + if err != nil { + return nil, err + } + // backward compatibility with the hard-coded trusted account + // assertions + // TODO: generate revision 1 of them with validation + // s/certified/verified/ + if validation == "certified" { + validation = "verified" + } + + 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, + validation: validation, + timestamp: timestamp, + }, nil +} diff --git a/asserts/account_key.go b/asserts/account_key.go new file mode 100644 index 00000000..17ee949f --- /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 IsNotFound(err) { + 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 && !IsNotFound(err) { + 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 IsNotFound(err) { + 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..4af90343 --- /dev/null +++ b/asserts/account_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 ( + "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: verified\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.Validation(), Equals, "verified") +} + +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) TestValidation(c *C) { + tests := []struct { + value string + isVerified bool + }{ + {"certified", true}, // backward compat for hard-coded trusted assertions + {"verified", true}, + {"unproven", false}, + {"nonsense", false}, + } + + template := strings.Replace(accountExample, "TSLINE", s.tsLine, 1) + for _, test := range tests { + encoded := strings.Replace( + template, + "validation: verified\n", + fmt.Sprintf("validation: %s\n", test.value), + 1, + ) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + account := assert.(*asserts.Account) + expected := test.value + if test.isVerified { + expected = "verified" + } + c.Check(account.Validation(), Equals, expected) + } +} + +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: verified\n", "", `"validation" header is mandatory`}, + {"validation: verified\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 \(key valid since.*\)`) +} + +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() + // default to signing db's authority + delete(headers, "authority-id") + 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..94e8d157 --- /dev/null +++ b/asserts/asserts.go @@ -0,0 +1,1018 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * 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 . + * + */ + +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} + RepairType = &AssertionType{"repair", []string{"brand-id", "repair-id"}, assembleRepair, 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} + SnapDeveloperType = &AssertionType{"snap-developer", []string{"snap-id", "publisher-id"}, assembleSnapDeveloper, 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} + StoreType = &AssertionType{"store", []string{"store"}, assembleStore, 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, + SnapDeveloperType.Name: SnapDeveloperType, + SystemUserType.Name: SystemUserType, + ValidationType.Name: ValidationType, + RepairType.Name: RepairType, + StoreType.Name: StoreType, + // no authority + DeviceSessionRequestType.Name: DeviceSessionRequestType, + SerialRequestType.Name: SerialRequestType, + AccountKeyRequestType.Name: AccountKeyRequestType, +} + +// Type returns the AssertionType with name or nil +func Type(name string) *AssertionType { + return typeRegistry[name] +} + +// TypeNames returns a sorted list of known assertion type names. +func TypeNames() []string { + names := make([]string, 0, len(typeRegistry)) + for k := range typeRegistry { + names = append(names, k) + } + + sort.Strings(names) + + return names +} + +var maxSupportedFormat = map[string]int{} + +func init() { + // register maxSupportedFormats while breaking initialisation loop + + // 1: plugs and slots + // 2: support for $SLOT()/$PLUG()/$MISSING + // 3: support for on-store/on-brand/on-model device scope constraints + maxSupportedFormat[SnapDeclarationType.Name] = 3 +} + +func MockMaxSupportedFormat(assertType *AssertionType, maxFormat int) (restore func()) { + prev := maxSupportedFormat[assertType.Name] + maxSupportedFormat[assertType.Name] = maxFormat + return func() { + maxSupportedFormat[assertType.Name] = prev + } +} + +var formatAnalyzer = map[*AssertionType]func(headers map[string]interface{}, body []byte) (formatnum int, err error){ + SnapDeclarationType: snapDeclarationFormatAnalyze, +} + +// SuggestFormat returns a minimum format that supports the features that would be used by an assertion with the given components. +func SuggestFormat(assertType *AssertionType, headers map[string]interface{}, body []byte) (formatnum int, err error) { + analyzer := formatAnalyzer[assertType] + if analyzer == nil { + // no analyzer, format 0 is all there is + return 0, nil + } + formatnum, err = analyzer(headers, body) + if err != nil { + return 0, fmt.Errorf("assertion %s: %v", assertType.Name, err) + } + return formatnum, nil +} + +// HeadersFromPrimaryKey constructs a headers mapping from the +// primaryKey values and the assertion type, it errors if primaryKey +// has the wrong length. +func HeadersFromPrimaryKey(assertType *AssertionType, primaryKey []string) (headers map[string]string, err error) { + if len(primaryKey) != len(assertType.PrimaryKey) { + return nil, fmt.Errorf("primary key has wrong length for %q assertion", assertType.Name) + } + headers = make(map[string]string, len(assertType.PrimaryKey)) + for i, name := range assertType.PrimaryKey { + keyVal := primaryKey[i] + if keyVal == "" { + return nil, fmt.Errorf("primary key %q header cannot be empty", name) + } + headers[name] = keyVal + } + return headers, nil +} + +// PrimaryKeyFromHeaders extracts the tuple of values from headers +// corresponding to a primary key under the assertion type, it errors +// if there are missing primary key headers. +func PrimaryKeyFromHeaders(assertType *AssertionType, headers map[string]string) (primaryKey []string, err error) { + primaryKey = make([]string, len(assertType.PrimaryKey)) + for i, k := range assertType.PrimaryKey { + keyVal := headers[k] + if keyVal == "" { + return nil, fmt.Errorf("must provide primary key: %v", k) + } + primaryKey[i] = keyVal + } + return primaryKey, nil +} + +// 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) { + headers, err := HeadersFromPrimaryKey(ref.Type, ref.PrimaryKey) + if err != nil { + return nil, fmt.Errorf("%q assertion reference primary key has the wrong length (expected %v): %v", ref.Type.Name, ref.Type.PrimaryKey, ref.PrimaryKey) + } + 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 text, +// SIGNATURE is the signature +// +// Both BODY and HEADER must be UTF8. +// +// 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 or maps, 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 ) +// +// * entries of map each of the form: +// +// " "*baseindent " " NAME ":" ( " " SIMPLEVALUE | "\n" MULTI ) +// +// baseindent starts at 0 and then grows with nesting matching the +// previous level introduction (e.g. 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) +// format (a positive int for the format iteration of the type used) +// +// 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 + maxSigSize int + + defaultMaxBodySize int + typeMaxBodySize map[*AssertionType]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 defaultDecoderBufSize = 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: defaultDecoderBufSize, + maxHeadersSize: MaxHeadersSize, + maxSigSize: MaxSignatureSize, + defaultMaxBodySize: MaxBodySize, + }).initBuffer() +} + +// NewDecoderWithTypeMaxBodySize returns a Decoder to parse the stream of assertions from the reader enforcing optional per type max body sizes or the default one as fallback. +func NewDecoderWithTypeMaxBodySize(r io.Reader, typeMaxBodySize map[*AssertionType]int) *Decoder { + return (&Decoder{ + rd: r, + initialBufSize: defaultDecoderBufSize, + maxHeadersSize: MaxHeadersSize, + maxSigSize: MaxSignatureSize, + defaultMaxBodySize: MaxBodySize, + typeMaxBodySize: typeMaxBodySize, + }).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) + } + + typeStr, _ := headers["type"].(string) + typ := Type(typeStr) + + length, err := checkIntWithDefault(headers, "body-length", 0) + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + if typMaxBodySize := d.typeMaxBodySize[typ]; typMaxBodySize != 0 && length > typMaxBodySize { + return nil, fmt.Errorf("assertion body length %d exceeds maximum body size %d for %q assertions", length, typMaxBodySize, typ.Name) + } else if length > d.defaultMaxBodySize { + 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()) + } + + suggestedFormat, err := SuggestFormat(assertType, finalHeaders, finalBody) + if err != nil { + return nil, err + } + + if suggestedFormat > formatnum { + return nil, fmt.Errorf("cannot sign %q assertion with format set to %d lower than min format %d covering included features", assertType.Name, formatnum, suggestedFormat) + } + + 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} +} + +func (enc *Encoder) writeSep(last byte) error { + if last != '\n' { + _, err := enc.wr.Write(nl) + if err != nil { + return err + } + } + enc.nextSep = nl + return nil +} + +// WriteEncoded writes the encoded assertion into the stream with the required separator. +func (enc *Encoder) WriteEncoded(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 + } + + return enc.writeSep(encoded[sz-1]) +} + +// WriteContentSignature writes the content and signature of an assertion into the stream with all the required separators. +func (enc *Encoder) WriteContentSignature(content, signature []byte) error { + if len(content) == 0 { + return fmt.Errorf("internal error: content cannot be empty") + } + + sz := len(signature) + if sz == 0 { + return fmt.Errorf("internal error: signature cannot be empty") + } + + _, err := enc.wr.Write(enc.nextSep) + if err != nil { + return err + } + + _, err = enc.wr.Write(content) + if err != nil { + return err + } + _, err = enc.wr.Write(nlnl) + if err != nil { + return err + } + _, err = enc.wr.Write(signature) + if err != nil { + return err + } + + return enc.writeSep(signature[sz-1]) +} + +// 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 { + return enc.WriteContentSignature(assert.Signature()) +} + +// 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..ccc143e7 --- /dev/null +++ b/asserts/asserts_test.go @@ -0,0 +1,899 @@ +// -*- 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) TestTypeNames(c *C) { + c.Check(asserts.TypeNames(), DeepEquals, []string{ + "account", + "account-key", + "account-key-request", + "base-declaration", + "device-session-request", + "model", + "repair", + "serial", + "serial-request", + "snap-build", + "snap-declaration", + "snap-developer", + "snap-revision", + "store", + "system-user", + "test-only", + "test-only-2", + "test-only-no-authority", + "test-only-no-authority-pk", + "validation", + }) +} + +func (as *assertsSuite) TestSuggestFormat(c *C) { + fmtnum, err := asserts.SuggestFormat(asserts.Type("test-only-2"), nil, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 0) +} + +func (as *assertsSuite) TestPrimaryKeyHelpers(c *C) { + headers, err := asserts.HeadersFromPrimaryKey(asserts.TestOnlyType, []string{"one"}) + c.Assert(err, IsNil) + c.Check(headers, DeepEquals, map[string]string{ + "primary-key": "one", + }) + + headers, err = asserts.HeadersFromPrimaryKey(asserts.TestOnly2Type, []string{"bar", "baz"}) + c.Assert(err, IsNil) + c.Check(headers, DeepEquals, map[string]string{ + "pk1": "bar", + "pk2": "baz", + }) + + _, err = asserts.HeadersFromPrimaryKey(asserts.TestOnly2Type, []string{"bar"}) + c.Check(err, ErrorMatches, `primary key has wrong length for "test-only-2" assertion`) + + _, err = asserts.HeadersFromPrimaryKey(asserts.TestOnly2Type, []string{"", "baz"}) + c.Check(err, ErrorMatches, `primary key "pk1" header cannot be empty`) + + pk, err := asserts.PrimaryKeyFromHeaders(asserts.TestOnly2Type, headers) + c.Assert(err, IsNil) + c.Check(pk, DeepEquals, []string{"bar", "baz"}) + + headers["other"] = "foo" + pk1, err := asserts.PrimaryKeyFromHeaders(asserts.TestOnly2Type, headers) + c.Assert(err, IsNil) + c.Check(pk1, DeepEquals, pk) + + delete(headers, "pk2") + _, err = asserts.PrimaryKeyFromHeaders(asserts.TestOnly2Type, headers) + c.Check(err, ErrorMatches, `must provide primary key: pk2`) +} + +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) + enc.WriteEncoded([]byte(exampleEmptyBody2NlNl)) + enc.WriteEncoded([]byte(exampleBodyAndExtraHeaders)) + enc.WriteEncoded([]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) TestDecoderDefaultMaxBodySize(c *C) { + enc := strings.Replace(exampleBodyAndExtraHeaders, "body-length: 8", "body-length: 2097153", 1) + decoder := asserts.NewDecoder(bytes.NewBufferString(enc)) + _, err := decoder.Decode() + c.Assert(err, ErrorMatches, "assertion body length 2097153 exceeds maximum body size") +} + +func (as *assertsSuite) TestDecoderWithTypeMaxBodySize(c *C) { + ex1 := strings.Replace(exampleBodyAndExtraHeaders, "body-length: 8", "body-length: 2097152", 1) + ex1 = strings.Replace(ex1, "THE-BODY", strings.Repeat("B", 2*1024*1024), 1) + ex1toobig := strings.Replace(exampleBodyAndExtraHeaders, "body-length: 8", "body-length: 2097153", 1) + ex1toobig = strings.Replace(ex1toobig, "THE-BODY", strings.Repeat("B", 2*1024*1024+1), 1) + const ex2 = `type: test-only-2 +authority-id: auth-id1 +pk1: foo +pk2: bar +body-length: 3 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +XYZ + +AXNpZw==` + + decoder := asserts.NewDecoderWithTypeMaxBodySize(bytes.NewBufferString(ex1+"\n"+ex2), map[*asserts.AssertionType]int{ + asserts.TestOnly2Type: 3, + }) + a1, err := decoder.Decode() + c.Assert(err, IsNil) + c.Check(a1.Body(), HasLen, 2*1024*1024) + a2, err := decoder.Decode() + c.Assert(err, IsNil) + c.Check(a2.Body(), DeepEquals, []byte("XYZ")) + + decoder = asserts.NewDecoderWithTypeMaxBodySize(bytes.NewBufferString(ex1+"\n"+ex2), map[*asserts.AssertionType]int{ + asserts.TestOnly2Type: 2, + }) + a1, err = decoder.Decode() + c.Assert(err, IsNil) + c.Check(a1.Body(), HasLen, 2*1024*1024) + _, err = decoder.Decode() + c.Assert(err, ErrorMatches, `assertion body length 3 exceeds maximum body size 2 for "test-only-2" assertions`) + + decoder = asserts.NewDecoderWithTypeMaxBodySize(bytes.NewBufferString(ex2+"\n\n"+ex1toobig), map[*asserts.AssertionType]int{ + asserts.TestOnly2Type: 3, + }) + a2, err = decoder.Decode() + c.Assert(err, IsNil) + c.Check(a2.Body(), DeepEquals, []byte("XYZ")) + _, err = decoder.Decode() + c.Assert(err, ErrorMatches, "assertion body length 2097153 exceeds maximum body size") +} + +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", + "store", + "snap-declaration", + "snap-build", + "snap-revision", + "snap-developer", + "model", + "serial", + "system-user", + "validation", + "repair", + } + 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..2e8aed75 --- /dev/null +++ b/asserts/assertstest/assertstest.go @@ -0,0 +1,446 @@ +// -*- 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) { + if _, ok := headers["authority-id"]; !ok { + // copy before modifying + headers2 := make(map[string]interface{}, len(headers)+1) + for h, v := range headers { + headers2[h] = v + } + headers = headers2 + 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 + + // Generic authority assertions. + GenericAccount *asserts.Account + GenericKey *asserts.AccountKey + GenericModelsKey *asserts.AccountKey + Generic []asserts.Assertion + GenericClassicModel *asserts.Model + + // 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 +} + +// StoreKeys holds a set of store private keys. +type StoreKeys struct { + Root asserts.PrivateKey + Store asserts.PrivateKey + Generic asserts.PrivateKey + GenericModels asserts.PrivateKey +} + +var ( + rootPrivKey, _ = GenerateKey(1024) + storePrivKey, _ = GenerateKey(752) + genericPrivKey, _ = GenerateKey(752) + genericModelsPrivKey, _ = GenerateKey(752) + + pregenKeys = StoreKeys{ + Root: rootPrivKey, + Store: storePrivKey, + Generic: genericPrivKey, + GenericModels: genericModelsPrivKey, + } +) + +// NewStoreStack creates a new store assertion stack. It panics on error. +// Optional keys specify private keys to use for the various roles. +func NewStoreStack(authorityID string, keys *StoreKeys) *StoreStack { + if keys == nil { + keys = &pregenKeys + } + + rootSigning := NewSigningDB(authorityID, keys.Root) + ts := time.Now().Format(time.RFC3339) + trustedAcct := NewAccount(rootSigning, authorityID, map[string]interface{}{ + "account-id": authorityID, + "validation": "verified", + "timestamp": ts, + }, "") + trustedKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{ + "name": "root", + "since": ts, + }, keys.Root.PublicKey(), "") + trusted := []asserts.Assertion{trustedAcct, trustedKey} + + genericAcct := NewAccount(rootSigning, "generic", map[string]interface{}{ + "account-id": "generic", + "validation": "verified", + "timestamp": ts, + }, "") + + err := rootSigning.ImportKey(keys.GenericModels) + if err != nil { + panic(err) + } + genericModelsKey := NewAccountKey(rootSigning, genericAcct, map[string]interface{}{ + "name": "models", + "since": ts, + }, keys.GenericModels.PublicKey(), "") + generic := []asserts.Assertion{genericAcct, genericModelsKey} + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: trusted, + OtherPredefined: generic, + }) + if err != nil { + panic(err) + } + err = db.ImportKey(keys.Store) + if err != nil { + panic(err) + } + storeKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{ + "name": "store", + }, keys.Store.PublicKey(), "") + err = db.Add(storeKey) + if err != nil { + panic(err) + } + + err = db.ImportKey(keys.Generic) + if err != nil { + panic(err) + } + genericKey := NewAccountKey(rootSigning, genericAcct, map[string]interface{}{ + "name": "serials", + "since": ts, + }, keys.Generic.PublicKey(), "") + err = db.Add(genericKey) + if err != nil { + panic(err) + } + + a, err := rootSigning.Sign(asserts.ModelType, map[string]interface{}{ + "authority-id": "generic", + "series": "16", + "brand-id": "generic", + "model": "generic-classic", + "classic": "true", + "timestamp": ts, + }, nil, genericModelsKey.PublicKeyID()) + if err != nil { + panic(err) + } + genericClassicMod := a.(*asserts.Model) + + return &StoreStack{ + TrustedAccount: trustedAcct, + TrustedKey: trustedKey, + Trusted: trusted, + + GenericAccount: genericAcct, + GenericKey: genericKey, + GenericModelsKey: genericModelsKey, + Generic: generic, + GenericClassicModel: genericClassicMod, + + 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 asserts.IsNotFound(err) { + 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..40a7c38e --- /dev/null +++ b/asserts/assertstest/assertstest_test.go @@ -0,0 +1,163 @@ +// -*- 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) { + store := assertstest.NewStoreStack("super", nil) + + c.Check(store.TrustedAccount.AccountID(), Equals, "super") + c.Check(store.TrustedAccount.Validation(), Equals, "verified") + + c.Check(store.TrustedKey.AccountID(), Equals, "super") + c.Check(store.TrustedKey.Name(), Equals, "root") + + c.Check(store.GenericAccount.AccountID(), Equals, "generic") + c.Check(store.GenericAccount.Validation(), Equals, "verified") + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + OtherPredefined: store.Generic, + }) + 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") + + c.Check(store.GenericKey.AccountID(), Equals, "generic") + c.Check(store.GenericKey.Name(), Equals, "serials") + + c.Check(store.GenericModelsKey.AccountID(), Equals, "generic") + c.Check(store.GenericModelsKey.Name(), Equals, "models") + + g, err := store.Find(asserts.AccountType, map[string]string{ + "account-id": "generic", + }) + c.Assert(err, IsNil) + c.Assert(g.Headers(), DeepEquals, store.GenericAccount.Headers()) + + g, err = store.Find(asserts.AccountKeyType, map[string]string{ + "public-key-sha3-384": store.GenericKey.PublicKeyID(), + }) + c.Assert(err, IsNil) + c.Assert(g.Headers(), DeepEquals, store.GenericKey.Headers()) + + g, err = store.Find(asserts.AccountKeyType, map[string]string{ + "public-key-sha3-384": store.GenericModelsKey.PublicKeyID(), + }) + c.Assert(err, IsNil) + c.Assert(g.Headers(), DeepEquals, store.GenericModelsKey.Headers()) + + acct := assertstest.NewAccount(store, "devel1", nil, "") + c.Check(acct.Username(), Equals, "devel1") + c.Check(acct.AccountID(), HasLen, 32) + c.Check(acct.Validation(), Equals, "unproven") + + 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") + + a, err := db.Find(asserts.AccountType, map[string]string{ + "account-id": "generic", + }) + c.Assert(err, IsNil) + c.Assert(a.Headers(), DeepEquals, store.GenericAccount.Headers()) + + c.Check(store.GenericClassicModel.AuthorityID(), Equals, "generic") + c.Check(store.GenericClassicModel.BrandID(), Equals, "generic") + c.Check(store.GenericClassicModel.Model(), Equals, "generic-classic") + c.Check(store.GenericClassicModel.Classic(), Equals, true) + err = db.Check(store.GenericClassicModel) + c.Assert(err, IsNil) + + err = db.Add(store.GenericKey) + c.Assert(err, IsNil) +} 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..14a4018d --- /dev/null +++ b/asserts/database.go @@ -0,0 +1,623 @@ +// -*- 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 ( + "fmt" + "regexp" + "time" +) + +// NotFoundError is returned when an assertion can not be found. +type NotFoundError struct { + Type *AssertionType + Headers map[string]string +} + +func (e *NotFoundError) Error() string { + pk, err := PrimaryKeyFromHeaders(e.Type, e.Headers) + if err != nil || len(e.Headers) != len(pk) { + // TODO: worth conveying more information? + return fmt.Sprintf("%s assertion not found", e.Type.Name) + } + + return fmt.Sprintf("%v not found", &Ref{Type: e.Type, PrimaryKey: pk}) +} + +// IsNotFound returns whether err is an assertion not found error. +func IsNotFound(err error) bool { + _, ok := err.(*NotFoundError) + return ok +} + +// 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 a + // NotFoundError, usually with omitted Headers. + 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, &NotFoundError{Type: t} +} + +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), + // used to establish root keys and trusted authorities + Trusted []Assertion + // predefined assertions but that do not establish foundational trust + OtherPredefined []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 +} + +// 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 a NotFoundError if the assertion cannot be found. + Find(assertionType *AssertionType, headers map[string]string) (Assertion, error) + // FindPredefined finds an assertion in the predefined sets + // (trusted or not) based on arbitrary headers. Provided + // headers must contain the primary key for the assertion + // type. It returns a NotFoundError if the assertion cannot + // be found. + FindPredefined(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 a + // NotFoundError if the assertion cannot be found. + FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error) + // FindMany finds assertions based on arbitrary headers. + // It returns a NotFoundError if no assertion can be found. + FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) + // FindManyPredefined finds assertions in the predefined sets + // (trusted or not) based on arbitrary headers. It returns a + // NotFoundError if no assertion can be found. + FindManyPredefined(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 + predefined 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("cannot predefine 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("cannot predefine trusted account %q: %v", acct.DisplayName(), err) + } + default: + return nil, fmt.Errorf("cannot predefine trusted assertions that are not account-key or account: %s", a.Type().Name) + } + } + + otherPredefinedBackstore := NewMemoryBackstore() + + for _, a := range cfg.OtherPredefined { + err := otherPredefinedBackstore.Put(a.Type(), a) + if err != nil { + return nil, fmt.Errorf("cannot predefine assertion %v: %v", a.Ref(), err) + } + } + + 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, + predefined: otherPredefinedBackstore, + // order here is relevant, Find* precedence and + // findAccountKey depend on it, trusted should win over the + // general backstore! + backstores: []Backstore{trustedBackstore, otherPredefinedBackstore, 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 !IsNotFound(err) { + return nil, err + } + } + return nil, &NotFoundError{Type: AccountKeyType} +} + +// 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 IsNotFound(err) { + 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 && !IsNotFound(err) { + 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 !IsNotFound(err) { + return fmt.Errorf("cannot add %q assertion with primary key clashing with a trusted assertion: %v", ref.Type.Name, ref.PrimaryKey) + } + + _, err = db.predefined.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat()) + if !IsNotFound(err) { + return fmt.Errorf("cannot add %q assertion with primary key clashing with a predefined 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, err := PrimaryKeyFromHeaders(assertionType, headers) + if err != nil { + return nil, err + } + + var assert Assertion + for _, bs := range backstores { + a, err := bs.Get(assertionType, keyValues, maxFormat) + if err == nil { + assert = a + break + } + if !IsNotFound(err) { + return nil, err + } + } + + if assert == nil || !searchMatch(assert, headers) { + return nil, &NotFoundError{Type: assertionType, Headers: headers} + } + + return assert, nil +} + +// Find an assertion based on arbitrary headers. +// Provided headers must contain the primary key for the assertion type. +// It returns a NotFoundError 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 a NotFoundError 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) +} + +// FindPredefined finds an assertion in the predefined sets (trusted +// or not) based on arbitrary headers. Provided headers must contain +// the primary key for the assertion type. It returns a NotFoundError +// if the assertion cannot be found. +func (db *Database) FindPredefined(assertionType *AssertionType, headers map[string]string) (Assertion, error) { + return find([]Backstore{db.trusted, db.predefined}, assertionType, headers, -1) +} + +// 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 a NotFoundError 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) +} + +func (db *Database) findMany(backstores []Backstore, 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 backstores { + err = bs.Search(assertionType, headers, foundCb, maxFormat) + if err != nil { + return nil, err + } + } + + if len(res) == 0 { + return nil, &NotFoundError{Type: assertionType, Headers: headers} + } + return res, nil +} + +// FindMany finds assertions based on arbitrary headers. +// It returns a NotFoundError if no assertion can be found. +func (db *Database) FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) { + return db.findMany(db.backstores, assertionType, headers) +} + +// FindManyPrefined finds assertions in the predefined sets (trusted +// or not) based on arbitrary headers. It returns a NotFoundError if +// no assertion can be found. +func (db *Database) FindManyPredefined(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) { + return db.findMany([]Backstore{db.trusted, db.predefined}, assertionType, headers) +} + +// 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) { + until := "" + if !signingKey.Until().IsZero() { + until = fmt.Sprintf(" until %q", signingKey.Until()) + } + return fmt.Errorf("%s assertion timestamp outside of signing key validity (key valid since %q%s)", assert.Type().Name, signingKey.Since(), until) + } + } + 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..a9255c56 --- /dev/null +++ b/asserts/database_test.go @@ -0,0 +1,1190 @@ +// -*- 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": "verified", + "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 predefine 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) + + headers := map[string]interface{}{ + "type": "account", + "authority-id": "canonical", + "account-id": "predefined", + "validation": "verified", + "display-name": "Predef", + "timestamp": time.Now().Format(time.RFC3339), + } + predefAcct, err := safs.signingDB.Sign(asserts.AccountType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + trustedKey := testPrivKey0 + cfg := &asserts.DatabaseConfig{ + Backstore: bs, + Trusted: []asserts.Assertion{ + asserts.BootstrapAccountForTest("canonical"), + asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey()), + }, + OtherPredefined: []asserts.Assertion{ + predefAcct, + }, + } + 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) TestSignInadequateFormat(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "format-1-feature": "true", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `cannot sign "test-only" assertion with format set to 0 lower than min format 1 covering included features`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestAddRefusesSelfSignedKey(c *C) { + aKey := testPrivKey2 + + aKeyEncoded, err := asserts.EncodePublicKey(aKey.PublicKey()) + c.Assert(err, IsNil) + + now := time.Now().UTC() + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "canonical", + "public-key-sha3-384": aKey.PublicKey().ID(), + "name": "default", + "since": now.Format(time.RFC3339), + } + acctKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, aKeyEncoded, aKey) + c.Assert(err, IsNil) + + // this must fail + err = safs.db.Add(acctKey) + c.Check(err, ErrorMatches, `no matching public key.*`) +} + +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) TestNotFoundError(c *C) { + err1 := &asserts.NotFoundError{ + Type: asserts.SnapDeclarationType, + Headers: map[string]string{ + "series": "16", + "snap-id": "snap-id", + }, + } + c.Check(asserts.IsNotFound(err1), Equals, true) + c.Check(err1.Error(), Equals, "snap-declaration (snap-id; series:16) not found") + + err2 := &asserts.NotFoundError{ + Type: asserts.SnapRevisionType, + } + c.Check(asserts.IsNotFound(err1), Equals, true) + c.Check(err2.Error(), Equals, "snap-revision assertion not found") +} + +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) + + hdrs := map[string]string{ + "primary-key": "b", + } + retrieved1, err := safs.db.Find(asserts.TestOnlyType, hdrs) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + Headers: hdrs, + }) + c.Check(retrieved1, IsNil) + + // checking also extra headers + hdrs = map[string]string{ + "primary-key": "a", + "authority-id": "other-auth-id", + } + retrieved1, err = safs.db.Find(asserts.TestOnlyType, hdrs) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + Headers: hdrs, + }) + 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) + + hdrs := map[string]string{ + "primary-key": "b", + "other": "other-x", + } + res, err = safs.db.FindMany(asserts.TestOnlyType, hdrs) + c.Assert(res, HasLen, 0) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + Headers: hdrs, + }) +} + +func (safs *signAddFindSuite) TestFindFindsPredefined(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 predefined account as well + predefAcct, err := safs.db.Find(asserts.AccountType, map[string]string{ + "account-id": "predefined", + }) + c.Assert(err, IsNil) + c.Assert(predefAcct.(*asserts.Account).AccountID(), Equals, "predefined") + c.Assert(predefAcct.(*asserts.Account).DisplayName(), Equals, "Predef") + + // find trusted and indirectly trusted + accKeys, err := safs.db.FindMany(asserts.AccountKeyType, nil) + c.Assert(err, IsNil) + c.Check(accKeys, HasLen, 2) + + accts, err := safs.db.FindMany(asserts.AccountType, nil) + c.Assert(err, IsNil) + c.Check(accts, HasLen, 3) +} + +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 + hdrs := map[string]string{ + "account-id": acct1.AccountID(), + } + _, err = safs.db.FindTrusted(asserts.AccountType, hdrs) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.AccountType, + Headers: hdrs, + }) + + hdrs = map[string]string{ + "account-id": acct1.AccountID(), + "public-key-sha3-384": acct1Key.PublicKeyID(), + } + _, err = safs.db.FindTrusted(asserts.AccountKeyType, hdrs) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.AccountKeyType, + Headers: hdrs, + }) + + _, err = safs.db.FindTrusted(asserts.AccountType, map[string]string{ + "account-id": "predefined", + }) + c.Check(asserts.IsNotFound(err), Equals, true) +} + +func (safs *signAddFindSuite) TestFindPredefined(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.FindPredefined(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.FindPredefined(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 predefined account as well + predefAcct, err := safs.db.FindPredefined(asserts.AccountType, map[string]string{ + "account-id": "predefined", + }) + c.Assert(err, IsNil) + c.Assert(predefAcct.(*asserts.Account).AccountID(), Equals, "predefined") + c.Assert(predefAcct.(*asserts.Account).DisplayName(), Equals, "Predef") + + // doesn't find not trusted or predefined assertions + hdrs := map[string]string{ + "account-id": acct1.AccountID(), + } + _, err = safs.db.FindPredefined(asserts.AccountType, hdrs) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.AccountType, + Headers: hdrs, + }) + + hdrs = map[string]string{ + "account-id": acct1.AccountID(), + "public-key-sha3-384": acct1Key.PublicKeyID(), + } + _, err = safs.db.FindPredefined(asserts.AccountKeyType, hdrs) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.AccountKeyType, + Headers: hdrs, + }) +} + +func (safs *signAddFindSuite) TestFindManyPredefined(c *C) { + headers := map[string]interface{}{ + "type": "account", + "authority-id": "canonical", + "account-id": "predefined", + "validation": "verified", + "display-name": "Predef", + "timestamp": time.Now().Format(time.RFC3339), + } + predefAcct, err := safs.signingDB.Sign(asserts.AccountType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + trustedKey0 := testPrivKey0 + trustedKey1 := testPrivKey1 + cfg := &asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: []asserts.Assertion{ + asserts.BootstrapAccountForTest("canonical"), + asserts.BootstrapAccountKeyForTest("canonical", trustedKey0.PublicKey()), + asserts.BootstrapAccountKeyForTest("canonical", trustedKey1.PublicKey()), + }, + OtherPredefined: []asserts.Assertion{ + predefAcct, + }, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + pk1 := testPrivKey2 + + 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 = db.Add(acct1) + c.Assert(err, IsNil) + err = db.Add(acct1Key) + c.Assert(err, IsNil) + + // find the trusted account + tAccts, err := db.FindManyPredefined(asserts.AccountType, map[string]string{ + "account-id": "canonical", + }) + c.Assert(err, IsNil) + c.Assert(tAccts, HasLen, 1) + c.Assert(tAccts[0].(*asserts.Account).AccountID(), Equals, "canonical") + + // find the predefined account + pAccts, err := db.FindManyPredefined(asserts.AccountType, map[string]string{ + "account-id": "predefined", + }) + c.Assert(err, IsNil) + c.Assert(pAccts, HasLen, 1) + c.Assert(pAccts[0].(*asserts.Account).AccountID(), Equals, "predefined") + + // find the multiple trusted keys + tKeys, err := db.FindManyPredefined(asserts.AccountKeyType, map[string]string{ + "account-id": "canonical", + }) + c.Assert(err, IsNil) + c.Assert(tKeys, HasLen, 2) + got := make(map[string]string) + for _, a := range tKeys { + acctKey := a.(*asserts.AccountKey) + got[acctKey.PublicKeyID()] = acctKey.AccountID() + } + c.Check(got, DeepEquals, map[string]string{ + trustedKey0.PublicKey().ID(): "canonical", + trustedKey1.PublicKey().ID(): "canonical", + }) + + // doesn't find not predefined assertions + hdrs := map[string]string{ + "account-id": acct1.AccountID(), + } + _, err = db.FindManyPredefined(asserts.AccountType, hdrs) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.AccountType, + Headers: hdrs, + }) + + _, err = db.FindManyPredefined(asserts.AccountKeyType, map[string]string{ + "account-id": acct1.AccountID(), + "public-key-sha3-384": acct1Key.PublicKeyID(), + }) + c.Check(asserts.IsNotFound(err), Equals, true) +} + +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) TestDontLetAddConfusinglyAssertionClashingWithPredefinedOnes(c *C) { + headers := map[string]interface{}{ + "type": "account", + "authority-id": "canonical", + "account-id": "predefined", + "validation": "verified", + "display-name": "Predef", + "timestamp": time.Now().Format(time.RFC3339), + } + predefAcct, err := safs.signingDB.Sign(asserts.AccountType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(predefAcct) + c.Check(err, ErrorMatches, `cannot add "account" assertion with primary key clashing with a predefined 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, DeepEquals, &asserts.NotFoundError{ + Type: ref.Type, + Headers: map[string]string{ + "pk1": "kb", + "pk2": "ka", + }, + }) +} + +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.NotFoundError{Type: asserts.TestOnlyType}, 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..f03a586b --- /dev/null +++ b/asserts/device_asserts.go @@ -0,0 +1,557 @@ +// -*- 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" + + "github.com/snapcore/snapd/strutil" +) + +// Model holds a model assertion, which is a statement by a brand +// about the properties of a device model. +type Model struct { + assertionBase + classic bool + 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") +} + +// Classic returns whether the model is a classic system. +func (mod *Model) Classic() bool { + return mod.classic +} + +// Architecture returns the archicteture the model is based on. +func (mod *Model) Architecture() string { + return mod.HeaderString("architecture") +} + +// snapWithTrack represents a snap that includes optional track +// information like `snapName=trackName` +type snapWithTrack string + +func (s snapWithTrack) Snap() string { + return strings.SplitN(string(s), "=", 2)[0] +} + +func (s snapWithTrack) Track() string { + l := strings.SplitN(string(s), "=", 2) + if len(l) > 1 { + return l[1] + } + return "" +} + +// Gadget returns the gadget snap the model uses. +func (mod *Model) Gadget() string { + return snapWithTrack(mod.HeaderString("gadget")).Snap() +} + +// GadgetTrack returns the gadget track the model uses. +func (mod *Model) GadgetTrack() string { + return snapWithTrack(mod.HeaderString("gadget")).Track() +} + +// Kernel returns the kernel snap the model uses. +func (mod *Model) Kernel() string { + return snapWithTrack(mod.HeaderString("kernel")).Snap() +} + +// KernelTrack returns the kernel track the model uses. +func (mod *Model) KernelTrack() string { + return snapWithTrack(mod.HeaderString("kernel")).Track() +} + +// Base returns the base snap the model uses. +func (mod *Model) Base() string { + return mod.HeaderString("base") +} + +// 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 checkSnapWithTrackHeader(header string, headers map[string]interface{}) error { + _, ok := headers[header] + if !ok { + return nil + } + value, ok := headers[header].(string) + if !ok { + return fmt.Errorf(`%q header must be a string`, header) + } + l := strings.SplitN(value, "=", 2) + + if err := validateSnapName(l[0], header); err != nil { + return err + } + if len(l) == 1 { + return nil + } + track := l[1] + if strings.Count(track, "/") != 0 { + return fmt.Errorf(`%q channel selector must be a track name only`, header) + } + channelRisks := []string{"stable", "candidate", "beta", "edge"} + if strutil.ListContains(channelRisks, track) { + return fmt.Errorf(`%q channel selector must be a track name`, header) + } + return nil +} + +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 +} + +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"} + classicModelOptional = []string{"architecture", "gadget"} +) + +var almostValidName = regexp.MustCompile("^[a-z0-9-]*[a-z][a-z0-9-]*$") + +// validateSnapName checks whether the name can be used as a snap name +// +// This function should be synchronized with the reference implementation +// snap.ValidateName() in snap/validate.go +func validateSnapName(name string, headerName string) error { + isValidName := func() bool { + if !almostValidName.MatchString(name) { + return false + } + if name[0] == '-' || name[len(name)-1] == '-' || strings.Contains(name, "--") { + return false + } + return true + } + + if len(name) > 40 || !isValidName() { + return fmt.Errorf("invalid snap name in %q header: %s", headerName, name) + } + return nil +} + +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 + } + + classic, err := checkOptionalBool(assert.headers, "classic") + if err != nil { + return nil, err + } + + if classic { + if _, ok := assert.headers["kernel"]; ok { + return nil, fmt.Errorf("cannot specify a kernel with a classic model") + } + if _, ok := assert.headers["base"]; ok { + return nil, fmt.Errorf("cannot specify a base with a classic model") + } + } + + checker := checkNotEmptyString + toCheck := modelMandatory + if classic { + checker = checkOptionalString + toCheck = classicModelOptional + } + + for _, h := range toCheck { + if _, err := checker(assert.headers, h); err != nil { + return nil, err + } + } + + // kernel/gadget must be valid snap names and can have (optional) tracks + // - validate those + if err := checkSnapWithTrackHeader("kernel", assert.headers); err != nil { + return nil, err + } + if err := checkSnapWithTrackHeader("gadget", assert.headers); err != nil { + return nil, err + } + // base, if provided, must be a valid snap name too + base, err := checkOptionalString(assert.headers, "base") + if err != nil { + return nil, err + } + if base != "" { + if err := validateSnapName(base, "base"); 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 + } + + // required snap must be valid snap names + reqSnaps, err := checkStringList(assert.headers, "required-snaps") + if err != nil { + return nil, err + } + for _, name := range reqSnaps { + if err := validateSnapName(name, "required-snaps"); 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, + classic: classic, + 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..6df3ae25 --- /dev/null +++ b/asserts/device_asserts_test.go @@ -0,0 +1,712 @@ +// -*- 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" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +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" + + "base: core18\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==" + + classicModelExample = "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" + + "classic: true\n" + + "architecture: amd64\n" + + "gadget: brand-gadget\n" + + "store: brand-store\n" + + 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.GadgetTrack(), Equals, "") + c.Check(model.Kernel(), Equals, "baz-linux") + c.Check(model.KernelTrack(), Equals, "") + c.Check(model.Base(), Equals, "core18") + 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) TestDecodeBaseIsOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, "base: core18\n", "base: \n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.Base(), Equals, "") + + encoded = strings.Replace(withTimestamp, "base: core18\n", "", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model = a.(*asserts.Model) + c.Check(model.Base(), 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) TestDecodeValidatesSnapNames(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n - foo_bar\n - bar\n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: foo_bar`) + + encoded = strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n - foo\n - bar-;;''\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: bar-;;''`) + + encoded = strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux_instance\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: baz-linux_instance`) + + encoded = strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget_instance\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "gadget" header: brand-gadget_instance`) + + encoded = strings.Replace(withTimestamp, "base: core18\n", "base: core18_instance\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "base" header: core18_instance`) +} + +func (mods modelSuite) TestDecodeValidSnapNames(c *C) { + // reuse test cases for snap.ValidateName() + + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + + validNames := []string{ + "a", "aa", "aaa", "aaaa", + "a-a", "aa-a", "a-aa", "a-b-c", + "a0", "a-0", "a-0a", + "01game", "1-or-2", + // a regexp stresser + "u-94903713687486543234157734673284536758", + } + for _, name := range validNames { + encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.Kernel(), Equals, name) + } + invalidNames := []string{ + // name cannot be empty, never reaches snap name validation + "", + // names cannot be too long + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", + "1111111111111111111111111111111111111111x", + "x1111111111111111111111111111111111111111", + "x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x", + // a regexp stresser + "u-9490371368748654323415773467328453675-", + // dashes alone are not a name + "-", "--", + // double dashes in a name are not allowed + "a--a", + // name should not end with a dash + "a-", + // name cannot have any spaces in it + "a ", " a", "a a", + // a number alone is not a name + "0", "123", + // identifier must be plain ASCII + "日本語", "한글", "ру́сский язы́к", + // instance names are invalid too + "foo_bar", "x_1", + } + for _, name := range invalidNames { + encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(a, IsNil) + if name != "" { + c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: .*`) + } else { + c.Assert(err, ErrorMatches, `assertion model: "kernel" header should not be empty`) + } + } +} + +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"}) +} + +func (mods *modelSuite) TestDecodeKernelTrack(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux=18\n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.Kernel(), Equals, "baz-linux") + c.Check(model.KernelTrack(), Equals, "18") +} + +func (mods *modelSuite) TestDecodeGadgetTrack(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget=18\n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.Gadget(), Equals, "brand-gadget") + c.Check(model.GadgetTrack(), Equals, "18") +} + +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`}, + {"gadget: brand-gadget\n", "gadget: brand-gadget=x/x/x\n", `"gadget" channel selector must be a track name only`}, + {"gadget: brand-gadget\n", "gadget: brand-gadget=stable\n", `"gadget" channel selector must be a track name`}, + {"gadget: brand-gadget\n", "gadget: brand-gadget=18/beta\n", `"gadget" channel selector must be a track name only`}, + {"gadget: brand-gadget\n", "gadget:\n - xyz \n", `"gadget" header must be a string`}, + {"kernel: baz-linux\n", "", `"kernel" header is mandatory`}, + {"kernel: baz-linux\n", "kernel: \n", `"kernel" header should not be empty`}, + {"kernel: baz-linux\n", "kernel: baz-linux=x/x/x\n", `"kernel" channel selector must be a track name only`}, + {"kernel: baz-linux\n", "kernel: baz-linux=stable\n", `"kernel" channel selector must be a track name`}, + {"kernel: baz-linux\n", "kernel: baz-linux=18/beta\n", `"kernel" channel selector must be a track name only`}, + {"kernel: baz-linux\n", "kernel:\n - xyz \n", `"kernel" header must be a string`}, + {"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, "brand-id1", 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, "brand-id1", 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 \(key valid since.*\)`) +} + +func (mods *modelSuite) TestClassicDecodeOK(c *C) { + encoded := strings.Replace(classicModelExample, "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.Classic(), Equals, true) + c.Check(model.Architecture(), Equals, "amd64") + c.Check(model.Gadget(), Equals, "brand-gadget") + c.Check(model.Kernel(), Equals, "") + c.Check(model.KernelTrack(), Equals, "") + c.Check(model.Store(), Equals, "brand-store") + c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"}) +} + +func (mods *modelSuite) TestClassicDecodeInvalid(c *C) { + encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1) + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"classic: true\n", "classic: foo\n", `"classic" header must be 'true' or 'false'`}, + {"architecture: amd64\n", "architecture:\n - foo\n", `"architecture" header must be a string`}, + {"gadget: brand-gadget\n", "gadget:\n - foo\n", `"gadget" header must be a string`}, + {"gadget: brand-gadget\n", "kernel: brand-kernel\n", `cannot specify a kernel with a classic model`}, + {"gadget: brand-gadget\n", "base: some-base\n", `cannot specify a base with a classic model`}, + } + + 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) TestClassicDecodeGadgetAndArchOptional(c *C) { + encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "gadget: brand-gadget\n", "", 1) + encoded = strings.Replace(encoded, "architecture: amd64\n", "", 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.Classic(), Equals, true) + c.Check(model.Architecture(), Equals, "") + c.Check(model.Gadget(), Equals, "") +} + +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: " + 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`}, + {"model: baz-3000\n", "model: _what\n", `"model" header contains invalid characters: "_what"`}, + {"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) TestSerialCheck(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) + ex, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + storeDB, db := makeStoreAndCheckDB(c) + brandDB := setup3rdPartySigning(c, "brand1", storeDB, db) + + tests := []struct { + signDB assertstest.SignerDB + brandID string + authID string + keyID string + }{ + {brandDB, brandDB.AuthorityID, "", brandDB.KeyID}, + } + + for _, test := range tests { + headers := ex.Headers() + headers["brand-id"] = test.brandID + if test.authID != "" { + headers["authority-id"] = test.authID + } else { + headers["authority-id"] = test.brandID + } + headers["timestamp"] = time.Now().Format(time.RFC3339) + serial, err := test.signDB.Sign(asserts.SerialType, headers, nil, test.keyID) + c.Assert(err, IsNil) + + err = db.Check(serial) + c.Check(err, IsNil) + } +} + +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..7e2e09f3 --- /dev/null +++ b/asserts/export_test.go @@ -0,0 +1,193 @@ +// -*- 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, + maxSigSize: maxSigSize, + defaultMaxBodySize: maxBodySize, + }).initBuffer() +} + +func BootstrapAccountForTest(authorityID string) *Account { + return &Account{ + assertionBase: assertionBase{ + headers: map[string]interface{}{ + "type": "account", + "authority-id": authorityID, + "account-id": authorityID, + "validation": "verified", + }, + }, + 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 + formatAnalyzer[TestOnlyType] = func(headers map[string]interface{}, _ []byte) (int, error) { + if _, ok := headers["format-1-feature"]; ok { + return 1, nil + } + return 0, nil + } +} + +// 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 +) + +type featureExposer interface { + feature(flabel string) bool +} + +func RuleFeature(rule featureExposer, flabel string) bool { + return rule.feature(flabel) +} diff --git a/asserts/fetcher.go b/asserts/fetcher.go new file mode 100644 index 00000000..0e353e35 --- /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 predefined assertion, in which case + // there is nothing to do + _, err := ref.Resolve(f.db.FindPredefined) + if err == nil { + return nil + } + if !IsNotFound(err) { + return err + } + u := ref.Unique() + switch f.fetched[u] { + case fetchSaved: + return nil // nothing to do + case fetchRetrieved: + return fmt.Errorf("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..36531e75 --- /dev/null +++ b/asserts/fetcher_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 ( + "crypto" + "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) { + s.storeSigning = assertstest.NewStoreStack("can0nical", nil) +} + +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 *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..632079a9 --- /dev/null +++ b/asserts/fsbackstore.go @@ -0,0 +1,221 @@ +// -*- 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 := assert.Ref().PrimaryKey + + 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() + + a, err := fsbs.currentAssertion(assertType, key, maxFormat) + if err == errNotFound { + return nil, &NotFoundError{Type: assertType} + } + return a, err +} + +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..003d9983 --- /dev/null +++ b/asserts/fsbackstore_test.go @@ -0,0 +1,258 @@ +// -*- 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, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + // Headers can be omitted by Backstores + }) + c.Check(a, IsNil) + + err = bs.Put(asserts.TestOnlyType, af2) + c.Assert(err, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 1) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + }) + c.Check(a, IsNil) + + 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..1d1d0b00 --- /dev/null +++ b/asserts/gpgkeypairmgr.go @@ -0,0 +1,353 @@ +// -*- 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" + "strings" + + "github.com/snapcore/snapd/osutil" +) + +func ensureGPGHomeDirectory() (string, error) { + real, err := osutil.RealUser() + if err != nil { + return "", err + } + + uid, gid, err := osutil.UidGid(real) + 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..30cbe346 --- /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", nil) + + 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..3a91089f --- /dev/null +++ b/asserts/header_checks.go @@ -0,0 +1,274 @@ +// -*- 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) { + return checkExistsStringWhat(headers, name, "header") +} + +func checkExistsStringWhat(m map[string]interface{}, name, what string) (string, error) { + value, ok := m[name] + if !ok { + return "", fmt.Errorf("%q %s is mandatory", name, what) + } + s, ok := value.(string) + if !ok { + return "", fmt.Errorf("%q %s must be a string", name, what) + } + return s, nil +} + +func checkNotEmptyString(headers map[string]interface{}, name string) (string, error) { + return checkNotEmptyStringWhat(headers, name, "header") +} + +func checkNotEmptyStringWhat(m map[string]interface{}, name, what string) (string, error) { + s, err := checkExistsStringWhat(m, name, what) + if err != nil { + return "", err + } + if len(s) == 0 { + return "", fmt.Errorf("%q %s should not be empty", name, what) + } + 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) { + return checkRFC3339DateWhat(headers, name, "header") +} + +func checkRFC3339DateWhat(m map[string]interface{}, name, what string) (time.Time, error) { + dateStr, err := checkNotEmptyStringWhat(m, name, what) + if err != nil { + return time.Time{}, err + } + date, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + return time.Time{}, fmt.Errorf("%q %s is not a RFC3339 date: %v", name, what, err) + } + return date, nil +} + +func checkRFC3339DateWithDefault(headers map[string]interface{}, name string, defl time.Time) (time.Time, error) { + return checkRFC3339DateWithDefaultWhat(headers, name, "header", defl) +} + +func checkRFC3339DateWithDefaultWhat(m map[string]interface{}, name, what string, defl time.Time) (time.Time, error) { + value, ok := m[name] + if !ok { + return defl, nil + } + dateStr, ok := value.(string) + if !ok { + return time.Time{}, fmt.Errorf("%q %s must be a string", name, what) + } + date, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + return time.Time{}, fmt.Errorf("%q %s is not a RFC3339 date: %v", name, what, 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 +} + +// checkStringListInMap returns the `name` entry in the `m` map as a (possibly nil) `[]string` +// if `m` has an entry for `name` and it isn't a `[]string`, an error is returned +// if pattern is not nil, all the strings must match that pattern, otherwise an error is returned +// `what` is a descriptor, used for error messages +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 != nil && !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, nil) +} + +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) { + return checkStringMatchesWhat(headers, name, "header", pattern) +} + +func checkStringMatchesWhat(headers map[string]interface{}, name, what string, pattern *regexp.Regexp) (string, error) { + s, err := checkNotEmptyStringWhat(headers, name, what) + if err != nil { + return "", err + } + if !pattern.MatchString(s) { + return "", fmt.Errorf("%q %s contains invalid characters: %q", name, what, 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..6b4cbe09 --- /dev/null +++ b/asserts/ifacedecls.go @@ -0,0 +1,1099 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * 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 . + * + */ + +package asserts + +import ( + "errors" + "fmt" + "reflect" + "regexp" + "strconv" + "strings" +) + +// AttrMatchContext has contextual helpers for evaluating attribute constraints. +type AttrMatchContext interface { + PlugAttr(arg string) (interface{}, error) + SlotAttr(arg string) (interface{}, error) +} + +const ( + // feature label for $SLOT()/$PLUG()/$MISSING + dollarAttrConstraintsFeature = "dollar-attr-constraints" + // feature label for on-store/on-brand/on-model + deviceScopeConstraintsFeature = "device-scope-constraints" +) + +type attrMatcher interface { + match(apath string, v interface{}, ctx AttrMatchContext) error + + feature(flabel string) bool +} + +func chain(path, k string) string { + if path == "" { + return k + } + return fmt.Sprintf("%s.%s", path, 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") + } + if strings.HasPrefix(x, "$") { + if x == "$MISSING" { + return missingAttrMatcher{}, nil + } + return compileEvalAttrMatcher(cc, x) + } + 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(apath, k string, matcher1 attrMatcher, v interface{}, ctx AttrMatchContext) error { + apath = chain(apath, k) + // every entry matcher expects the attribute to be set except for $MISSING + if _, ok := matcher1.(missingAttrMatcher); !ok && v == nil { + return fmt.Errorf("attribute %q has constraints but is unset", apath) + } + if err := matcher1.match(apath, v, ctx); err != nil { + return err + } + return nil +} + +func matchList(apath string, matcher attrMatcher, l []interface{}, ctx AttrMatchContext) error { + for i, elem := range l { + if err := matcher.match(chain(apath, strconv.Itoa(i)), elem, ctx); err != nil { + return err + } + } + return nil +} + +func (matcher mapAttrMatcher) feature(flabel string) bool { + for _, matcher1 := range matcher { + if matcher1.feature(flabel) { + return true + } + } + return false +} + +func (matcher mapAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + switch x := v.(type) { + case Attrer: + // we get Atter from root-level Check (apath is "") + for k, matcher1 := range matcher { + v, _ := x.Lookup(k) + if err := matchEntry("", k, matcher1, v, ctx); err != nil { + return err + } + } + case map[string]interface{}: // maps in attributes look like this + for k, matcher1 := range matcher { + if err := matchEntry(apath, k, matcher1, x[k], ctx); err != nil { + return err + } + } + case []interface{}: + return matchList(apath, matcher, x, ctx) + default: + return fmt.Errorf("attribute %q must be a map", apath) + } + return nil +} + +type missingAttrMatcher struct{} + +func (matcher missingAttrMatcher) feature(flabel string) bool { + return flabel == dollarAttrConstraintsFeature +} + +func (matcher missingAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + if v != nil { + return fmt.Errorf("attribute %q is constrained to be missing but is set", apath) + } + return nil +} + +type evalAttrMatcher struct { + // first iteration supports just $(SLOT|PLUG)(arg) + op string + arg string +} + +var ( + validEvalAttrMatcher = regexp.MustCompile(`^\$(SLOT|PLUG)\((.+)\)$`) +) + +func compileEvalAttrMatcher(cc compileContext, s string) (attrMatcher, error) { + ops := validEvalAttrMatcher.FindStringSubmatch(s) + if len(ops) == 0 { + return nil, fmt.Errorf("cannot compile %q constraint %q: not a valid $SLOT()/$PLUG() constraint", cc, s) + } + return evalAttrMatcher{ + op: ops[1], + arg: ops[2], + }, nil +} + +func (matcher evalAttrMatcher) feature(flabel string) bool { + return flabel == dollarAttrConstraintsFeature +} + +func (matcher evalAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + if ctx == nil { + return fmt.Errorf("attribute %q cannot be matched without context", apath) + } + var comp func(string) (interface{}, error) + switch matcher.op { + case "SLOT": + comp = ctx.SlotAttr + case "PLUG": + comp = ctx.PlugAttr + } + v1, err := comp(matcher.arg) + if err != nil { + return fmt.Errorf("attribute %q constraint $%s(%s) cannot be evaluated: %v", apath, matcher.op, matcher.arg, err) + } + if !reflect.DeepEqual(v, v1) { + return fmt.Errorf("attribute %q does not match $%s(%s): %v != %v", apath, matcher.op, matcher.arg, v, v1) + } + 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) feature(flabel string) bool { + return false +} + +func (matcher regexpAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) 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(apath, matcher, x, ctx) + default: + return fmt.Errorf("attribute %q must be a scalar or list", apath) + } + if !matcher.Regexp.MatchString(s) { + return fmt.Errorf("attribute %q value %q does not match %v", apath, 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) feature(flabel string) bool { + for _, alt := range matcher.alts { + if alt.feature(flabel) { + return true + } + } + return false +} + +func (matcher altAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + var firstErr error + for _, alt := range matcher.alts { + err := alt.match(apath, v, ctx) + if err == nil { + return nil + } + if firstErr == nil { + firstErr = err + } + } + apathDescr := "" + if apath != "" { + apathDescr = fmt.Sprintf(" for attribute %q", apath) + } + return fmt.Errorf("no alternative%s matches: %v", apathDescr, firstErr) +} + +// AttributeConstraints implements a set of constraints on the attributes of a slot or plug. +type AttributeConstraints struct { + matcher attrMatcher +} + +func (ac *AttributeConstraints) feature(flabel string) bool { + return ac.matcher.feature(flabel) +} + +// 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) feature(flabel string) bool { + return false +} + +func (matcher fixedAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + return matcher.result +} + +var ( + AlwaysMatchAttributes = &AttributeConstraints{matcher: fixedAttrMatcher{nil}} + NeverMatchAttributes = &AttributeConstraints{matcher: fixedAttrMatcher{errors.New("not allowed")}} +) + +// Attrer reflects part of the Attrer interface (see interfaces.Attrer). +type Attrer interface { + Lookup(path string) (interface{}, bool) +} + +// Check checks whether attrs don't match the constraints. +func (c *AttributeConstraints) Check(attrer Attrer, ctx AttrMatchContext) error { + return c.matcher.match("", attrer, ctx) +} + +// OnClassicConstraint specifies a constraint based whether the system is classic and optional specific distros' sets. +type OnClassicConstraint struct { + Classic bool + SystemIDs []string +} + +// DeviceScopeConstraint specifies a constraints based on which brand +// store, brand or model the device belongs to. +type DeviceScopeConstraint struct { + Store []string + Brand []string + // Model is a list of precise "/" constraints + Model []string +} + +var ( + validStoreID = regexp.MustCompile("^[-A-Z0-9a-z_]+$") + validBrandSlashModel = regexp.MustCompile("^(" + + strings.Trim(validAccountID.String(), "^$") + + ")/(" + + strings.Trim(validModel.String(), "^$") + + ")$") + deviceScopeConstraints = map[string]*regexp.Regexp{ + "on-store": validStoreID, + "on-brand": validAccountID, + // on-model constraints are of the form list of + // / strings where are account + // IDs as they appear in the respective model assertion + "on-model": validBrandSlashModel, + } +) + +func detectDeviceScopeConstraint(cMap map[string]interface{}) bool { + // for consistency and simplicity we support all of on-store, + // on-brand, and on-model to appear together. The interpretation + // layer will AND them as usual + for field := range deviceScopeConstraints { + if cMap[field] != nil { + return true + } + } + return false +} + +func compileDeviceScopeConstraint(cMap map[string]interface{}, context string) (constr *DeviceScopeConstraint, err error) { + // initial map size of 2: we expect usual cases to have just one of the + // constraints or rarely 2 + deviceConstr := make(map[string][]string, 2) + for field, validRegexp := range deviceScopeConstraints { + vals, err := checkStringListInMap(cMap, field, fmt.Sprintf("%s in %s", field, context), validRegexp) + if err != nil { + return nil, err + } + deviceConstr[field] = vals + } + + if len(deviceConstr) == 0 { + return nil, fmt.Errorf("internal error: misdetected device scope constraints in %s", context) + } + return &DeviceScopeConstraint{ + Store: deviceConstr["on-store"], + Brand: deviceConstr["on-brand"], + Model: deviceConstr["on-model"], + }, nil +} + +// 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) + setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) +} + +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 !detectDeviceScopeConstraint(cMap) { + defaultUsed++ + } else { + c, err := compileDeviceScopeConstraint(cMap, context) + if err != nil { + return err + } + target.setDeviceScopeConstraint(c) + } + // checks whether defaults have been used for everything, which is not + // well-formed + // +1+1 accounts for defaults for missing on-classic plus missing + // on-store/on-brand/on-model + if defaultUsed == len(attributeConstraints)+len(idConstraints)+1+1 { + return fmt.Errorf("%s must specify at least one of %s, %s, on-classic, on-store, on-brand, on-model", 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 (r *PlugRule) feature(flabel string) bool { + for _, cs := range [][]*PlugInstallationConstraints{r.AllowInstallation, r.DenyInstallation} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + for _, cs := range [][]*PlugConnectionConstraints{r.AllowConnection, r.DenyConnection, r.AllowAutoConnection, r.DenyAutoConnection} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + return false +} + +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 + + DeviceScope *DeviceScopeConstraint +} + +func (c *PlugInstallationConstraints) feature(flabel string) bool { + if flabel == deviceScopeConstraintsFeature { + return c.DeviceScope != nil + } + return c.PlugAttributes.feature(flabel) +} + +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 (c *PlugInstallationConstraints) setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) { + c.DeviceScope = deviceScope +} + +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 + + DeviceScope *DeviceScopeConstraint +} + +func (c *PlugConnectionConstraints) feature(flabel string) bool { + if flabel == deviceScopeConstraintsFeature { + return c.DeviceScope != nil + } + return c.PlugAttributes.feature(flabel) || c.SlotAttributes.feature(flabel) +} + +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 +} + +func (c *PlugConnectionConstraints) setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) { + c.DeviceScope = deviceScope +} + +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 (r *SlotRule) feature(flabel string) bool { + for _, cs := range [][]*SlotInstallationConstraints{r.AllowInstallation, r.DenyInstallation} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + for _, cs := range [][]*SlotConnectionConstraints{r.AllowConnection, r.DenyConnection, r.AllowAutoConnection, r.DenyAutoConnection} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + return false +} + +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 + + DeviceScope *DeviceScopeConstraint +} + +func (c *SlotInstallationConstraints) feature(flabel string) bool { + if flabel == deviceScopeConstraintsFeature { + return c.DeviceScope != nil + } + return c.SlotAttributes.feature(flabel) +} + +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 (c *SlotInstallationConstraints) setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) { + c.DeviceScope = deviceScope +} + +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 + + DeviceScope *DeviceScopeConstraint +} + +func (c *SlotConnectionConstraints) feature(flabel string) bool { + if flabel == deviceScopeConstraintsFeature { + return c.DeviceScope != nil + } + return c.PlugAttributes.feature(flabel) || c.SlotAttributes.feature(flabel) +} + +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 (c *SlotConnectionConstraints) setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) { + c.DeviceScope = deviceScope +} + +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..cb5d74ff --- /dev/null +++ b/asserts/ifacedecls_test.go @@ -0,0 +1,1812 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * 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 . + * + */ + +package asserts_test + +import ( + "fmt" + "regexp" + + . "gopkg.in/check.v1" + "gopkg.in/yaml.v2" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/snap" + + "github.com/snapcore/snapd/testutil" +) + +var ( + _ = Suite(&attrConstraintsSuite{}) + _ = Suite(&plugSlotRulesSuite{}) +) + +type attrConstraintsSuite struct { + testutil.BaseTest +} + +type attrerObject map[string]interface{} + +func (o attrerObject) Lookup(path string) (interface{}, bool) { + v, ok := o[path] + return v, ok +} + +func attrs(yml string) *attrerObject { + 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) + } + + // NOTE: it's important to go through snap yaml here even though we're really interested in Attrs only, + // as InfoFromSnapYaml normalizes yaml values. + info, err := snap.InfoFromSnapYaml(snapYaml) + if err != nil { + panic(err) + } + + var ao attrerObject + ao = info.Plugs["plug"].Attrs + return &ao +} + +func (s *attrConstraintsSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) +} + +func (s *attrConstraintsSuite) TearDownTest(c *C) { + s.BaseTest.TearDownTest(c) +} + +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) + + plug := attrerObject(map[string]interface{}{ + "foo": "FOO", + "bar": "BAR", + "baz": "BAZ", + }) + err = cstrs.Check(plug, nil) + c.Check(err, IsNil) + + plug = attrerObject(map[string]interface{}{ + "foo": "FOO", + "bar": "BAZ", + "baz": "BAZ", + }) + err = cstrs.Check(plug, nil) + c.Check(err, ErrorMatches, `attribute "bar" value "BAZ" does not match \^\(BAR\)\$`) + + plug = attrerObject(map[string]interface{}{ + "foo": "FOO", + "baz": "BAZ", + }) + err = cstrs.Check(plug, nil) + 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) + + plug := attrerObject(map[string]interface{}{ + "bar": "BAR", + }) + err = cstrs.Check(plug, nil) + c.Check(err, IsNil) + + plug = attrerObject(map[string]interface{}{ + "bar": "BARR", + }) + err = cstrs.Check(plug, nil) + c.Check(err, ErrorMatches, `attribute "bar" value "BARR" does not match \^\(BAR|BAZ\)\$`) + + plug = attrerObject(map[string]interface{}{ + "bar": "BBAZ", + }) + err = cstrs.Check(plug, nil) + c.Check(err, ErrorMatches, `attribute "bar" value "BAZZ" does not match \^\(BAR|BAZ\)\$`) + + plug = attrerObject(map[string]interface{}{ + "bar": "BABAZ", + }) + err = cstrs.Check(plug, nil) + c.Check(err, ErrorMatches, `attribute "bar" value "BABAZ" does not match \^\(BAR|BAZ\)\$`) + + plug = attrerObject(map[string]interface{}{ + "bar": "BARAZ", + }) + err = cstrs.Check(plug, nil) + 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 +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: BAZ +baz: BAZ +`), nil) + 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 +`), nil) + 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 +`), nil) + 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) + + plug := attrerObject(map[string]interface{}{ + "foo": "FOO", + "bar": "BAR", + "baz": "BAZ", + }) + err = cstrs.Check(plug, nil) + c.Check(err, IsNil) + + plug = attrerObject(map[string]interface{}{ + "foo": "FOO", + "bar": "BAZ", + "baz": "BAZ", + }) + err = cstrs.Check(plug, nil) + c.Check(err, IsNil) + + plug = attrerObject(map[string]interface{}{ + "foo": "FOO", + "bar": "BARR", + "baz": "BAR", + }) + err = cstrs.Check(plug, nil) + 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 +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR22 +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR3 +`), nil) + 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 +`), nil) + c.Check(err, IsNil) + + plug := attrerObject(map[string]interface{}{ + "foo": int64(1), + "bar": true, + }) + err = cstrs.Check(plug, nil) + 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`) + + wrongDollarConstraints := []string{ + "$", + "$FOO(a)", + "$SLOT", + "$SLOT()", + } + + for _, wrong := range wrongDollarConstraints { + _, err := asserts.CompileAttributeConstraints(map[string]interface{}{ + "foo": wrong, + }) + c.Check(err, ErrorMatches, fmt.Sprintf(`cannot compile "foo" constraint "%s": not a valid \$SLOT\(\)/\$PLUG\(\) constraint`, regexp.QuoteMeta(wrong))) + + } +} + +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"] +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: ["/foo/x", "/foo"] +`), nil) + c.Check(err, ErrorMatches, `attribute "foo\.1" value "/foo" does not match \^\(/foo/\.\*\)\$`) +} + +func (s *attrConstraintsSuite) TestMissingCheck(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: $MISSING`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(cstrs, "dollar-attr-constraints"), Equals, true) + + err = cstrs.Check(attrs(` +bar: baz +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: ["x"] +`), nil) + c.Check(err, ErrorMatches, `attribute "foo" is constrained to be missing but is set`) +} + +type testEvalAttr struct { + comp func(side string, arg string) (interface{}, error) +} + +func (ca testEvalAttr) SlotAttr(arg string) (interface{}, error) { + return ca.comp("slot", arg) +} + +func (ca testEvalAttr) PlugAttr(arg string) (interface{}, error) { + return ca.comp("plug", arg) +} + +func (s *attrConstraintsSuite) TestEvalCheck(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: $SLOT(foo) + bar: $PLUG(bar.baz)`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(cstrs, "dollar-attr-constraints"), Equals, true) + + err = cstrs.Check(attrs(` +foo: foo +bar: bar +`), nil) + c.Check(err, ErrorMatches, `attribute "(foo|bar)" cannot be matched without context`) + + calls := make(map[[2]string]bool) + comp1 := func(op string, arg string) (interface{}, error) { + calls[[2]string{op, arg}] = true + return arg, nil + } + + err = cstrs.Check(attrs(` +foo: foo +bar: bar.baz +`), testEvalAttr{comp1}) + c.Check(err, IsNil) + + c.Check(calls, DeepEquals, map[[2]string]bool{ + {"slot", "foo"}: true, + {"plug", "bar.baz"}: true, + }) + + comp2 := func(op string, arg string) (interface{}, error) { + if op == "plug" { + return nil, fmt.Errorf("boom") + } + return arg, nil + } + + err = cstrs.Check(attrs(` +foo: foo +bar: bar.baz +`), testEvalAttr{comp2}) + c.Check(err, ErrorMatches, `attribute "bar" constraint \$PLUG\(bar\.baz\) cannot be evaluated: boom`) + + comp3 := func(op string, arg string) (interface{}, error) { + if op == "slot" { + return "other-value", nil + } + return arg, nil + } + + err = cstrs.Check(attrs(` +foo: foo +bar: bar.baz +`), testEvalAttr{comp3}) + c.Check(err, ErrorMatches, `attribute "foo" does not match \$SLOT\(foo\): foo != other-value`) +} + +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"}] +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: [{p: "zzz"}, {p: "/foo/y"}] +`), nil) + 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, nil), IsNil) +} + +func (s *attrConstraintsSuite) TestNeverMatchAttributeConstraints(c *C) { + c.Check(asserts.NeverMatchAttributes.Check(nil, nil), NotNil) +} + +type plugSlotRulesSuite struct{} + +func checkAttrs(c *C, attrs *asserts.AttributeConstraints, witness, expected string) { + plug := attrerObject(map[string]interface{}{ + witness: "XYZ", + }) + c.Check(attrs.Check(plug, nil), ErrorMatches, fmt.Sprintf(`attribute "%s".*does not match.*`, witness)) + plug = attrerObject(map[string]interface{}{ + witness: expected, + }) + c.Check(attrs.Check(plug, nil), 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) TestCompilePlugRuleInstallationConstraintsDeviceScope(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].DeviceScope, IsNil) + + tests := []struct { + rule string + expected asserts.DeviceScopeConstraint + }{ + {`iface: + allow-installation: + on-store: + - my-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store"}}}, + {`iface: + allow-installation: + on-store: + - my-store + - other-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store", "other-store"}}}, + {`iface: + allow-installation: + on-brand: + - my-brand + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT`, asserts.DeviceScopeConstraint{Brand: []string{"my-brand", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT"}}}, + {`iface: + allow-installation: + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + {`iface: + allow-installation: + on-store: + - store1 + - store2 + on-brand: + - my-brand + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{ + Store: []string{"store1", "store2"}, + Brand: []string{"my-brand"}, + Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + } + + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].DeviceScope, DeepEquals, &t.expected) + } +} + +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) TestCompilePlugRuleConnectionConstraintsDeviceScope(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.AllowInstallation[0].DeviceScope, IsNil) + + tests := []struct { + rule string + expected asserts.DeviceScopeConstraint + }{ + {`iface: + allow-connection: + on-store: + - my-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store"}}}, + {`iface: + allow-connection: + on-store: + - my-store + - other-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store", "other-store"}}}, + {`iface: + allow-connection: + on-brand: + - my-brand + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT`, asserts.DeviceScopeConstraint{Brand: []string{"my-brand", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT"}}}, + {`iface: + allow-connection: + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + {`iface: + allow-connection: + on-store: + - store1 + - store2 + on-brand: + - my-brand + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{ + Store: []string{"store1", "store2"}, + Brand: []string{"my-brand"}, + Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + } + + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].DeviceScope, DeepEquals, &t.expected) + } +} + +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, on-store, on-brand, on-model`}, + {`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, on-store, on-brand, on-model`}, + {`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, on-store, on-brand, on-model`}, + {`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, on-store, on-brand, on-model`}, + {`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`}, + {`iface: + allow-installation: + on-store: true`, `on-store in allow-installation in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-installation: + on-store: store1`, `on-store in allow-installation in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-installation: + on-store: + - zoom!`, `on-store in allow-installation in plug rule for interface \"iface\" contains an invalid element: \"zoom!\"`}, + {`iface: + allow-connection: + on-brand: true`, `on-brand in allow-connection in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-connection: + on-brand: brand1`, `on-brand in allow-connection in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-connection: + on-brand: + - zoom!`, `on-brand in allow-connection in plug rule for interface \"iface\" contains an invalid element: \"zoom!\"`}, + {`iface: + allow-auto-connection: + on-model: true`, `on-model in allow-auto-connection in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-auto-connection: + on-model: foo/bar`, `on-model in allow-auto-connection in plug rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-auto-connection: + on-model: + - foo/!qz`, `on-model in allow-auto-connection in plug rule for interface \"iface\" contains an invalid element: \"foo/!qz"`}, + } + + 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)) + } +} + +var ( + deviceScopeConstrs = map[string][]interface{}{ + "on-store": {"store"}, + "on-brand": {"brand"}, + "on-model": {"brand/model"}, + } +) + +func (s *plugSlotRulesSuite) TestPlugRuleFeatures(c *C) { + combos := []struct { + subrule string + attrConstraints []string + }{ + {"allow-installation", []string{"plug-attributes"}}, + {"deny-installation", []string{"plug-attributes"}}, + {"allow-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-connection", []string{"plug-attributes", "slot-attributes"}}, + {"allow-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + } + + for _, combo := range combos { + for _, attrConstr := range combo.attrConstraints { + attrConstraintMap := map[string]interface{}{ + "a": "ATTR", + "other": []interface{}{"x", "y"}, + } + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + attrConstr: attrConstraintMap, + }, + } + + rule, err := asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, false, Commentf("%v", ruleMap)) + + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, false, Commentf("%v", ruleMap)) + + attrConstraintMap["a"] = "$MISSING" + rule, err = asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + + // covers also alternation + attrConstraintMap["a"] = []interface{}{"$SLOT(a)"} + rule, err = asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, false, Commentf("%v", ruleMap)) + + } + + for deviceScopeConstr, value := range deviceScopeConstrs { + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + deviceScopeConstr: value, + }, + } + + rule, err := asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, true, Commentf("%v", ruleMap)) + } + } +} + +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) TestCompileSlotRuleInstallationConstraintsDeviceScope(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].DeviceScope, IsNil) + + tests := []struct { + rule string + expected asserts.DeviceScopeConstraint + }{ + {`iface: + allow-installation: + on-store: + - my-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store"}}}, + {`iface: + allow-installation: + on-store: + - my-store + - other-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store", "other-store"}}}, + {`iface: + allow-installation: + on-brand: + - my-brand + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT`, asserts.DeviceScopeConstraint{Brand: []string{"my-brand", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT"}}}, + {`iface: + allow-installation: + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + {`iface: + allow-installation: + on-store: + - store1 + - store2 + on-brand: + - my-brand + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{ + Store: []string{"store1", "store2"}, + Brand: []string{"my-brand"}, + Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + } + + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].DeviceScope, DeepEquals, &t.expected) + } +} + +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) TestCompileSlotRuleConnectionConstraintsDeviceScope(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].DeviceScope, IsNil) + + tests := []struct { + rule string + expected asserts.DeviceScopeConstraint + }{ + {`iface: + allow-connection: + on-store: + - my-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store"}}}, + {`iface: + allow-connection: + on-store: + - my-store + - other-store`, asserts.DeviceScopeConstraint{Store: []string{"my-store", "other-store"}}}, + {`iface: + allow-connection: + on-brand: + - my-brand + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT`, asserts.DeviceScopeConstraint{Brand: []string{"my-brand", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT"}}}, + {`iface: + allow-connection: + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + {`iface: + allow-connection: + on-store: + - store1 + - store2 + on-brand: + - my-brand + on-model: + - my-brand/bar + - s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz`, asserts.DeviceScopeConstraint{ + Store: []string{"store1", "store2"}, + Brand: []string{"my-brand"}, + Model: []string{"my-brand/bar", "s9zGdwb16ysLeRW6nRivwZS5r9puP8JT/baz"}}}, + } + + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].DeviceScope, DeepEquals, &t.expected) + } +} + +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, on-store, on-brand, on-model`}, + {`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, on-store, on-brand, on-model`}, + {`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, on-store, on-brand, on-model`}, + {`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, on-store, on-brand, on-model`}, + {`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`}, + {`iface: + allow-installation: + on-store: true`, `on-store in allow-installation in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-installation: + on-store: store1`, `on-store in allow-installation in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-installation: + on-store: + - zoom!`, `on-store in allow-installation in slot rule for interface \"iface\" contains an invalid element: \"zoom!\"`}, + {`iface: + allow-connection: + on-brand: true`, `on-brand in allow-connection in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-connection: + on-brand: brand1`, `on-brand in allow-connection in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-connection: + on-brand: + - zoom!`, `on-brand in allow-connection in slot rule for interface \"iface\" contains an invalid element: \"zoom!\"`}, + {`iface: + allow-auto-connection: + on-model: true`, `on-model in allow-auto-connection in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-auto-connection: + on-model: foo/bar`, `on-model in allow-auto-connection in slot rule for interface \"iface\" must be a list of strings`}, + {`iface: + allow-auto-connection: + on-model: + - foo//bar`, `on-model in allow-auto-connection in slot rule for interface \"iface\" contains an invalid element: \"foo//bar"`}, + } + + 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)) + } +} + +func (s *plugSlotRulesSuite) TestSlotRuleFeatures(c *C) { + combos := []struct { + subrule string + attrConstraints []string + }{ + {"allow-installation", []string{"slot-attributes"}}, + {"deny-installation", []string{"slot-attributes"}}, + {"allow-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-connection", []string{"plug-attributes", "slot-attributes"}}, + {"allow-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + } + + for _, combo := range combos { + for _, attrConstr := range combo.attrConstraints { + attrConstraintMap := map[string]interface{}{ + "a": "ATTR", + } + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + attrConstr: attrConstraintMap, + }, + } + + rule, err := asserts.CompileSlotRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, false, Commentf("%v", ruleMap)) + + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, false, Commentf("%v", ruleMap)) + + attrConstraintMap["a"] = "$PLUG(a)" + rule, err = asserts.CompileSlotRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, false, Commentf("%v", ruleMap)) + } + + for deviceScopeConstr, value := range deviceScopeConstrs { + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + deviceScopeConstr: value, + }, + } + + rule, err := asserts.CompileSlotRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, true, Commentf("%v", ruleMap)) + } + } +} + +func (s *plugSlotRulesSuite) TestValidOnStoreBrandModel(c *C) { + tests := []struct { + constr string + value string + valid bool + }{ + {"on-store", "", false}, + {"on-store", "foo", true}, + {"on-store", "F_o-O88", true}, + {"on-store", "foo!", false}, + {"on-store", "foo.", false}, + {"on-store", "foo/", false}, + {"on-brand", "", false}, + // custom set brands (length 2-28) + {"on-brand", "dwell", true}, + {"on-brand", "Dwell", false}, + {"on-brand", "dwell-88", true}, + {"on-brand", "dwell_88", false}, + {"on-brand", "dwell.88", false}, + {"on-brand", "dwell:88", false}, + {"on-brand", "dwell!88", false}, + {"on-brand", "a", false}, + {"on-brand", "ab", true}, + {"on-brand", "0123456789012345678901234567", true}, + // snappy id brands (fixed length 32) + {"on-brand", "01234567890123456789012345678", false}, + {"on-brand", "012345678901234567890123456789", false}, + {"on-brand", "0123456789012345678901234567890", false}, + {"on-brand", "01234567890123456789012345678901", true}, + {"on-brand", "abcdefghijklmnopqrstuvwxyz678901", true}, + {"on-brand", "ABCDEFGHIJKLMNOPQRSTUVWCYZ678901", true}, + {"on-brand", "ABCDEFGHIJKLMNOPQRSTUVWCYZ678901X", false}, + {"on-brand", "ABCDEFGHIJKLMNOPQ!STUVWCYZ678901", false}, + {"on-brand", "ABCDEFGHIJKLMNOPQ_STUVWCYZ678901", false}, + {"on-brand", "ABCDEFGHIJKLMNOPQ-STUVWCYZ678901", false}, + {"on-model", "", false}, + {"on-model", "/", false}, + {"on-model", "dwell/dwell1", true}, + {"on-model", "dwell", false}, + {"on-model", "dwell/", false}, + {"on-model", "dwell//dwell1", false}, + {"on-model", "dwell/-dwell1", false}, + {"on-model", "dwell/dwell1-", false}, + {"on-model", "dwell/dwell1-23", true}, + {"on-model", "dwell/dwell1!", false}, + {"on-model", "dwell/dwe_ll1", false}, + {"on-model", "dwell/dwe.ll1", false}, + } + + check := func(constr, value string, valid bool) { + ruleMap := map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + constr: []interface{}{value}, + }, + } + + _, err := asserts.CompilePlugRule("iface", ruleMap) + if valid { + c.Check(err, IsNil, Commentf("%v", ruleMap)) + } else { + c.Check(err, ErrorMatches, fmt.Sprintf(`%s in allow-auto-connection in plug rule for interface "iface" contains an invalid element: %q`, constr, value), Commentf("%v", ruleMap)) + } + } + + for _, t := range tests { + check(t.constr, t.value, t.valid) + + if t.constr == "on-brand" { + // reuse and double check all brands also in the context of on-model! + + check("on-model", t.value+"/foo", t.valid) + } + } +} diff --git a/asserts/membackstore.go b/asserts/membackstore.go new file mode 100644 index 00000000..a705f1f5 --- /dev/null +++ b/asserts/membackstore.go @@ -0,0 +1,191 @@ +// -*- 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 ( + "errors" + "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 +} + +// errNotFound is used internally by backends, it is converted to the richer +// NotFoundError only at their public interface boundary +var errNotFound = errors.New("assertion not found") + +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, 1+len(assertType.PrimaryKey)) + internalKey[0] = assertType.Name + internalKey = append(internalKey, assert.Ref().PrimaryKey...) + + 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) + + a, err := mbs.top.get(internalKey, maxFormat) + if err == errNotFound { + return nil, &NotFoundError{Type: assertType} + } + return a, err +} + +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..644c0604 --- /dev/null +++ b/asserts/membackstore_test.go @@ -0,0 +1,351 @@ +// -*- 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, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + // Headers can be omitted by Backstores + }) + 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, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + }) + 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, FitsTypeOf, &asserts.NotFoundError{}) + + err = bs.Put(asserts.TestOnlyType, af2) + c.Assert(err, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 1) + c.Assert(err, FitsTypeOf, &asserts.NotFoundError{}) + + 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/repair.go b/asserts/repair.go new file mode 100644 index 00000000..27d765f7 --- /dev/null +++ b/asserts/repair.go @@ -0,0 +1,159 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +// Repair holds an repair assertion which allows running repair +// code to fixup broken systems. It can be limited by series and models. +type Repair struct { + assertionBase + + series []string + architectures []string + models []string + + id int + + disabled bool + timestamp time.Time +} + +// BrandID returns the brand identifier that signed this assertion. +func (r *Repair) BrandID() string { + return r.HeaderString("brand-id") +} + +// RepairID returns the sequential id of the repair. There +// should be a public place to look up details about the repair +// by brand-id and repair-id. +// (e.g. the snapcraft forum). +func (r *Repair) RepairID() int { + return r.id +} + +// Summary returns the mandatory summary description of the repair. +func (r *Repair) Summary() string { + return r.HeaderString("summary") +} + +// Architectures returns the architectures that this assertions applies to. +func (r *Repair) Architectures() []string { + return r.architectures +} + +// Series returns the series that this assertion is valid for. +func (r *Repair) Series() []string { + return r.series +} + +// Models returns the models that this assertion is valid for. +// It is a list of "brand-id/model-name" strings. +func (r *Repair) Models() []string { + return r.models +} + +// Disabled returns true if the repair has been disabled. +func (r *Repair) Disabled() bool { + return r.disabled +} + +// Timestamp returns the time when the repair was issued. +func (r *Repair) Timestamp() time.Time { + return r.timestamp +} + +// Implement further consistency checks. +func (r *Repair) checkConsistency(db RODatabase, acck *AccountKey) error { + // Do the cross-checks when this assertion is actually used, + // i.e. in the future repair code + + return nil +} + +// sanity +var _ consistencyChecker = (*Repair)(nil) + +// the repair-id can for now be a sequential number starting with 1 +var validRepairID = regexp.MustCompile("^[1-9][0-9]*$") + +func assembleRepair(assert assertionBase) (Assertion, error) { + err := checkAuthorityMatchesBrand(&assert) + if err != nil { + return nil, err + } + + repairID, err := checkStringMatches(assert.headers, "repair-id", validRepairID) + if err != nil { + return nil, err + } + id, err := strconv.Atoi(repairID) + if err != nil { + // given it matched it can likely only be too large + return nil, fmt.Errorf("repair-id too large: %s", repairID) + } + + summary, err := checkNotEmptyString(assert.headers, "summary") + if err != nil { + return nil, err + } + if strings.ContainsAny(summary, "\n\r") { + return nil, fmt.Errorf(`"summary" header cannot have newlines`) + } + + series, err := checkStringList(assert.headers, "series") + if err != nil { + return nil, err + } + models, err := checkStringList(assert.headers, "models") + if err != nil { + return nil, err + } + architectures, err := checkStringList(assert.headers, "architectures") + if err != nil { + return nil, err + } + + disabled, err := checkOptionalBool(assert.headers, "disabled") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &Repair{ + assertionBase: assert, + series: series, + architectures: architectures, + models: models, + id: id, + disabled: disabled, + timestamp: timestamp, + }, nil +} diff --git a/asserts/repair_test.go b/asserts/repair_test.go new file mode 100644 index 00000000..ffac7397 --- /dev/null +++ b/asserts/repair_test.go @@ -0,0 +1,183 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts_test + +import ( + "fmt" + "io/ioutil" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/snapcore/snapd/asserts" + . "gopkg.in/check.v1" +) + +var ( + _ = Suite(&repairSuite{}) +) + +type repairSuite struct { + modelsLine string + ts time.Time + tsLine string + + repairStr string +} + +const script = `#!/bin/sh +set -e +echo "Unpack embedded payload" +match=$(grep --text --line-number '^PAYLOAD:$' $0 | cut -d ':' -f 1) +payload_start=$((match + 1)) +# Using "base64" as its part of coreutils which should be available +# everywhere +tail -n +$payload_start $0 | base64 --decode - | tar -xzf - +# run embedded content +./hello +exit 0 +# payload generated with, may contain binary data +# printf '#!/bin/sh\necho hello from the inside\n' > hello +# chmod +x hello +# tar czf - hello | base64 - +PAYLOAD: +H4sIAJJt+FgAA+3STQrCMBDF8ax7ihEP0CkxyXn8iCZQE2jr/W11Iwi6KiL8f5u3mLd4i0mx76tZ +l86Cc0t2welrPu2c6awGr95bG4x26rw1oivveriN034QMfFSy6fet/uf2m7aQy7tmJp4TFXS8g5y +HupVphQllzGfYvPrkQAAAAAAAAAAAAAAAACAN3dTp9TNACgAAA== +` + +var repairExample = fmt.Sprintf("type: repair\n"+ + "authority-id: acme\n"+ + "brand-id: acme\n"+ + "summary: example repair\n"+ + "architectures:\n"+ + " - amd64\n"+ + " - arm64\n"+ + "repair-id: 42\n"+ + "series:\n"+ + " - 16\n"+ + "MODELSLINE"+ + "TSLINE"+ + "body-length: %v\n"+ + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij"+ + "\n\n"+ + script+"\n\n"+ + "AXNpZw==", len(script)) + +func (s *repairSuite) SetUpTest(c *C) { + s.modelsLine = "models:\n - acme/frobinator\n" + s.ts = time.Now().Truncate(time.Second).UTC() + s.tsLine = "timestamp: " + s.ts.Format(time.RFC3339) + "\n" + + s.repairStr = strings.Replace(repairExample, "MODELSLINE", s.modelsLine, 1) + s.repairStr = strings.Replace(s.repairStr, "TSLINE", s.tsLine, 1) +} + +func (s *repairSuite) TestDecodeOK(c *C) { + a, err := asserts.Decode([]byte(s.repairStr)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.RepairType) + repair := a.(*asserts.Repair) + c.Check(repair.Timestamp(), Equals, s.ts) + c.Check(repair.BrandID(), Equals, "acme") + c.Check(repair.RepairID(), Equals, 42) + c.Check(repair.Summary(), Equals, "example repair") + c.Check(repair.Series(), DeepEquals, []string{"16"}) + c.Check(repair.Architectures(), DeepEquals, []string{"amd64", "arm64"}) + c.Check(repair.Models(), DeepEquals, []string{"acme/frobinator"}) + c.Check(string(repair.Body()), Equals, script) +} + +const ( + repairErrPrefix = "assertion repair: " +) + +func (s *repairSuite) TestDisabled(c *C) { + disabledTests := []struct { + disabled, expectedErr string + dis bool + }{ + {"true", "", true}, + {"false", "", false}, + {"foo", `"disabled" header must be 'true' or 'false'`, false}, + } + + for _, test := range disabledTests { + repairStr := strings.Replace(repairExample, "MODELSLINE", fmt.Sprintf("disabled: %s\n", test.disabled), 1) + repairStr = strings.Replace(repairStr, "TSLINE", s.tsLine, 1) + + a, err := asserts.Decode([]byte(repairStr)) + if test.expectedErr != "" { + c.Check(err, ErrorMatches, repairErrPrefix+test.expectedErr) + } else { + c.Assert(err, IsNil) + repair := a.(*asserts.Repair) + c.Check(repair.Disabled(), Equals, test.dis) + } + } +} + +func (s *repairSuite) TestDecodeInvalid(c *C) { + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"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`}, + {"architectures:\n - amd64\n - arm64\n", "architectures: foo\n", `"architectures" header must be a list of strings`}, + {"models:\n - acme/frobinator\n", "models: \n", `"models" header must be a list of strings`}, + {"models:\n - acme/frobinator\n", "models: something\n", `"models" header must be a list of strings`}, + {"repair-id: 42\n", "repair-id: no-number\n", `"repair-id" header contains invalid characters: "no-number"`}, + {"repair-id: 42\n", "repair-id: 0\n", `"repair-id" header contains invalid characters: "0"`}, + {"repair-id: 42\n", "repair-id: 01\n", `"repair-id" header contains invalid characters: "01"`}, + {"repair-id: 42\n", "repair-id: 99999999999999999999\n", `repair-id too large:.*`}, + {"brand-id: acme\n", "brand-id: brand-id-not-eq-authority-id\n", `authority-id and brand-id must match, repair assertions are expected to be signed by the brand: "acme" != "brand-id-not-eq-authority-id"`}, + {"summary: example repair\n", "", `"summary" header is mandatory`}, + {"summary: example repair\n", "summary: \n", `"summary" header should not be empty`}, + {"summary: example repair\n", "summary:\n multi\n line\n", `"summary" header cannot have newlines`}, + {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(s.repairStr, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, repairErrPrefix+test.expectedErr) + } +} + +// FIXME: move to a different layer later +func (s *repairSuite) TestRepairCanEmbeddScripts(c *C) { + a, err := asserts.Decode([]byte(s.repairStr)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.RepairType) + repair := a.(*asserts.Repair) + + tmpdir := c.MkDir() + repairScript := filepath.Join(tmpdir, "repair") + err = ioutil.WriteFile(repairScript, []byte(repair.Body()), 0755) + c.Assert(err, IsNil) + cmd := exec.Command(repairScript) + cmd.Dir = tmpdir + output, err := cmd.CombinedOutput() + c.Check(err, IsNil) + c.Check(string(output), Equals, `Unpack embedded payload +hello from the inside +`) +} diff --git a/asserts/signtool/sign.go b/asserts/signtool/sign.go new file mode 100644 index 00000000..a341278b --- /dev/null +++ b/asserts/signtool/sign.go @@ -0,0 +1,110 @@ +// -*- 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 + + // Complement specifies complementary headers to what is in + // Statement, for use by tools that fill-in/compute some of + // the headers. Headers appearing both in Statement and + // Complement are an error, except for "type" that needs + // instead to match if present. Pseudo-header "body" can also + // be specified here. + Complement map[string]interface{} +} + +// 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) + } + + for name, value := range opts.Complement { + if v, ok := headers[name]; ok { + if name == "type" { + if v != value { + return nil, fmt.Errorf("repeated assertion type does not match") + } + } else { + return nil, fmt.Errorf("complementary header %q clashes with assertion input", name) + } + } + headers[name] = value + } + + 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..681e0f02 --- /dev/null +++ b/asserts/signtool/sign_test.go @@ -0,0 +1,262 @@ +// -*- 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) TestSignJSONWithBodyAndComplementRevision(c *C) { + statement := exampleJSON(map[string]interface{}{ + "body": "BODY", + }) + opts := signtool.Options{ + KeyID: s.testKeyID, + + Statement: statement, + Complement: map[string]interface{}{ + "revision": "11", + }, + } + + 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) TestSignJSONWithRevisionAndComplementBodyAndRepeatedType(c *C) { + statement := exampleJSON(map[string]interface{}{ + "revision": "11", + }) + opts := signtool.Options{ + KeyID: s.testKeyID, + + Statement: statement, + Complement: map[string]interface{}{ + "type": "model", + "body": "BODY", + }, + } + + 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 + complement map[string]interface{} + }{ + {`cannot parse the assertion input as JSON:.*`, + []byte("\x00"), + nil, + }, + {`invalid assertion type: what`, + exampleJSON(map[string]interface{}{"type": "what"}), + nil, + }, + {`assertion type must be a string, not: \[\]`, + exampleJSON(map[string]interface{}{"type": emptyList}), + nil, + }, + {`missing assertion type header`, + exampleJSON(map[string]interface{}{"type": nil}), + nil, + }, + {"revision should be positive: -10", + exampleJSON(map[string]interface{}{"revision": "-10"}), + nil, + }, + {`"authority-id" header is mandatory`, + exampleJSON(map[string]interface{}{"authority-id": nil}), + nil, + }, + {`body if specified must be a string`, + exampleJSON(map[string]interface{}{"body": emptyList}), + nil, + }, + {`repeated assertion type does not match`, + exampleJSON(nil), + map[string]interface{}{"type": "foo"}, + }, + {`complementary header "kernel" clashes with assertion input`, + exampleJSON(nil), + map[string]interface{}{"kernel": "foo"}, + }, + } + + for _, t := range tests { + fresh := opts + + fresh.Statement = t.brokenStatement + fresh.Complement = t.complement + + _, 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..51b20019 --- /dev/null +++ b/asserts/snap_asserts.go @@ -0,0 +1,943 @@ +// -*- 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 + aliases map[string]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. +// XXX: deprecated, will go away +func (snapdcl *SnapDeclaration) AutoAliases() []string { + return snapdcl.autoAliases +} + +// Aliases returns the optional explicit aliases granted to this snap. +func (snapdcl *SnapDeclaration) Aliases() map[string]string { + return snapdcl.aliases +} + +// 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 IsNotFound(err) { + 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()}}, + } +} + +func compilePlugRules(plugs map[string]interface{}, compiled func(iface string, plugRule *PlugRule)) error { + for iface, rule := range plugs { + plugRule, err := compilePlugRule(iface, rule) + if err != nil { + return err + } + compiled(iface, plugRule) + } + return nil +} + +func compileSlotRules(slots map[string]interface{}, compiled func(iface string, slotRule *SlotRule)) error { + for iface, rule := range slots { + slotRule, err := compileSlotRule(iface, rule) + if err != nil { + return err + } + compiled(iface, slotRule) + } + return nil +} + +func snapDeclarationFormatAnalyze(headers map[string]interface{}, body []byte) (formatnum int, err error) { + _, plugsOk := headers["plugs"] + _, slotsOk := headers["slots"] + if !(plugsOk || slotsOk) { + return 0, nil + } + + formatnum = 1 + setFormatNum := func(num int) { + if num > formatnum { + formatnum = num + } + } + + plugs, err := checkMap(headers, "plugs") + if err != nil { + return 0, err + } + err = compilePlugRules(plugs, func(_ string, rule *PlugRule) { + if rule.feature(dollarAttrConstraintsFeature) { + setFormatNum(2) + } + if rule.feature(deviceScopeConstraintsFeature) { + setFormatNum(3) + } + }) + if err != nil { + return 0, err + } + + slots, err := checkMap(headers, "slots") + if err != nil { + return 0, err + } + err = compileSlotRules(slots, func(_ string, rule *SlotRule) { + if rule.feature(dollarAttrConstraintsFeature) { + setFormatNum(2) + } + if rule.feature(deviceScopeConstraintsFeature) { + setFormatNum(3) + } + }) + if err != nil { + return 0, err + } + + return formatnum, nil +} + +var ( + validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") + validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") +) + +func checkAliases(headers map[string]interface{}) (map[string]string, error) { + value, ok := headers["aliases"] + if !ok { + return nil, nil + } + aliasList, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf(`"aliases" header must be a list of alias maps`) + } + if len(aliasList) == 0 { + return nil, nil + } + + aliasMap := make(map[string]string, len(aliasList)) + for i, item := range aliasList { + aliasItem, ok := item.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf(`"aliases" header must be a list of alias maps`) + } + + what := fmt.Sprintf(`in "aliases" item %d`, i+1) + name, err := checkStringMatchesWhat(aliasItem, "name", what, validAlias) + if err != nil { + return nil, err + } + + what = fmt.Sprintf(`for alias %q`, name) + target, err := checkStringMatchesWhat(aliasItem, "target", what, validAppName) + if err != nil { + return nil, err + } + + if _, ok := aliasMap[name]; ok { + return nil, fmt.Errorf(`duplicated definition in "aliases" for alias %q`, name) + } + + aliasMap[name] = target + } + + return aliasMap, nil +} + +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)) + err := compilePlugRules(plugs, func(iface string, rule *PlugRule) { + plugRules[iface] = rule + }) + if err != nil { + return nil, err + } + } + + slots, err := checkMap(assert.headers, "slots") + if err != nil { + return nil, err + } + if slots != nil { + slotRules = make(map[string]*SlotRule, len(slots)) + err := compileSlotRules(slots, func(iface string, rule *SlotRule) { + slotRules[iface] = rule + }) + if err != nil { + return nil, err + } + } + + // XXX: depracated, will go away later + autoAliases, err := checkStringListMatches(assert.headers, "auto-aliases", validAlias) + if err != nil { + return nil, err + } + + aliases, err := checkAliases(assert.headers) + if err != nil { + return nil, err + } + + return &SnapDeclaration{ + assertionBase: assert, + refreshControl: refControl, + plugRules: plugRules, + slotRules: slotRules, + autoAliases: autoAliases, + aliases: aliases, + 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 IsNotFound(err) { + 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 IsNotFound(err) { + 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 IsNotFound(err) { + 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 IsNotFound(err) { + 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)) + err := compilePlugRules(plugs, func(iface string, rule *PlugRule) { + plugRules[iface] = rule + }) + if err != nil { + return nil, err + } + } + + 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)) + err := compileSlotRules(slots, func(iface string, rule *SlotRule) { + slotRules[iface] = rule + }) + if err != nil { + return nil, err + } + } + + 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 +} + +type dateRange struct { + Since time.Time + Until time.Time +} + +// SnapDeveloper holds a snap-developer assertion, defining the developers who +// can collaborate on a snap while it's owned by a specific publisher. +// +// The primary key (snap-id, publisher-id) allows a snap to have many +// snap-developer assertions, e.g. to allow a future publisher's collaborations +// to be defined before the snap is transferred. However only the +// snap-developer for the current publisher (the snap-declaration publisher-id) +// is relevant to a device. +type SnapDeveloper struct { + assertionBase + developerRanges map[string][]*dateRange +} + +// SnapID returns the snap id of the snap. +func (snapdev *SnapDeveloper) SnapID() string { + return snapdev.HeaderString("snap-id") +} + +// PublisherID returns the publisher's account id. +func (snapdev *SnapDeveloper) PublisherID() string { + return snapdev.HeaderString("publisher-id") +} + +func (snapdev *SnapDeveloper) checkConsistency(db RODatabase, acck *AccountKey) error { + // Check authority is the publisher or trusted. + authorityID := snapdev.AuthorityID() + publisherID := snapdev.PublisherID() + if !db.IsTrustedAccount(authorityID) && (publisherID != authorityID) { + return fmt.Errorf("snap-developer must be signed by the publisher or a trusted authority but got authority %q and publisher %q", authorityID, publisherID) + } + + // Check snap-declaration for the snap-id exists for the series. + // Note: the current publisher is irrelevant here because this assertion + // may be for a future publisher. + _, 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": snapdev.SnapID(), + }) + if err != nil { + if IsNotFound(err) { + return fmt.Errorf("snap-developer assertion for snap id %q does not have a matching snap-declaration assertion", snapdev.SnapID()) + } + return err + } + + // check there's an account for the publisher-id + _, err = db.Find(AccountType, map[string]string{"account-id": publisherID}) + if err != nil { + if IsNotFound(err) { + return fmt.Errorf("snap-developer assertion for snap-id %q does not have a matching account assertion for the publisher %q", snapdev.SnapID(), publisherID) + } + return err + } + + // check there's an account for each developer + for developerID := range snapdev.developerRanges { + if developerID == publisherID { + continue + } + _, err = db.Find(AccountType, map[string]string{"account-id": developerID}) + if err != nil { + if IsNotFound(err) { + return fmt.Errorf("snap-developer assertion for snap-id %q does not have a matching account assertion for the developer %q", snapdev.SnapID(), developerID) + } + return err + } + } + + return nil +} + +// sanity +var _ consistencyChecker = (*SnapDeveloper)(nil) + +// Prerequisites returns references to this snap-developer's prerequisite assertions. +func (snapdev *SnapDeveloper) Prerequisites() []*Ref { + // Capacity for the snap-declaration, the publisher and all developers. + refs := make([]*Ref, 0, 2+len(snapdev.developerRanges)) + + // snap-declaration + // XXX: mediate getting current series through some context object? this gets the job done for now + refs = append(refs, &Ref{SnapDeclarationType, []string{release.Series, snapdev.SnapID()}}) + + // the publisher and developers + publisherID := snapdev.PublisherID() + refs = append(refs, &Ref{AccountType, []string{publisherID}}) + for developerID := range snapdev.developerRanges { + if developerID != publisherID { + refs = append(refs, &Ref{AccountType, []string{developerID}}) + } + } + + return refs +} + +func assembleSnapDeveloper(assert assertionBase) (Assertion, error) { + developerRanges, err := checkDevelopers(assert.headers) + if err != nil { + return nil, err + } + + return &SnapDeveloper{ + assertionBase: assert, + developerRanges: developerRanges, + }, nil +} + +func checkDevelopers(headers map[string]interface{}) (map[string][]*dateRange, error) { + value, ok := headers["developers"] + if !ok { + return nil, nil + } + developers, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf(`"developers" must be a list of developer maps`) + } + if len(developers) == 0 { + return nil, nil + } + + // Used to check for a developer with revoking and non-revoking items. + // No entry means developer not yet seen, false means seen but not revoked, + // true means seen and revoked. + revocationStatus := map[string]bool{} + + developerRanges := make(map[string][]*dateRange) + for i, item := range developers { + developer, ok := item.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf(`"developers" must be a list of developer maps`) + } + + what := fmt.Sprintf(`in "developers" item %d`, i+1) + accountID, err := checkStringMatchesWhat(developer, "developer-id", what, validAccountID) + if err != nil { + return nil, err + } + + what = fmt.Sprintf(`in "developers" item %d for developer %q`, i+1, accountID) + since, err := checkRFC3339DateWhat(developer, "since", what) + if err != nil { + return nil, err + } + until, err := checkRFC3339DateWithDefaultWhat(developer, "until", what, time.Time{}) + if err != nil { + return nil, err + } + if !until.IsZero() && since.After(until) { + return nil, fmt.Errorf(`"since" %s must be less than or equal to "until"`, what) + } + + // Track/check for revocation conflicts. + revoked := since.Equal(until) + previouslyRevoked, ok := revocationStatus[accountID] + if !ok { + revocationStatus[accountID] = revoked + } else if previouslyRevoked || revoked { + return nil, fmt.Errorf(`revocation for developer %q must be standalone but found other "developers" items`, accountID) + } + + developerRanges[accountID] = append(developerRanges[accountID], &dateRange{since, until}) + } + + return developerRanges, nil +} diff --git a/asserts/snap_asserts_test.go b/asserts/snap_asserts_test.go new file mode 100644 index 00000000..29e2bc1b --- /dev/null +++ b/asserts/snap_asserts_test.go @@ -0,0 +1,1870 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * 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 . + * + */ + +package asserts_test + +import ( + "encoding/base64" + "io/ioutil" + "path/filepath" + "sort" + "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{}) + _ = Suite(&snapDevSuite{}) +) + +type snapDeclSuite struct { + ts time.Time + tsLine string +} + +type emptyAttrerObject struct{} + +func (o emptyAttrerObject) Lookup(path string) (interface{}, bool) { + return nil, false +} + +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 + + `aliases: + - + name: cmd1 + target: cmd-1 + - + name: cmd_2 + target: cmd-2 + - + name: Cmd-3 + target: cmd-3 + - + name: CMD.4 + target: cmd-4 +` + + "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"}) + c.Check(snapDecl.Aliases(), DeepEquals, map[string]string{ + "cmd1": "cmd-1", + "cmd_2": "cmd-2", + "Cmd-3": "cmd-3", + "CMD.4": "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) { + aliases := `aliases: + - + name: cmd_1 + target: cmd-1 +` + 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" + + aliases + + "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"`}, + {aliases, "aliases: cmd0\n", `"aliases" header must be a list of alias maps`}, + {aliases, "aliases:\n - cmd1\n", `"aliases" header must be a list of alias maps`}, + {"name: cmd_1\n", "name: .cmd1\n", `"name" in "aliases" item 1 contains invalid characters: ".cmd1"`}, + {"target: cmd-1\n", "target: -cmd-1\n", `"target" for alias "cmd_1" contains invalid characters: "-cmd-1"`}, + {aliases, aliases + " -\n name: cmd_1\n target: foo\n", `duplicated definition in "aliases" for alias "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 +format: 1 +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) + + plug := emptyAttrerObject{} + slot := emptyAttrerObject{} + + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(plug, 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(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(plug, 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(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "b2".*`) + c.Assert(plugRule2.DenyConnection, HasLen, 1) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(slot, 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(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(plug, 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(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(plug, 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(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "d2".*`) + c.Assert(slotRule4.DenyAutoConnection, HasLen, 1) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(slot, 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(slot, nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) +} + +func (sds *snapDeclSuite) TestSuggestedFormat(c *C) { + fmtnum, err := asserts.SuggestFormat(asserts.SnapDeclarationType, nil, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 0) + + headers := map[string]interface{}{ + "plugs": map[string]interface{}{ + "interface1": "true", + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 1) + + headers = map[string]interface{}{ + "slots": map[string]interface{}{ + "interface2": "true", + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 1) + + headers = map[string]interface{}{ + "plugs": map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "plug-attributes": map[string]interface{}{ + "x": "$SLOT(x)", + }, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 2) + + headers = map[string]interface{}{ + "slots": map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "plug-attributes": map[string]interface{}{ + "x": "$SLOT(x)", + }, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 2) + + // combinations with on-store/on-brand/on-model => format 3 + for _, side := range []string{"plugs", "slots"} { + for k, vals := range deviceScopeConstrs { + + headers := map[string]interface{}{ + side: map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-installation": map[string]interface{}{ + k: vals, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 3) + + for _, conn := range []string{"connection", "auto-connection"} { + + headers = map[string]interface{}{ + side: map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-" + conn: map[string]interface{}{ + k: vals, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 3) + } + } + } + + // higher format features win + + headers = map[string]interface{}{ + "plugs": map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "on-store": []interface{}{"store"}, + }, + }, + }, + "slots": map[string]interface{}{ + "interface4": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "plug-attributes": map[string]interface{}{ + "x": "$SLOT(x)", + }, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 3) + + headers = map[string]interface{}{ + "plugs": map[string]interface{}{ + "interface4": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "slot-attributes": map[string]interface{}{ + "x": "$SLOT(x)", + }, + }, + }, + }, + "slots": map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "on-store": []interface{}{"store"}, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 3) + + // errors + headers = map[string]interface{}{ + "plugs": "what", + } + _, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, ErrorMatches, `assertion snap-declaration: "plugs" header must be a map`) + + headers = map[string]interface{}{ + "slots": "what", + } + _, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, ErrorMatches, `assertion snap-declaration: "slots" header must be a map`) +} + +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) (store *assertstest.StoreStack, checkDB *asserts.Database) { + store = assertstest.NewStoreStack("canonical", nil) + cfg := &asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + OtherPredefined: store.Generic, + } + checkDB, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + // add store key + err = checkDB.Add(store.StoreAccountKey("")) + c.Assert(err, IsNil) + // add generic key + err = checkDB.Add(store.GenericKey) + c.Assert(err, IsNil) + + return store, checkDB +} + +func setup3rdPartySigning(c *C, username string, storeDB assertstest.SignerDB, 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 \(key valid since.*\)`) +} + +type snapRevSuite struct { + ts time.Time + tsLine 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 \(key valid since.*\)`) +} + +func (srs *snapRevSuite) TestSnapRevisionCheckUntrustedAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := srs.makeHeaders(map[string]interface{}{ + "authority-id": "other", + }) + 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(map[string]interface{}{ + "authority-id": "canonical", // not the publisher + }) + 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) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + headers := vs.makeHeaders(nil) + a, err := devDB.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) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + prereqSnapDecl2(c, storeDB, db) + + headers := vs.makeHeaders(nil) + a, err := devDB.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) + + plug := emptyAttrerObject{} + slot := emptyAttrerObject{} + + 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(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(plug, 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(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(plug, 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(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "b2".*`) + c.Assert(plugRule2.DenyConnection, HasLen, 1) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(slot, 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(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(plug, 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(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(plug, 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(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "d2".*`) + c.Assert(slotRule4.DenyConnection, HasLen, 1) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(slot, 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(slot, 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)) + } +} + +type snapDevSuite struct { + developersLines string + validEncoded string +} + +func (sds *snapDevSuite) SetUpSuite(c *C) { + sds.developersLines = "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-02-01T00:00:00.0Z\n" + sds.validEncoded = "type: snap-developer\n" + + "authority-id: dev-id1\n" + + "snap-id: snap-id-1\n" + + "publisher-id: dev-id1\n" + + sds.developersLines + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" +} + +func (sds *snapDevSuite) TestDecodeOK(c *C) { + encoded := sds.validEncoded + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapDeveloperType) + snapDev := a.(*asserts.SnapDeveloper) + c.Check(snapDev.AuthorityID(), Equals, "dev-id1") + c.Check(snapDev.PublisherID(), Equals, "dev-id1") + c.Check(snapDev.SnapID(), Equals, "snap-id-1") +} + +func (sds *snapDevSuite) TestDevelopersOptional(c *C) { + encoded := strings.Replace(sds.validEncoded, sds.developersLines, "", 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) +} + +func (sds *snapDevSuite) TestDevelopersUntilOptional(c *C) { + encoded := strings.Replace( + sds.validEncoded, sds.developersLines, + "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n", 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) +} + +func (sds *snapDevSuite) TestDevelopersRevoked(c *C) { + encoded := sds.validEncoded + encoded = strings.Replace( + encoded, sds.developersLines, + "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-01-01T00:00:00.0Z\n", 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) + // TODO(matt): check actually revoked rather than just parsed +} + +const ( + snapDevErrPrefix = "assertion snap-developer: " +) + +func (sds *snapDevSuite) TestDecodeInvalid(c *C) { + encoded := sds.validEncoded + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"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`}, + {"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`}, + {sds.developersLines, "developers: \n", `"developers" must be a list of developer maps`}, + {sds.developersLines, "developers: foo\n", `"developers" must be a list of developer maps`}, + {sds.developersLines, "developers:\n foo: bar\n", `"developers" must be a list of developer maps`}, + {sds.developersLines, "developers:\n - foo\n", `"developers" must be a list of developer maps`}, + {sds.developersLines, "developers:\n -\n foo: bar\n", `"developer-id" in "developers" item 1 is mandatory`}, + {sds.developersLines, "developers:\n -\n developer-id: a\n", + `"developer-id" in "developers" item 1 contains invalid characters: "a"`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n", + `"since" in "developers" item 1 for developer "dev-id2" is mandatory`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: \n", + `"since" in "developers" item 1 for developer "dev-id2" should not be empty`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: foo\n", + `"since" in "developers" item 1 for developer "dev-id2" is not a RFC3339 date.*`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: \n", + `"until" in "developers" item 1 for developer "dev-id2" is not a RFC3339 date.*`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: foo\n", + `"until" in "developers" item 1 for developer "dev-id2" is not a RFC3339 date.*`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n -\n foo: bar\n", + `"developer-id" in "developers" item 2 is mandatory`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: 2017-01-02T00:00:00.0Z\n until: 2017-01-01T00:00:00.0Z\n", + `"since" in "developers" item 1 for developer "dev-id2" must be less than or equal to "until"`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, snapDevErrPrefix+test.expectedErr) + } +} + +func (sds *snapDevSuite) TestRevokedValidation(c *C) { + // Multiple non-revoking items are fine. + encoded := strings.Replace(sds.validEncoded, sds.developersLines, + "developers:\n"+ + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-02-01T00:00:00.0Z\n"+ + " -\n developer-id: dev-id2\n since: 2017-03-01T00:00:00.0Z\n", + 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) + + // Multiple revocations for different developers are fine. + encoded = strings.Replace(sds.validEncoded, sds.developersLines, + "developers:\n"+ + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-01-01T00:00:00.0Z\n"+ + " -\n developer-id: dev-id3\n since: 2017-02-01T00:00:00.0Z\n until: 2017-02-01T00:00:00.0Z\n", + 1) + _, err = asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) + + invalidTests := []string{ + // Multiple revocations. + "developers:\n" + + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-01-01T00:00:00.0Z\n" + + " -\n developer-id: dev-id2\n since: 2017-02-01T00:00:00.0Z\n until: 2017-02-01T00:00:00.0Z\n", + // Revocation after non-revoking. + "developers:\n" + + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n" + + " -\n developer-id: dev-id2\n since: 2017-03-01T00:00:00.0Z\n until: 2017-03-01T00:00:00.0Z\n", + // Non-revoking after revocation. + "developers:\n" + + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-01-01T00:00:00.0Z\n" + + " -\n developer-id: dev-id2\n since: 2017-02-01T00:00:00.0Z\n", + } + for _, test := range invalidTests { + encoded := strings.Replace(sds.validEncoded, sds.developersLines, test, 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, ErrorMatches, snapDevErrPrefix+`revocation for developer "dev-id2" must be standalone but found other "developers" items`) + } +} + +func (sds *snapDevSuite) TestAuthorityIsPublisher(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "snap-name-1", + "publisher-id": "dev-id1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + snapDev, err := devDB.Sign(asserts.SnapDeveloperType, map[string]interface{}{ + "snap-id": "snap-id-1", + "publisher-id": "dev-id1", + }, nil, "") + c.Assert(err, IsNil) + // Just to be super sure ... + c.Assert(snapDev.HeaderString("authority-id"), Equals, "dev-id1") + c.Assert(snapDev.HeaderString("publisher-id"), Equals, "dev-id1") + + err = db.Check(snapDev) + c.Assert(err, IsNil) +} + +func (sds *snapDevSuite) TestAuthorityIsNotPublisher(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "snap-name-1", + "publisher-id": "dev-id1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + snapDev, err := devDB.Sign(asserts.SnapDeveloperType, map[string]interface{}{ + "authority-id": "dev-id1", + "snap-id": "snap-id-1", + "publisher-id": "dev-id2", + }, nil, "") + c.Assert(err, IsNil) + // Just to be super sure ... + c.Assert(snapDev.HeaderString("authority-id"), Equals, "dev-id1") + c.Assert(snapDev.HeaderString("publisher-id"), Equals, "dev-id2") + + err = db.Check(snapDev) + c.Assert(err, ErrorMatches, `snap-developer must be signed by the publisher or a trusted authority but got authority "dev-id1" and publisher "dev-id2"`) +} + +func (sds *snapDevSuite) TestAuthorityIsNotPublisherButIsTrusted(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + account, err := storeDB.Sign(asserts.AccountType, map[string]interface{}{ + "account-id": "dev-id1", + "display-name": "dev-id1", + "validation": "unknown", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(account) + c.Assert(err, IsNil) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "snap-name-1", + "publisher-id": "dev-id1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + snapDev, err := storeDB.Sign(asserts.SnapDeveloperType, map[string]interface{}{ + "snap-id": "snap-id-1", + "publisher-id": "dev-id1", + }, nil, "") + c.Assert(err, IsNil) + // Just to be super sure ... + c.Assert(snapDev.HeaderString("authority-id"), Equals, "canonical") + c.Assert(snapDev.HeaderString("publisher-id"), Equals, "dev-id1") + + err = db.Check(snapDev) + c.Assert(err, IsNil) +} + +func (sds *snapDevSuite) TestCheckNewPublisherAccountExists(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + account, err := storeDB.Sign(asserts.AccountType, map[string]interface{}{ + "account-id": "dev-id1", + "display-name": "dev-id1", + "validation": "unknown", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(account) + c.Assert(err, IsNil) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "snap-name-1", + "publisher-id": "dev-id1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + snapDev, err := storeDB.Sign(asserts.SnapDeveloperType, map[string]interface{}{ + "snap-id": "snap-id-1", + "publisher-id": "dev-id2", + }, nil, "") + c.Assert(err, IsNil) + // Just to be super sure ... + c.Assert(snapDev.HeaderString("authority-id"), Equals, "canonical") + c.Assert(snapDev.HeaderString("publisher-id"), Equals, "dev-id2") + + // There's no account for dev-id2 yet so it should fail. + err = db.Check(snapDev) + c.Assert(err, ErrorMatches, `snap-developer assertion for snap-id "snap-id-1" does not have a matching account assertion for the publisher "dev-id2"`) + + // But once the dev-id2 account is added the snap-developer is ok. + account, err = storeDB.Sign(asserts.AccountType, map[string]interface{}{ + "account-id": "dev-id2", + "display-name": "dev-id2", + "validation": "unknown", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(account) + c.Assert(err, IsNil) + + err = db.Check(snapDev) + c.Assert(err, IsNil) +} + +func (sds *snapDevSuite) TestCheckDeveloperAccountExists(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "snap-name-1", + "publisher-id": "dev-id1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + snapDev, err := devDB.Sign(asserts.SnapDeveloperType, map[string]interface{}{ + "snap-id": "snap-id-1", + "publisher-id": "dev-id1", + "developers": []interface{}{ + map[string]interface{}{ + "developer-id": "dev-id2", + "since": "2017-01-01T00:00:00.0Z", + }, + }, + }, nil, "") + c.Assert(err, IsNil) + err = db.Check(snapDev) + c.Assert(err, ErrorMatches, `snap-developer assertion for snap-id "snap-id-1" does not have a matching account assertion for the developer "dev-id2"`) +} + +func (sds *snapDevSuite) TestCheckMissingDeclaration(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + headers := map[string]interface{}{ + "authority-id": "dev-id1", + "snap-id": "snap-id-1", + "publisher-id": "dev-id1", + } + snapDev, err := devDB.Sign(asserts.SnapDeveloperType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapDev) + c.Assert(err, ErrorMatches, `snap-developer assertion for snap id "snap-id-1" does not have a matching snap-declaration assertion`) +} + +func (sds *snapDevSuite) TestPrerequisitesNoDevelopers(c *C) { + encoded := strings.Replace(sds.validEncoded, sds.developersLines, "", 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + prereqs := assert.Prerequisites() + sort.Sort(RefSlice(prereqs)) + c.Assert(prereqs, DeepEquals, []*asserts.Ref{ + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id1"}}, + {Type: asserts.SnapDeclarationType, PrimaryKey: []string{"16", "snap-id-1"}}, + }) +} + +func (sds *snapDevSuite) TestPrerequisitesWithDevelopers(c *C) { + encoded := strings.Replace( + sds.validEncoded, sds.developersLines, + "developers:\n"+ + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n"+ + " -\n developer-id: dev-id3\n since: 2017-01-01T00:00:00.0Z\n", + 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + prereqs := assert.Prerequisites() + sort.Sort(RefSlice(prereqs)) + c.Assert(prereqs, DeepEquals, []*asserts.Ref{ + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id1"}}, + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id2"}}, + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id3"}}, + {Type: asserts.SnapDeclarationType, PrimaryKey: []string{"16", "snap-id-1"}}, + }) +} + +func (sds *snapDevSuite) TestPrerequisitesWithDeveloperRepeated(c *C) { + encoded := strings.Replace( + sds.validEncoded, sds.developersLines, + "developers:\n"+ + " -\n developer-id: dev-id2\n since: 2015-01-01T00:00:00.0Z\n until: 2016-01-01T00:00:00.0Z\n"+ + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n", + 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + prereqs := assert.Prerequisites() + sort.Sort(RefSlice(prereqs)) + c.Assert(prereqs, DeepEquals, []*asserts.Ref{ + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id1"}}, + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id2"}}, + {Type: asserts.SnapDeclarationType, PrimaryKey: []string{"16", "snap-id-1"}}, + }) +} + +func (sds *snapDevSuite) TestPrerequisitesWithPublisherAsDeveloper(c *C) { + encoded := strings.Replace( + sds.validEncoded, sds.developersLines, + "developers:\n -\n developer-id: dev-id1\n since: 2017-01-01T00:00:00.0Z\n", + 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + prereqs := assert.Prerequisites() + sort.Sort(RefSlice(prereqs)) + c.Assert(prereqs, DeepEquals, []*asserts.Ref{ + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id1"}}, + {Type: asserts.SnapDeclarationType, PrimaryKey: []string{"16", "snap-id-1"}}, + }) +} + +type RefSlice []*asserts.Ref + +func (s RefSlice) Len() int { + return len(s) +} + +func (s RefSlice) Less(i, j int) bool { + iref, jref := s[i], s[j] + if v := strings.Compare(iref.Type.Name, jref.Type.Name); v != 0 { + return v == -1 + } + for n, ipk := range iref.PrimaryKey { + jpk := jref.PrimaryKey[n] + if v := strings.Compare(ipk, jpk); v != 0 { + return v == -1 + } + } + return false +} + +func (s RefSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} diff --git a/asserts/snapasserts/snapasserts.go b/asserts/snapasserts/snapasserts.go new file mode 100644 index 00000000..2cbb1040 --- /dev/null +++ b/asserts/snapasserts/snapasserts.go @@ -0,0 +1,155 @@ +// -*- 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" +) + +type Finder interface { + // Find an assertion based on arbitrary headers. Provided + // headers must contain the primary key for the assertion + // type. It returns a asserts.NotFoundError if the assertion + // cannot be found. + Find(assertionType *asserts.AssertionType, headers map[string]string) (asserts.Assertion, error) +} + +func findSnapDeclaration(snapID, name string, db Finder) (*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 instance 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(instanceName, snapSHA3_384 string, snapSize uint64, si *snap.SideInfo, db Finder) 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", instanceName, 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", instanceName, 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", instanceName, si.Revision, snapID, snapRev.SnapRevision(), snapRev.SnapID()) + } + + snapDecl, err := findSnapDeclaration(snapID, instanceName, db) + if err != nil { + return err + } + + if snapDecl.SnapName() != snap.InstanceSnap(instanceName) { + return fmt.Errorf("cannot install %q, snap %q is undergoing a rename to %q", instanceName, snap.InstanceSnap(instanceName), 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 an asserts.NotFoundError if it cannot find them. +func DeriveSideInfo(snapPath string, db Finder) (*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) +} + +// FetchStore fetches the store assertion and its prerequisites for the given store id using the given fetcher. +func FetchStore(f asserts.Fetcher, storeID string) error { + ref := &asserts.Ref{ + Type: asserts.StoreType, + PrimaryKey: []string{storeID}, + } + + return f.Fetch(ref) +} diff --git a/asserts/snapasserts/snapasserts_test.go b/asserts/snapasserts/snapasserts_test.go new file mode 100644 index 00000000..b0d64d04 --- /dev/null +++ b/asserts/snapasserts/snapasserts_test.go @@ -0,0 +1,334 @@ +// -*- 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) { + s.storeSigning = assertstest.NewStoreStack("can0nical", nil) + + 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, with the regular snap name + err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB) + c.Check(err, IsNil) + // and a snap instance name + err = snapasserts.CrossCheck("foo_instance", 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)) + err = snapasserts.CrossCheck("foo_instance", digest, size+1, si, s.localDB) + c.Check(err, ErrorMatches, fmt.Sprintf(`snap "foo_instance" 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`) + err = snapasserts.CrossCheck("foo_instance", digest, size, &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(21), + }, s.localDB) + c.Check(err, ErrorMatches, `snap "foo_instance" 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`) + err = snapasserts.CrossCheck("foo_instance", digest, size, &snap.SideInfo{ + SnapID: "snap-id-other", + Revision: snap.R(12), + }, s.localDB) + c.Check(err, ErrorMatches, `snap "foo_instance" 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 "baz", snap "baz" is undergoing a rename to "foo"`) + err = snapasserts.CrossCheck("baz_instance", digest, size, si, s.localDB) + c.Check(err, ErrorMatches, `cannot install "baz_instance", snap "baz" 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`) + err = snapasserts.CrossCheck("foo_instance", digest, size, si, s.localDB) + c.Check(err, ErrorMatches, `cannot install snap "foo_instance" 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(asserts.IsNotFound(err), Equals, true) +} + +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/store_asserts.go b/asserts/store_asserts.go new file mode 100644 index 00000000..74c559de --- /dev/null +++ b/asserts/store_asserts.go @@ -0,0 +1,162 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts + +import ( + "fmt" + "net/url" + "time" +) + +// Store holds a store assertion, defining the configuration needed to connect +// a device to the store or relative to a non-default store. +type Store struct { + assertionBase + url *url.URL + friendlyStores []string + timestamp time.Time +} + +// Store returns the identifying name of the operator's store. +func (store *Store) Store() string { + return store.HeaderString("store") +} + +// OperatorID returns the account id of the store's operator. +func (store *Store) OperatorID() string { + return store.HeaderString("operator-id") +} + +// URL returns the URL of the store's API. +func (store *Store) URL() *url.URL { + return store.url +} + +// FriendlyStores returns stores holding snaps that are also exposed +// through this one. +func (store *Store) FriendlyStores() []string { + return store.friendlyStores +} + +// Location returns a summary of the store's location/purpose. +func (store *Store) Location() string { + return store.HeaderString("location") +} + +// Timestamp returns the time when the store assertion was issued. +func (store *Store) Timestamp() time.Time { + return store.timestamp +} + +func (store *Store) checkConsistency(db RODatabase, acck *AccountKey) error { + // Will be applied to a system's snapd or influence snapd + // policy decisions (via friendly-stores) so must be signed by a trusted + // authority! + if !db.IsTrustedAccount(store.AuthorityID()) { + return fmt.Errorf("store assertion %q is not signed by a directly trusted authority: %s", + store.Store(), store.AuthorityID()) + } + + _, err := db.Find(AccountType, map[string]string{"account-id": store.OperatorID()}) + if err != nil { + if IsNotFound(err) { + return fmt.Errorf( + "store assertion %q does not have a matching account assertion for the operator %q", + store.Store(), store.OperatorID()) + } + return err + } + + return nil +} + +// Prerequisites returns references to this store's prerequisite assertions. +func (store *Store) Prerequisites() []*Ref { + return []*Ref{ + {AccountType, []string{store.OperatorID()}}, + } +} + +// checkStoreURL validates the "url" header and returns a full URL or nil. +func checkStoreURL(headers map[string]interface{}) (*url.URL, error) { + s, err := checkOptionalString(headers, "url") + if err != nil { + return nil, err + } + + if s == "" { + return nil, nil + } + + errWhat := `"url" header` + + u, err := url.Parse(s) + if err != nil { + return nil, fmt.Errorf("%s must be a valid URL: %s", errWhat, s) + } + if u.Scheme != "http" && u.Scheme != "https" { + return nil, fmt.Errorf(`%s scheme must be "https" or "http": %s`, errWhat, s) + } + if u.Host == "" { + return nil, fmt.Errorf(`%s must have a host: %s`, errWhat, s) + } + if u.RawQuery != "" { + return nil, fmt.Errorf(`%s must not have a query: %s`, errWhat, s) + } + if u.Fragment != "" { + return nil, fmt.Errorf(`%s must not have a fragment: %s`, errWhat, s) + } + + return u, nil +} + +func assembleStore(assert assertionBase) (Assertion, error) { + _, err := checkNotEmptyString(assert.headers, "operator-id") + if err != nil { + return nil, err + } + + url, err := checkStoreURL(assert.headers) + if err != nil { + return nil, err + } + + friendlyStores, err := checkStringList(assert.headers, "friendly-stores") + if err != nil { + return nil, err + } + + _, err = checkOptionalString(assert.headers, "location") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &Store{ + assertionBase: assert, + url: url, + friendlyStores: friendlyStores, + timestamp: timestamp, + }, nil +} diff --git a/asserts/store_asserts_test.go b/asserts/store_asserts_test.go new file mode 100644 index 00000000..8bc37d1f --- /dev/null +++ b/asserts/store_asserts_test.go @@ -0,0 +1,235 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package asserts_test + +import ( + "fmt" + "strings" + "time" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + . "gopkg.in/check.v1" +) + +var _ = Suite(&storeSuite{}) + +type storeSuite struct { + ts time.Time + tsLine string + validExample string +} + +func (s *storeSuite) SetUpSuite(c *C) { + s.ts = time.Now().Truncate(time.Second).UTC() + s.tsLine = "timestamp: " + s.ts.Format(time.RFC3339) + "\n" + s.validExample = "type: store\n" + + "authority-id: canonical\n" + + "store: store1\n" + + "operator-id: op-id1\n" + + "url: https://store.example.com\n" + + "location: upstairs\n" + + s.tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n" + + "\n" + + "AXNpZw==" +} + +func (s *storeSuite) TestDecodeOK(c *C) { + a, err := asserts.Decode([]byte(s.validExample)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.StoreType) + store := a.(*asserts.Store) + + c.Check(store.OperatorID(), Equals, "op-id1") + c.Check(store.Store(), Equals, "store1") + c.Check(store.URL().String(), Equals, "https://store.example.com") + c.Check(store.Location(), Equals, "upstairs") + c.Check(store.Timestamp().Equal(s.ts), Equals, true) + c.Check(store.FriendlyStores(), HasLen, 0) +} + +var storeErrPrefix = "assertion store: " + +func (s *storeSuite) TestDecodeInvalidHeaders(c *C) { + tests := []struct{ original, invalid, expectedErr string }{ + {"store: store1\n", "", `"store" header is mandatory`}, + {"store: store1\n", "store: \n", `"store" header should not be empty`}, + {"operator-id: op-id1\n", "", `"operator-id" header is mandatory`}, + {"operator-id: op-id1\n", "operator-id: \n", `"operator-id" header should not be empty`}, + {"url: https://store.example.com\n", "url:\n - foo\n", `"url" header must be a string`}, + {"location: upstairs\n", "location:\n - foo\n", `"location" header must be a string`}, + {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: .*`}, + {"url: https://store.example.com\n", "friendly-stores: foo\n", `"friendly-stores" header must be a list of strings`}, + } + + for _, test := range tests { + invalid := strings.Replace(s.validExample, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, storeErrPrefix+test.expectedErr) + } +} + +func (s *storeSuite) TestURLOptional(c *C) { + tests := []string{"", "url: \n"} + for _, test := range tests { + encoded := strings.Replace(s.validExample, "url: https://store.example.com\n", test, 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + store := assert.(*asserts.Store) + c.Check(store.URL(), IsNil) + } +} + +func (s *storeSuite) TestURL(c *C) { + tests := []struct { + url string + err string + }{ + // Valid URLs. + {"http://example.com/", ""}, + {"https://example.com/", ""}, + {"https://example.com/some/path/", ""}, + {"https://example.com:443/", ""}, + {"https://example.com:1234/", ""}, + {"https://user:pass@example.com/", ""}, + {"https://token@example.com/", ""}, + + // Invalid URLs. + {"://example.com", `"url" header must be a valid URL`}, + {"example.com", `"url" header scheme must be "https" or "http"`}, + {"//example.com", `"url" header scheme must be "https" or "http"`}, + {"ftp://example.com", `"url" header scheme must be "https" or "http"`}, + {"mailto:someone@example.com", `"url" header scheme must be "https" or "http"`}, + {"https://", `"url" header must have a host`}, + {"https:///", `"url" header must have a host`}, + {"https:///some/path", `"url" header must have a host`}, + {"https://example.com/?foo=bar", `"url" header must not have a query`}, + {"https://example.com/#fragment", `"url" header must not have a fragment`}, + } + + for _, test := range tests { + encoded := strings.Replace( + s.validExample, "url: https://store.example.com\n", + fmt.Sprintf("url: %s\n", test.url), 1) + assert, err := asserts.Decode([]byte(encoded)) + if test.err != "" { + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, storeErrPrefix+test.err+": "+test.url) + } else { + c.Assert(err, IsNil) + c.Check(assert.(*asserts.Store).URL().String(), Equals, test.url) + } + } +} + +func (s *storeSuite) TestLocationOptional(c *C) { + encoded := strings.Replace(s.validExample, "location: upstairs\n", "", 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) +} + +func (s *storeSuite) TestLocation(c *C) { + for _, test := range []string{"foo", "bar", ""} { + encoded := strings.Replace( + s.validExample, "location: upstairs\n", + fmt.Sprintf("location: %s\n", test), 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + store := assert.(*asserts.Store) + c.Check(store.Location(), Equals, test) + } +} + +func (s *storeSuite) TestCheckAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + // Add account for operator. + operator := assertstest.NewAccount(storeDB, "op-id1", nil, "") + err := db.Add(operator) + c.Assert(err, IsNil) + + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + storeHeaders := map[string]interface{}{ + "store": "store1", + "operator-id": operator.HeaderString("account-id"), + "timestamp": time.Now().Format(time.RFC3339), + } + + // store signed by some other account fails. + store, err := otherDB.Sign(asserts.StoreType, storeHeaders, nil, "") + c.Assert(err, IsNil) + err = db.Check(store) + c.Assert(err, ErrorMatches, `store assertion "store1" is not signed by a directly trusted authority: other`) + + // but succeeds when signed by a trusted authority. + store, err = storeDB.Sign(asserts.StoreType, storeHeaders, nil, "") + c.Assert(err, IsNil) + err = db.Check(store) + c.Assert(err, IsNil) +} + +func (s *storeSuite) TestFriendlyStores(c *C) { + encoded := strings.Replace(s.validExample, "url: https://store.example.com\n", `friendly-stores: + - store1 + - store2 + - store3 +`, 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + store := assert.(*asserts.Store) + c.Check(store.URL(), IsNil) + c.Check(store.FriendlyStores(), DeepEquals, []string{"store1", "store2", "store3"}) +} + +func (s *storeSuite) TestCheckOperatorAccount(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + store, err := storeDB.Sign(asserts.StoreType, map[string]interface{}{ + "store": "store1", + "operator-id": "op-id1", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + // No account for operator op-id1 yet, so Check fails. + err = db.Check(store) + c.Assert(err, ErrorMatches, `store assertion "store1" does not have a matching account assertion for the operator "op-id1"`) + + // Add the op-id1 account. + operator := assertstest.NewAccount(storeDB, "op-id1", map[string]interface{}{"account-id": "op-id1"}, "") + err = db.Add(operator) + c.Assert(err, IsNil) + + // Now the operator exists so Check succeeds. + err = db.Check(store) + c.Assert(err, IsNil) +} + +func (s *storeSuite) TestPrerequisites(c *C) { + assert, err := asserts.Decode([]byte(s.validExample)) + c.Assert(err, IsNil) + c.Assert(assert.Prerequisites(), DeepEquals, []*asserts.Ref{ + {Type: asserts.AccountType, PrimaryKey: []string{"op-id1"}}, + }) +} diff --git a/asserts/sysdb/generic.go b/asserts/sysdb/generic.go new file mode 100644 index 00000000..e12cba59 --- /dev/null +++ b/asserts/sysdb/generic.go @@ -0,0 +1,196 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package sysdb + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/osutil" +) + +const ( + encodedGenericAccount = `type: account +authority-id: canonical +account-id: generic +display-name: Generic +timestamp: 2017-07-27T00:00:00.0Z +username: generic +validation: certified +sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk + +AcLDXAQAAQoABgUCWYuVIgAKCRDUpVvql9g3II66IACcoxSoX8+PQLa9TNuNBUs3bdTW6V5ZOdE8 +vnziIg+yqu3qYfWHcRf1qu7K9Igv5lH3uM5jh2AHlndaoX4Qg1Rm9rOZCkRr1dDUmdRDBXN2pdTA +oydd0Ivpeai4ATbSZs11h50/vN/mxBwM6TzdGHqRNt6lvygAPe7VtfchSW/J0NsSIHr9SUeuIHkJ +C79DV27B+9/m8pnpKJo/Fv8nKGs4sMduKVjrj9Po3UhpZEQWf3I3SeDI5IE4TgoDe+O7neGUtT6W +D9wnMWLphC+rHbJguxXG/fmnUYiM2U8o4WVrs/fjF0zDRH7rY3tbLPbFXf2OD4qfOvS//VLQWeCK +KAgKhwz0d5CqaHyKSplywSvwO/dxlrqOjt39k3EjYxVuNS5UQk/BzPoDZD5maisCFm9JZwqBlWHP +6XTj8rhHSkNAPXezs2ZpVSsdtNYmpLLzWIFsAviuoMjYYDyL6jZrD4RBNrNOvSNQGLezB+eyI5DW +9vr2ppCw8zr49epPvJ4uqj/AILgr52zworl7v/27X67BOSoRMmE4AOnvjSJ8cN6Yt83AuEI4aZbP +DlF2Znqp8o/srtmJ3ZMpsjIsAqVhCeTU6eWXbYfNUlIMSmC6CDwQQzsukU4M6NEwUQbWddiM3iNL +FdeFsBscXg4Qm/0Y3PULriDoct+VpBUhzwVXG+Lj6rjtcX7n1C/7u9i/+WIBJ7jU4FBjwOdgpSCQ +DSCb0PgTM2PfbScFpn3KVYs0kT/Jc40Lpw6CUG9iUIdz5qlJzhbRiuhU8yjEg9q/5lWizAuxcP+P +anNhmNXsme46IJh7WnlzPAVMsToz8bWY01LC3t33pPGlRJo109PMbNK7reMIb4KFiL4Hy7gVmTj9 +uydReVBUTZuMLRq1ShAJNScZ+HTpWruLoiC87Rf1++1KakahmtWYCdlJv/JSOyjSh8D9h0GEmqON +lKmzrNgQS8QhLh5uBcITN2Kt1UFGu2o9I8l0TgD5Uh9fG/R/A536fpcvIzOA/lhVttO6P9POwUVv +RIBZ3TpVOSzQ+ADpDexRUouPLPkgUwVBSctcgafMaj/w8DsULvlOYd3Sqq9a+zg6bZr9oPBKutUn +YkIUWmLW1OsdQ2eBS9dFqzJTAOELxNOUq37UGnIrMbk3Vn8hLK+S/+W9XL6WVxzmL1PT9FJZZ41p +KdaFV+mvrTfyoxuzXxkWbMwQkc56Ifn+IojbDwMI4FcTcl4dOeUrlnqwBJmTTwEhLVkYDvzYsVV9 +4joFUWhp10JMm3lO+3596m0kYWMhyvGfYnH7QcQ3GtMAz82yRHc1X+seeWyD/aIjlHYNYfaJ5Ogs +VC76lXi7swMtA9jV5FJIGmQufLo9f93NSYxqwpa8 +` + + encodedGenericModelsAccountKey = `type: account-key +authority-id: canonical +public-key-sha3-384: d-JcZF9nD9eBw7bwMnH61x-bklnQOhQud1Is6o_cn2wTj8EYDi9musrIT9z2MdAa +account-id: generic +name: models +since: 2017-07-27T00:00:00.0Z +body-length: 717 +sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk + +AcbBTQRWhcGAARAAoRakbLAMMoPeMz5MLCzAR6ALu/xxP9PuCdkknHH5lJrKE2adFj22DMwjWKj6 +0pZU1Ushv4r7eb1NmFfl7a6Pz5ert+O5Qt53feK30+yiZF+Pgsx46SVTGy8QvicxhDhChdJ7ugW2 +Vbz8dXDT9gv1E5hLl2BiuxxZHtMMTitO3bCtQcM/YwUeFljZZYd1FwxtgolnA5IUcHomIEQ5Xw6X +dCYGNkVjenb8aLBfi/ZZ84LHQjSbo3b87KP7syeEH2uuFJ2W8ZwGfUCll84gF+lYiLO6BQk8psIR +aRqnPfdjeuYg0ZLhdNV2Gu6GTNYMSrGLJ4vafAoIoMOifeIfK/DjN0XpfUIYwrM3UIvssEaLyE0L +i30PN5bpmmyfj5EDkJj9DqHzBly1kc20ciEtVCwOUijhQr4UjjfPiJFyed1/yndY1z/L85iATcsb +mwAw/wOyHKge/mlVztXV2H8DywcLV8Kbo5/ZZzcdKhDgL9URosQ5bMeYDPWwPsS02exHFl150dpR +p6MmeSCFPiQQjDrM3nWXLv/uemBE1IgX5q2eW6kJbSvRn519O3OrFEs2NBMEgvE3mIvewNlxFbDj +96Oj54Zh3rVtYu/g9yo2Bb2uf9gpOGS6TxrqN3aP5FigZzxkMCGFG8UOOFI7k2eQjMd8va5V8JTZ +ijWZgBjDB1YuQ1MAEQEAAQ== + +AcLDXAQAAQoABgUCWYuUigAKCRDUpVvql9g3IOobH/wLm7sfLu3A/QWrdrMB1xRe6JOKuOQoNEt0 +Vhg8q4MgOt1mxPzBUMGBJCcq9EiTYaUT4eDXSJL1OKFgh42oK5uY+GLsPWamxBY1Rg6QoESjJPcS +2niwTOjjTdpIrZ5M3pKRmxTxT+Wsq9j+1t4jvy/baI6+uO6KQh0UIMyOEhG+uJ8aJ2OcF3uV5gtF +fL1Y4Jr1Ir/4B2K7s8OhlrO1Yw3woB+YIkOjJ6oAOfQx5B/p1vK4uXOCIZarcfYX4XOhNgvPGaeL +O+NHk3GwTmEBngs49E8zq8ii8OoqIT6YzUd4taqHvZD4inTlw6MKGld7myCbZVZ3b0NXosplwYXa +jVL9ZBWTJukcIs4jEJ0XkTEuwvOpiGbtXdmDDlOSYkhZQdmQn3CIveGLRFa6pCi9a/jstyB+4sgk +MnwmJxEg8L3i1OvjgUM8uexCfg4cBVP9fCKuaC26uAXUiiHz7mIZhVSlLXHgUgMn5jekluPndgRZ +D2mGG0WscTMTb9uOpbLo6BWCwM7rGaZQgVSZsIj1cise05fjGpOozeqDhG25obcUXxhIUStztc9t +Z9MwSz9xdsUqV8XztEhkgfc7dh2fPWluiE9hLrdzyoU1xE6syujm8HE+bIJnDFYoE/Kw6WqIEm/3 +mWhnOmi9uZsMBErKZKO4sqcLfR/zIn2Lx0ivg/yZzHHnDY5hwdrhQtn+AHCb+QJ9AJVte9hI+kt+ +Fv8neohiMTCY8XxjrdB3QBPGesVsIMI5zAd14X4MqNKBYb4Ucg8YCIj7WLkQHbHO1GQwhPY8Tl9u +QqysZo/WnLVuvaruEBsBBGUJ7Ju5GtFKdWMdoH3YQmYHdxxxK37NPqBY70OrTSFJU5QT6PGFSvif +aMDg0X/aRj2uE3vgTI5hdqI4JYv1Mt1gYOPv4AMx/o/2q9dVENFYMTXcYBITMScUVV8NzmH8SNge +w7AWUPlQvWGZbTz62lYXHuUX1cdzz37B0LrEjh1ZC1V8emzfkLzEFYP/qUk1c4NjKsTjj5d463Gq +cn31Mr83tt5l7HWwP8bvTMIj98bOIJapsncGOzPYhs8cjZeOy0Q7EcvHjGRrj26CGWZacT3f0A0e +kb66ocAxV4nH1FDsfn8KdLKFgmSmW6SXkD2nqY94/pommJzUBF6s54DijZMXqHRwIRyPA8ymrCGt +t4shJh7dobC8Tg6RA84Bf9HkeqI97PQYFYMuNX0U59x2s0IQsOAYjH53NIf/jSPC4GDvLs7k+O76 +R2PJK1VN6/ckJZAb3Rum5Ak5sbLTpRAVHIAVU1NAjHc5lYUHhxXJmJsbw6Jawb9Xb3T96s+WdD3Y +062upMY95pr0ZPf1tVGgzpcVCEw7yAOw+SkMksx+ +` + + encodedGenericClassicModel = `type: model +authority-id: generic +series: 16 +brand-id: generic +model: generic-classic +classic: true +timestamp: 2017-07-27T00:00:00.0Z +sign-key-sha3-384: d-JcZF9nD9eBw7bwMnH61x-bklnQOhQud1Is6o_cn2wTj8EYDi9musrIT9z2MdAa + +AcLBXAQAAQoABgUCWYuXiAAKCRAdLQyY+/mCiST0D/0XGQauzV2bbTEy6DkrR1jlNbI6x8vfIdS8 +KvEWYvzOWNhNlVSfwNOkFjs3uMHgCO6/fCg03wGXTyV9D7ZgrMeUzWrYp6EmXk8/LQSaBnff86XO +4/vYyfyvEYavhF0kQ6QGg8Cqr0EaMyw0x9/zWEO/Ll9fH/8nv9qcQq8N4AbebNvNxtGsCmJuXpSe +2rxl3Dw8XarYBmqgcBQhXxRNpa6/AgaTNBpPOTqgNA8ZtmbZwYLuaFjpZP410aJSs+evSKepy/ce ++zTA7RB3384YQVeZDdTudX2fGtuCnBZBAJ+NYlk0t8VFXxyOhyMSXeylSpNSx4pCqmUZRyaf5SDS +g1XxJet4IP0stZH1SfPOwc9oE81/bJlKsb9QIQKQRewvtUCLfe9a6Vy/CYd2elvcWOmeANVrJK0m +nRaz6VBm09RJTuwUT6vNugXSOCeF7W3WN1RHJuex0zw+nP3eCehxFSr33YrVniaA7zGfjXvS8tKx +AINNQB4g2fpfet4na6lPPMYM41WHIHPCMTz/fJQ6dZBSEg6UUZ/GiQhGEfWPBteK7yd9pQ8qB3fj +ER4UvKnR7hcVI26e3NGNkXP5kp0SFCkV5NQs8rzXzokpB7p/V5Pnqp3Km6wu45cU6UiTZFhR2IMT +l+6AMtrS4gDGHktOhwfmOMWqmhvR/INF+TjaWbsB6g== +` +) + +var ( + genericAssertions []asserts.Assertion + genericStagingAssertions []asserts.Assertion + genericExtraAssertions []asserts.Assertion + + genericClassicModel *asserts.Model + genericStagingClassicModel *asserts.Model + genericClassicModelOverride *asserts.Model +) + +func init() { + genericAccount, err := asserts.Decode([]byte(encodedGenericAccount)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s account: %v`, err)) + } + genericModelsAccountKey, err := asserts.Decode([]byte(encodedGenericModelsAccountKey)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s "models" account-key: %v`, err)) + } + + genericAssertions = []asserts.Assertion{genericAccount, genericModelsAccountKey} + + a, err := asserts.Decode([]byte(encodedGenericClassicModel)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s "generic-classic" model: %v`, err)) + } + genericClassicModel = a.(*asserts.Model) +} + +// Generic returns a copy of the current set of predefined assertions for the 'generic' authority as used by Open. +func Generic() []asserts.Assertion { + generic := []asserts.Assertion(nil) + if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + generic = append(generic, genericAssertions...) + } else { + generic = append(generic, genericStagingAssertions...) + } + generic = append(generic, genericExtraAssertions...) + return generic +} + +// InjectGeneric injects further predefined assertions into the set used Open. +// Returns a restore function to reinstate the previous set. Useful +// for tests or called globally without worrying about restoring. +func InjectGeneric(extra []asserts.Assertion) (restore func()) { + prev := genericExtraAssertions + genericExtraAssertions = make([]asserts.Assertion, len(prev)+len(extra)) + copy(genericExtraAssertions, prev) + copy(genericExtraAssertions[len(prev):], extra) + return func() { + genericExtraAssertions = prev + } +} + +// GenericClassicModel returns the model assertion for the "generic"'s "generic-classic" fallback model. +func GenericClassicModel() *asserts.Model { + if genericClassicModelOverride != nil { + return genericClassicModelOverride + } + if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + return genericClassicModel + } else { + return genericStagingClassicModel + } +} + +// MockGenericClassicModel mocks the predefined generic-classic model returned by GenericClassicModel. +func MockGenericClassicModel(mod *asserts.Model) (restore func()) { + prevOverride := genericClassicModelOverride + genericClassicModelOverride = mod + return func() { + genericClassicModelOverride = prevOverride + } +} diff --git a/asserts/sysdb/staging.go b/asserts/sysdb/staging.go new file mode 100644 index 00000000..c26a48a4 --- /dev/null +++ b/asserts/sysdb/staging.go @@ -0,0 +1,183 @@ +// -*- 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== +` + encodedStagingGenericAccount = `type: account +authority-id: canonical +account-id: generic +display-name: Generic +timestamp: 2017-07-27T00:00:00.0Z +username: generic +validation: certified +sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu + +AcLBXAQAAQoABgUCWXmmFAAKCRAHKljtl9kuLkAWD/98LgECwAN8S09o4aEFpdGXgWpx8z58wl6T +5mZVDyYpCV9ugC2DqBqGQxp4X1P7Wn9+weXw8nmL7IywVn/hCVHJOmBLJSr3wLjpVBY9RrIHYoXi +k9W7IFo4ggw1j1FRLg2tKk81MnK0fK/Qws9OXzilDir5R2bQ/E0sodGW3NpbwtbpkY/BtP6YPoJ/ +1+205KG5m6oG8y6mf74bjMGfJ+iFFpIDayIpXl+YTkJ25BOVGcuC66cIrmdc63rBIHL2tU/3GUMB +xZGiyG9Fuli1uV4ALhN9j43hxAtVwXOn/qgOiN8TGQz3OvlVUXTuFVmkdvCdfT2XHrJjFmEs9SlL +u2EEmvaNFJ61lQG/VrN6O0BswenTlIO0tTFe126o/cTmKg8/ga4v2WjMlcOCzfu+cIZIzTTnn4Le +iXdQ6+c3QN+Co4SI0UvgJ4nGWQ9W+4q4xVJTliKTzK2BZ40vHUi51rMC/puqsMpnAbHSn4iy8vpf +CyJh7jyuITPEzfpurNMb+VD+1Brd2DJCVnlwQq+rzNerXd5xcHCdZsfX+ATukHgYTZWa467ZEFhI +Bk1xUWAYs8r2JDFb5YPtZuW7Vt1UUpFdx6DroL6OODvZ6mDUtsOa8nm7G1l4uRJtqunplPyCDjnL +aQhlAouLMltWeGITO+5jePHJKTnYQAFEvo0WIgEYpA== +` + encodedStagingGenericModelsAccountKey = `type: account-key +authority-id: canonical +public-key-sha3-384: 93jDrIGOXymDg9BPCLES5mAr6aGXU7e0wwXeJlIYIWbUzM_kB81CiqX7cTlB9Y1z +account-id: generic +name: models +since: 2017-07-27T00:00:00.0Z +body-length: 717 +sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu + +AcbBTQRWhcGAARAAxcyFC13COEmIwWwLsjp4AAILhWSp8/dQ6cOzY3T7tqqoSn9iKyidpJTfrtml +DKHZe0zC10fog2Mvp1AO7dNqK9kHUdCQE+YatHmkm1a3QoZqwJsj77w09Q+l1uvDjrfrF0S/KcYa +0hfDZQ+51T1msbatWN2qU42dX280IMV+zo1GpKK8z6br4glY2tki4CJokVAAt+bl4bBqDZ4EoBYe +9CsACmNhw2d/fOAlis2jwG3tWXMORX9FcGRx/COasvRb7rjA0DJfKxOnTw7uC0UjDUB6bU6O0smS +Q5oK3V5fJIAcMBXNe5MkdTKGLY61hTFLiw4F6MkrM3O8dnXtexCojV+QtROTIM4R2dJTOv7r2s/9 +KT4wIQmOcjMEWxyq1H1rqjCHBjGnKa6GC1j/4NwqlxUEiqZMYs12px9ypeEqjL3tKURCanPOlwXO +p2E1i+V53XznnS3RA7I6Aa37w/9clTJk5vzVT8G6+k6xsB9zwKYOsipG0zHjyuW9Qtkd15bA0Iv9 +MrZGE4U7RwEnt4jBa98rcLs1sCkJEau0hEU4MiPyqi8XL2b/TtPnCwN8rQRVQvakzGw83Ol/B8ZI +2OGu0aB6HAWbdy81yXIUES9ZtH7nK5X7dSdJu92wXBMOyel9cryHzlYFjSlPKyqRx2lsYk4K6Hiq +VRY3L12yjXkcHcsAEQEAAQ== + +AcLBXAQAAQoABgUCWXmevAAKCRAHKljtl9kuLltVEADAfpBlY4b4oImKPq8Pp6UKFjgcMVjJLcSI +EOfAMygIaZwzNSuOh2wPRBAMMZlcFlBEFLfGbh7R2RG1/R7PSR4q+gMZZ3qJ5QUjUUuGnkSfCLhK +jVtPlX8qdPWTdEgUeTEKNzHogP0MiIChHdeuv/iQ9fSgdw/lBZsblKAdrHv00ZQHup9XGWdZ4Fnb +cSiK9tZ3/nZAG18PErEh7wJntwygqcjScS0jTSa5BecQoy8O6wxKafQgxuixdHw+dt6sa34qzwel +ROb1VmNcmMGsv2YuPsRcqjgvL7drDzXRRcYhmiSCUFhGPx3RY0UWO9G9Pzok64l/1D7o6Xah3h4D +oxkepM5JTAiy165kfQzEFGMtvlv0d3mOCLMWqJzjzhhn/bPcoh5MO/PhpSR1y71tjjWtKR4SD1K/ +feR+KE73gEgqmHLss3TF/O2RxvbV15W0paxmiffUyeE/uQ1p5ddmBwke/gM/OOgUA4G3g9vgTeaQ +YVFCD0h75mX9GylVXUMmxlYSRVO59JsxCpWSqXWh4xkigbBZSAOPn6vkM/4nxZLf/ufVyNqnwo2r +Si/lWgk/lJoHaTVsqm9n0DxiR8lH54eFghprkN+KgGFMKlY127n50CEwqG1j4gKfjxmRycgKbx5O +a0IGmGjVnVmGOdX9wpg6fBHbhczXZtId02Q7yzF87A== +` + + encodedStagingGenericClassicModel = `type: model +authority-id: generic +series: 16 +brand-id: generic +model: generic-classic +classic: true +timestamp: 2017-07-27T00:00:00.0Z +sign-key-sha3-384: 93jDrIGOXymDg9BPCLES5mAr6aGXU7e0wwXeJlIYIWbUzM_kB81CiqX7cTlB9Y1z + +AcLBXAQAAQoABgUCWXnYbAAKCRDqhmvwxUsbelvOD/4qxiDs4blJoRSXmzvsKTyM/Z2QuLw9bqUj +QXKoCSB78ATFwr01kvqJMzwJ1eT4zKOajUERKPN9fN1af0w07DoYG5bt/Pb7s/UFDmwIQg244wLI +lQ/NPCAm4SEvN1GEe0OxdCpMuPe+x++FvFtnF7CXJPmLdHln6A1eMhwyxGX+el1QxhiR+mWLCCNp +B4ndjh154H5SXRw1lmUiYdE/kCsOqGeZ5ljTni+Rh8xDYxmVCthrLCUVtHhMVrKeylDwwS7Sf/HV +GY9r/C9r07xRom06bBN/vQwdoLzGuU3SS7UsN0Ud95tJhAUtP5jW1dN8otviMcOAdtj7jTwSX4FY +pdgmkldjaCRaHxBA923cjGgl98LCjbdG5KmmKoT6DTb3AyFOT2XwlRl/MaRJBK2Tp1nVNZDjLY4j +VfRETt17ZCONt3yn/OhQk8bV6EsdJvT2/nMlNejXgnMtLfbH8v6xWLKrLOVOjILVF5zgK8+z4+d2 +ILIZupGooMouhddmcHem76lSnS+y75NMQXg5lBrUU2xAQRloWTw0oF+Hr5vcZkX5f4R/yH8Zz1Dt ++zRs2zqOK5hjdejhU5x/N3KSBLy+TUMk7JsdVv0nhdpJUKrFyGWn+YzBNE2GgEfPfXnkaU91/AD2 +SWyt8kWVPmT3DCzs7u5IXYIVxcq4FjkmeU9sTrn88g== +` +) + +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} + + genericAccount, err := asserts.Decode([]byte(encodedStagingGenericAccount)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s account: %v`, err)) + } + genericModelsAccountKey, err := asserts.Decode([]byte(encodedStagingGenericModelsAccountKey)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s "models" account-key: %v`, err)) + } + + genericStagingAssertions = []asserts.Assertion{genericAccount, genericModelsAccountKey} + + a, err := asserts.Decode([]byte(encodedStagingGenericClassicModel)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s "generic-classic" model: %v`, err)) + } + genericStagingClassicModel = a.(*asserts.Model) +} diff --git a/asserts/sysdb/sysdb.go b/asserts/sysdb/sysdb.go new file mode 100644 index 00000000..66ef9cbe --- /dev/null +++ b/asserts/sysdb/sysdb.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 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(), + OtherPredefined: Generic(), + } + 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..2696477f --- /dev/null +++ b/asserts/sysdb/sysdb_test.go @@ -0,0 +1,216 @@ +// -*- 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 + extraGeneric []asserts.Assertion + otherModel *asserts.Model + 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": "verified", + "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} + + otherAcct := assertstest.NewAccount(signingDB, "gener1c", map[string]interface{}{ + "account-id": "gener1c", + "validation": "verified", + "timestamp": "2015-11-20T15:04:00Z", + }, "") + + sdbs.extraGeneric = []asserts.Assertion{otherAcct} + + a, err := signingDB.Sign(asserts.ModelType, map[string]interface{}{ + "series": "16", + "brand-id": "can0nical", + "model": "other-model", + "classic": "true", + "timestamp": "2015-11-20T15:04:00Z", + }, nil, "") + c.Assert(err, IsNil) + sdbs.otherModel = a.(*asserts.Model) + + 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) TestGeneric(c *C) { + generic := sysdb.Generic() + c.Check(generic, HasLen, 2) + + restore := sysdb.InjectGeneric(sdbs.extraGeneric) + defer restore() + + genericEx := sysdb.Generic() + c.Check(genericEx, HasLen, 3) +} + +func (sdbs *sysDBSuite) TestGenericClassicModel(c *C) { + m := sysdb.GenericClassicModel() + c.Assert(m, NotNil) + + c.Check(m.AuthorityID(), Equals, "generic") + c.Check(m.BrandID(), Equals, "generic") + c.Check(m.Model(), Equals, "generic-classic") + c.Check(m.Classic(), Equals, true) + + r := sysdb.MockGenericClassicModel(sdbs.otherModel) + defer r() + + m = sysdb.GenericClassicModel() + c.Check(m, Equals, sdbs.otherModel) +} + +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) + + c.Check(trustedAcc.(*asserts.Account).Validation(), Equals, "verified") + + err = db.Check(trustedAcc) + c.Check(err, IsNil) + + // check generic + genericAcc, err := db.Find(asserts.AccountType, map[string]string{ + "account-id": "generic", + }) + c.Assert(err, IsNil) + _, err = db.FindMany(asserts.AccountKeyType, map[string]string{ + "account-id": "generic", + "name": "models", + }) + c.Assert(err, IsNil) + + c.Check(genericAcc.(*asserts.Account).Validation(), Equals, "verified") + + err = db.Check(genericAcc) + c.Check(err, IsNil) + + err = db.Check(sysdb.GenericClassicModel()) + 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..9cd79640 --- /dev/null +++ b/asserts/sysdb/trusted.go @@ -0,0 +1,156 @@ +// -*- 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 { + if len(trustedStagingAssertions) == 0 { + panic("cannot work with the staging store without a testing build with compiled-in staging keys") + } + 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..5b71ed2b --- /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: 2018-06-27T14:25:40+02:00 +username: testrootorg +validation: verified +sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR + +AcLBUgQAAQoABgUCWzOCRAAA31gQAE5QgyBuxF3DGlMP32+3G5soq0uDLKG+sqFIEj/8j1dwLG0u +ut7UPEf5iTZFDqqyAaFBRUPxx1cGB/6WFrks3X3/325hVzv5DYA9d4508BXdlNBA++t7tTdb4rU6 +G57aVgbpMCdwdjRbRMv1LLVWnli1pj8Cvt/jiTMbJUwQ/CAO0UZA6EH+fAeHGB53NNvedAM1goWi +pS3XvruDtv8qTbVW9jNSIX1ADcLAbmM2xV2Vo54lfgN5NJd/4K4S7sPSsX7QLBghFkB0m9i5g/Qu +PecvJ9njebFF48yvG4W1owBNBfxD2oHNhK/GdtxsREDKgDXuIrhziXBzWNeYto8lCZ1D520k+xp+ +2rL1TYSy9IixOzAf2qBhUTQdXsoVfmBOyExlYVQDIFO+X4ufbhLzy2pTE4KWvFvF58HzGradbix6 +oUD5hiEjw1YoV8FKdMLDobcvGzgm+Kx/FQo2Iqm5GmfzPW/K3SntoptuHIDSk3B12F/F/EDoYiGS +MDWJJ4NMbFLMemJhvEI23IuZOTBEt27sGcgOju4wYkcsaHPEeXTGUBUgQADugBTwJtWmuybkmovM +aLn1kVYpht+0cZeAQR5L3nSOK7T3V+QvWSXt6PAiHJv+HnemrYarSmGVTDcpj1QuWXyX026RnkvP +SD73HCe5QPTjrvFvIa6o6n9khFgs +` + + 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..88309810 --- /dev/null +++ b/asserts/user.go @@ -0,0 +1,281 @@ +// -*- 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 + + forcePasswordChange bool +} + +// 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") +} + +// ForcePasswordChange returns true if the user needs to change the password +// after the first login. +func (su *SystemUser) ForcePasswordChange() bool { + return su.forcePasswordChange +} + +// 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 +} + +// see crypt(3) for the legal chars +var isValidSaltAndHash = regexp.MustCompile(`^[a-zA-Z0-9./]+$`).MatchString + +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) + } + } + + if !isValidSaltAndHash(shd.Salt) { + return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt) + } + if !isValidSaltAndHash(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 + } + password, err := checkHashedPassword(assert.headers, "password") + if err != nil { + return nil, err + } + forcePasswordChange, err := checkOptionalBool(assert.headers, "force-password-change") + if err != nil { + return nil, err + } + if forcePasswordChange && password == "" { + return nil, fmt.Errorf(`cannot use "force-password-change" with an empty "password"`) + } + + 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, + forcePasswordChange: forcePasswordChange, + }, nil +} diff --git a/asserts/user_test.go b/asserts/user_test.go new file mode 100644 index 00000000..e7e76ae8 --- /dev/null +++ b/asserts/user_test.go @@ -0,0 +1,214 @@ +// -*- 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) TestDecodeForcePasswdChange(c *C) { + + old := "password: $6$salt$hash\n" + new := "password: $6$salt$hash\nforce-password-change: true\n" + + valid := strings.Replace(s.systemUserStr, old, new, 1) + a, err := asserts.Decode([]byte(valid)) + c.Check(err, IsNil) + systemUser := a.(*asserts.SystemUser) + c.Check(systemUser.ForcePasswordChange(), Equals, true) +} + +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`}, + {"password: $6$salt$hash\n", "force-password-change: true\n", `cannot use "force-password-change" with an empty "password"`}, + {"password: $6$salt$hash\n", "password: $6$salt$hash\nforce-password-change: xxx\n", `"force-password-change" header must be 'true' or 'false'`}, + {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..39cfae35 --- /dev/null +++ b/boot/kernel_os.go @@ -0,0 +1,210 @@ +// -*- 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" + "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 +} + +// 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 base or kernel snap to be +// used in the next boot. For base snaps it up to the caller to select +// the right bootable base (from the model assertion). +func SetNextBoot(s *snap.Info) error { + if release.OnClassic { + return fmt.Errorf("cannot set next boot on classic systems") + } + + if s.Type != snap.TypeOS && s.Type != snap.TypeKernel && s.Type != snap.TypeBase { + return fmt.Errorf("cannot set next boot to snap %q with type %q", s.SnapName(), s.Type) + } + + 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, snap.TypeBase: + 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) + // and we are not in any special boot mode + m, err := bootloader.GetBootVars("snap_mode", goodBoot) + if err != nil { + return err + } + if m[goodBoot] == blobName { + // If we were in anything but default ("") mode before + // and now switch to the good core/kernel again, make + // sure to clean the snap_mode here. This also + // mitigates https://forum.snapcraft.io/t/5253 + if m["snap_mode"] != "" { + return bootloader.SetBootVars(map[string]string{ + "snap_mode": "", + nextBoot: "", + }) + } + return nil + } + + return bootloader.SetBootVars(map[string]string{ + nextBoot: blobName, + "snap_mode": "try", + }) +} + +// ChangeRequiresReboot returns whether a reboot is required to switch +// to the given OS, base or kernel snap. +func ChangeRequiresReboot(s *snap.Info) bool { + if s.Type != snap.TypeKernel && s.Type != snap.TypeOS && s.Type != snap.TypeBase { + 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, snap.TypeBase: + nextBoot = "snap_try_core" + goodBoot = "snap_core" + } + + m, err := bootloader.GetBootVars(nextBoot, goodBoot) + if err != nil { + logger.Noticef("cannot get boot variables: %s", err) + 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..a011ab9f --- /dev/null +++ b/boot/kernel_os_test.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 boot_test + +import ( + "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" + "github.com/snapcore/snapd/testutil" +) + +func TestBoot(t *testing.T) { TestingT(t) } + +type kernelOSSuite struct { + testutil.BaseTest + bootloader *boottest.MockBootloader +} + +var _ = Suite(&kernelOSSuite{}) + +func (s *kernelOSSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) + dirs.SetRootDir(c.MkDir()) + s.bootloader = boottest.NewMockBootloader("mock", c.MkDir()) + partition.ForceBootloader(s.bootloader) +} + +func (s *kernelOSSuite) TearDownTest(c *C) { + s.BaseTest.TearDownTest(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]) + c.Check(fullFn, testutil.FileEquals, 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.SideInfo{Revision: snap.R(42)}) + err := boot.SetNextBoot(snapInfo) + c.Assert(err, ErrorMatches, "cannot set next boot on classic systems") + + 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.ChangeRequiresReboot(info), Equals, true) +} + +func (s *kernelOSSuite) TestSetNextBootWithBaseForCore(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + info := &snap.Info{} + info.Type = snap.TypeBase + info.RealName = "core18" + info.Revision = snap.R(1818) + + err := boot.SetNextBoot(info) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snap_try_core": "core18_1818.snap", + "snap_mode": "try", + }) + + c.Check(boot.ChangeRequiresReboot(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.ChangeRequiresReboot(info), Equals, true) + + // simulate good boot + s.bootloader.BootVars["snap_kernel"] = "krnl_42.snap" + c.Check(boot.ChangeRequiresReboot(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) TestSetNextBootForKernelForTheSameKernelTryMode(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" + s.bootloader.BootVars["snap_try_kernel"] = "krnl_99.snap" + s.bootloader.BootVars["snap_mode"] = "try" + + err := boot.SetNextBoot(info) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snap_kernel": "krnl_40.snap", + "snap_try_kernel": "", + "snap_mode": "", + }) +} + +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..bf45f4cf --- /dev/null +++ b/client/aliases.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 client + +import ( + "bytes" + "encoding/json" +) + +// aliasAction represents an action performed on aliases. +// With action "unalias" if Snap and Alias are set to the same value, +// snapd will check if what is referred to is indeed a snap or an alias. +type aliasAction struct { + Action string `json:"action"` + Snap string `json:"snap,omitempty"` + App string `json:"app,omitempty"` + Alias string `json:"alias,omitempty"` +} + +// 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 sets up a manual alias from alias to app in snapName. +func (client *Client) Alias(snapName, app, alias string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "alias", + Snap: snapName, + App: app, + Alias: alias, + }) +} + +// // DisableAllAliases disables all aliases of a snap, removing all manual ones. +func (client *Client) DisableAllAliases(snapName string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "unalias", + Snap: snapName, + }) +} + +// RemoveManualAlias removes a manual alias. +func (client *Client) RemoveManualAlias(alias string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "unalias", + Alias: alias, + }) +} + +// Unalias tears down a manual alias or disables all aliases of a snap (removing all manual ones) +func (client *Client) Unalias(aliasOrSnap string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "unalias", + Snap: aliasOrSnap, + Alias: aliasOrSnap, + }) +} + +// Prefer enables all aliases of a snap in preference to conflicting aliases +// of other snaps whose aliases will be disabled (removed for manual ones). +func (client *Client) Prefer(snapName string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "prefer", + Snap: snapName, + }) +} + +// AliasStatus represents the status of an alias. +type AliasStatus struct { + Command string `json:"command"` + Status string `json:"status"` + Manual string `json:"manual,omitempty"` + Auto string `json:"auto,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..a850e529 --- /dev/null +++ b/client/aliases_test.go @@ -0,0 +1,195 @@ +// -*- 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", "cmd1", "alias1") + 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", "cmd1", "alias1") + 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", + "app": "cmd1", + "alias": "alias1", + }) +} + +func (cs *clientSuite) TestClientUnaliasCallsEndpoint(c *check.C) { + cs.cli.Unalias("alias1") + 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("alias1") + 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": "alias1", + "alias": "alias1", + }) +} + +func (cs *clientSuite) TestClientDisableAllAliasesCallsEndpoint(c *check.C) { + cs.cli.DisableAllAliases("some-snap") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientDisableAllAliases(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.DisableAllAliases("some-snap") + 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": "some-snap", + }) +} + +func (cs *clientSuite) TestClientRemoveManualAliasCallsEndpoint(c *check.C) { + cs.cli.RemoveManualAlias("alias1") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientRemoveManualAlias(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.RemoveManualAlias("alias1") + 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", + "alias": "alias1", + }) +} + +func (cs *clientSuite) TestClientPreferCallsEndpoint(c *check.C) { + cs.cli.Prefer("some-snap") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientPrefer(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.Prefer("some-snap") + 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": "prefer", + "snap": "some-snap", + }) +} + +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": {"command": "foo", "status": "auto", "auto": "foo"}, + "foo_reset": {"command": "foo.reset", "manual": "reset", "status": "manual"} + }, + "bar": { + "bar_dump": {"command": "bar.dump", "status": "manual", "manual": "dump"}, + "bar_dump.1": {"command": "bar.dump", "status": "disabled", "auto": "dump"} + } + } + }` + allStatuses, err := cs.cli.Aliases() + c.Assert(err, check.IsNil) + c.Check(allStatuses, check.DeepEquals, map[string]map[string]client.AliasStatus{ + "foo": { + "foo0": {Command: "foo", Status: "auto", Auto: "foo"}, + "foo_reset": {Command: "foo.reset", Status: "manual", Manual: "reset"}, + }, + "bar": { + "bar_dump": {Command: "bar.dump", Status: "manual", Manual: "dump"}, + "bar_dump.1": {Command: "bar.dump", Status: "disabled", Auto: "dump"}, + }, + }) +} diff --git a/client/apps.go b/client/apps.go new file mode 100644 index 00000000..b85790ae --- /dev/null +++ b/client/apps.go @@ -0,0 +1,263 @@ +// -*- 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 ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "net/url" + "strconv" + "strings" + "time" +) + +// AppActivator is a thing that activates the app that is a service in the +// system. +type AppActivator struct { + Name string + // Type describes the type of the unit, either timer or socket + Type string + Active bool + Enabled bool +} + +// AppInfo describes a single snap application. +type AppInfo struct { + Snap string `json:"snap,omitempty"` + Name string `json:"name"` + DesktopFile string `json:"desktop-file,omitempty"` + Daemon string `json:"daemon,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Active bool `json:"active,omitempty"` + CommonID string `json:"common-id,omitempty"` + Activators []AppActivator `json:"activators,omitempty"` +} + +// IsService returns true if the application is a background daemon. +func (a *AppInfo) IsService() bool { + if a == nil { + return false + } + if a.Daemon == "" { + return false + } + + return true +} + +// AppOptions represent the options of the Apps call. +type AppOptions struct { + // If Service is true, only return apps that are services + // (app.IsService() is true); otherwise, return all. + Service bool +} + +// Apps returns information about all matching apps. Each name can be +// either a snap or a snap.app. If names is empty, list all (that +// satisfy opts). +func (client *Client) Apps(names []string, opts AppOptions) ([]*AppInfo, error) { + q := make(url.Values) + if len(names) > 0 { + q.Add("names", strings.Join(names, ",")) + } + if opts.Service { + q.Add("select", "service") + } + + var appInfos []*AppInfo + _, err := client.doSync("GET", "/v2/apps", q, nil, nil, &appInfos) + + return appInfos, err +} + +// LogOptions represent the options of the Logs call. +type LogOptions struct { + N int // The maximum number of log lines to retrieve initially. If <0, no limit. + Follow bool // Whether to continue returning new lines as they appear +} + +// A Log holds the information of a single syslog entry +type Log struct { + Timestamp time.Time `json:"timestamp"` // Timestamp of the event, in RFC3339 format to µs precision. + Message string `json:"message"` // The log message itself + SID string `json:"sid"` // The syslog identifier + PID string `json:"pid"` // The process identifier +} + +func (l Log) String() string { + return fmt.Sprintf("%s %s[%s]: %s", l.Timestamp.Format(time.RFC3339), l.SID, l.PID, l.Message) +} + +// Logs asks for the logs of a series of services, by name. +func (client *Client) Logs(names []string, opts LogOptions) (<-chan Log, error) { + query := url.Values{} + if len(names) > 0 { + query.Set("names", strings.Join(names, ",")) + } + query.Set("n", strconv.Itoa(opts.N)) + if opts.Follow { + query.Set("follow", strconv.FormatBool(opts.Follow)) + } + + rsp, err := client.raw("GET", "/v2/logs", query, nil, nil) + if err != nil { + return nil, err + } + + if rsp.StatusCode != 200 { + var r response + defer rsp.Body.Close() + if err := decodeInto(rsp.Body, &r); err != nil { + return nil, err + } + return nil, r.err(client) + } + + ch := make(chan Log, 20) + go func() { + // logs come in application/json-seq, described in RFC7464: it's + // a series of . Decoders are + // expected to skip invalid or truncated or empty records. + scanner := bufio.NewScanner(rsp.Body) + for scanner.Scan() { + buf := scanner.Bytes() // the scanner prunes the ending LF + if len(buf) < 1 { + // truncated record? skip + continue + } + idx := bytes.IndexByte(buf, 0x1E) // find the initial RS + if idx < 0 { + // no RS? skip + continue + } + buf = buf[idx+1:] // drop the initial RS + var log Log + if err := json.Unmarshal(buf, &log); err != nil { + // truncated/corrupted/binary record? skip + continue + } + ch <- log + } + close(ch) + rsp.Body.Close() + }() + + return ch, nil +} + +// ErrNoNames is returned by Start, Stop, or Restart, when the given +// list of things on which to operate is empty. +var ErrNoNames = errors.New(`"names" must not be empty`) + +type appInstruction struct { + Action string `json:"action"` + Names []string `json:"names"` + StartOptions + StopOptions + RestartOptions +} + +// StartOptions represent the different options of the Start call. +type StartOptions struct { + // Enable, as well as starting, the listed services. A + // disabled service does not start on boot. + Enable bool `json:"enable,omitempty"` +} + +// Start services. +// +// It takes a list of names that can be snaps, of which all their +// services are started, or snap.service which are individual +// services to start; it shouldn't be empty. +func (client *Client) Start(names []string, opts StartOptions) (changeID string, err error) { + if len(names) == 0 { + return "", ErrNoNames + } + + buf, err := json.Marshal(appInstruction{ + Action: "start", + Names: names, + StartOptions: opts, + }) + if err != nil { + return "", err + } + return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf)) +} + +// StopOptions represent the different options of the Stop call. +type StopOptions struct { + // Disable, as well as stopping, the listed services. A + // service that is not disabled starts on boot. + Disable bool `json:"disable,omitempty"` +} + +// Stop services. +// +// It takes a list of names that can be snaps, of which all their +// services are stopped, or snap.service which are individual +// services to stop; it shouldn't be empty. +func (client *Client) Stop(names []string, opts StopOptions) (changeID string, err error) { + if len(names) == 0 { + return "", ErrNoNames + } + + buf, err := json.Marshal(appInstruction{ + Action: "stop", + Names: names, + StopOptions: opts, + }) + if err != nil { + return "", err + } + return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf)) +} + +// RestartOptions represent the different options of the Restart call. +type RestartOptions struct { + // Reload the services, if possible (i.e. if the App has a + // ReloadCommand, invoque it), instead of restarting. + Reload bool `json:"reload,omitempty"` +} + +// Restart services. +// +// It takes a list of names that can be snaps, of which all their +// services are restarted, or snap.service which are individual +// services to restart; it shouldn't be empty. If the service is not +// running, starts it. +func (client *Client) Restart(names []string, opts RestartOptions) (changeID string, err error) { + if len(names) == 0 { + return "", ErrNoNames + } + + buf, err := json.Marshal(appInstruction{ + Action: "restart", + Names: names, + RestartOptions: opts, + }) + if err != nil { + return "", err + } + return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf)) +} diff --git a/client/apps_test.go b/client/apps_test.go new file mode 100644 index 00000000..d3ae8b1c --- /dev/null +++ b/client/apps_test.go @@ -0,0 +1,396 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package client_test + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func mksvc(snap, app string) *client.AppInfo { + return &client.AppInfo{ + Snap: snap, + Name: app, + Daemon: "simple", + Active: true, + Enabled: true, + } + +} + +func testClientApps(cs *clientSuite, c *check.C) ([]*client.AppInfo, error) { + services, err := cs.cli.Apps([]string{"foo", "bar"}, client.AppOptions{}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/apps") + c.Check(cs.req.Method, check.Equals, "GET") + query := cs.req.URL.Query() + c.Check(query, check.HasLen, 1) + c.Check(query.Get("names"), check.Equals, "foo,bar") + + return services, err +} + +func testClientAppsService(cs *clientSuite, c *check.C) ([]*client.AppInfo, error) { + services, err := cs.cli.Apps([]string{"foo", "bar"}, client.AppOptions{Service: true}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/apps") + c.Check(cs.req.Method, check.Equals, "GET") + query := cs.req.URL.Query() + c.Check(query, check.HasLen, 2) + c.Check(query.Get("names"), check.Equals, "foo,bar") + c.Check(query.Get("select"), check.Equals, "service") + + return services, err +} + +var appcheckers = []func(*clientSuite, *check.C) ([]*client.AppInfo, error){testClientApps, testClientAppsService} + +func (cs *clientSuite) TestClientServiceGetHappy(c *check.C) { + expected := []*client.AppInfo{mksvc("foo", "foo"), mksvc("bar", "bar1")} + buf, err := json.Marshal(expected) + c.Assert(err, check.IsNil) + cs.rsp = fmt.Sprintf(`{"type": "sync", "result": %s}`, buf) + for _, chkr := range appcheckers { + actual, err := chkr(cs, c) + c.Assert(err, check.IsNil) + c.Check(actual, check.DeepEquals, expected) + } +} + +func (cs *clientSuite) TestClientServiceGetSad(c *check.C) { + cs.err = fmt.Errorf("xyzzy") + for _, chkr := range appcheckers { + actual, err := chkr(cs, c) + c.Assert(err, check.ErrorMatches, ".* xyzzy") + c.Check(actual, check.HasLen, 0) + } +} + +func (cs *clientSuite) TestClientAppCommonID(c *check.C) { + expected := []*client.AppInfo{{ + Snap: "foo", + Name: "foo", + CommonID: "org.foo", + }} + buf, err := json.Marshal(expected) + c.Assert(err, check.IsNil) + cs.rsp = fmt.Sprintf(`{"type": "sync", "result": %s}`, buf) + for _, chkr := range appcheckers { + actual, err := chkr(cs, c) + c.Assert(err, check.IsNil) + c.Check(actual, check.DeepEquals, expected) + } +} + +func testClientLogs(cs *clientSuite, c *check.C) ([]client.Log, error) { + ch, err := cs.cli.Logs([]string{"foo", "bar"}, client.LogOptions{N: -1, Follow: false}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/logs") + c.Check(cs.req.Method, check.Equals, "GET") + query := cs.req.URL.Query() + c.Check(query, check.HasLen, 2) + c.Check(query.Get("names"), check.Equals, "foo,bar") + c.Check(query.Get("n"), check.Equals, "-1") + + var logs []client.Log + if ch != nil { + for log := range ch { + logs = append(logs, log) + } + } + + return logs, err +} + +func (cs *clientSuite) TestClientLogsHappy(c *check.C) { + cs.rsp = ` +{"message":"hello"} +{"message":"bye"} +`[1:] // remove the first \n + + logs, err := testClientLogs(cs, c) + c.Assert(err, check.IsNil) + c.Check(logs, check.DeepEquals, []client.Log{{Message: "hello"}, {Message: "bye"}}) +} + +func (cs *clientSuite) TestClientLogsDealsWithIt(c *check.C) { + cs.rsp = `this is a line with no RS on it +this is a line with a RS after some junk{"message": "hello"} +{"message": "bye"} +and that was a regular line. The next one is empty, despite having a RS (and the one after is entirely empty): + + +` + logs, err := testClientLogs(cs, c) + c.Assert(err, check.IsNil) + c.Check(logs, check.DeepEquals, []client.Log{{Message: "hello"}, {Message: "bye"}}) +} + +func (cs *clientSuite) TestClientLogsSad(c *check.C) { + cs.err = fmt.Errorf("xyzzy") + actual, err := testClientLogs(cs, c) + c.Assert(err, check.ErrorMatches, ".* xyzzy") + c.Check(actual, check.HasLen, 0) +} + +func (cs *clientSuite) TestClientLogsOpts(c *check.C) { + const ( + maxint = int((^uint(0)) >> 1) + minint = -maxint - 1 + ) + for _, names := range [][]string{nil, {}, {"foo"}, {"foo", "bar"}} { + for _, n := range []int{-1, 0, 1, minint, maxint} { + for _, follow := range []bool{true, false} { + iterdesc := check.Commentf("names: %v, n: %v, follow: %v", names, n, follow) + + ch, err := cs.cli.Logs(names, client.LogOptions{N: n, Follow: follow}) + c.Check(err, check.IsNil, iterdesc) + c.Check(cs.req.URL.Path, check.Equals, "/v2/logs", iterdesc) + c.Check(cs.req.Method, check.Equals, "GET", iterdesc) + query := cs.req.URL.Query() + numQ := 0 + + var namesout []string + if ns := query.Get("names"); ns != "" { + namesout = strings.Split(ns, ",") + } + + c.Check(len(namesout), check.Equals, len(names), iterdesc) + if len(names) != 0 { + c.Check(namesout, check.DeepEquals, names, iterdesc) + numQ++ + } + + nout, nerr := strconv.Atoi(query.Get("n")) + c.Check(nerr, check.IsNil, iterdesc) + c.Check(nout, check.Equals, n, iterdesc) + numQ++ + + if follow { + fout, ferr := strconv.ParseBool(query.Get("follow")) + c.Check(fout, check.Equals, true, iterdesc) + c.Check(ferr, check.IsNil, iterdesc) + numQ++ + } + + c.Check(query, check.HasLen, numQ, iterdesc) + + for x := range ch { + c.Logf("expecting empty channel, got %v during %s", x, iterdesc) + c.Fail() + } + } + } + } +} + +func (cs *clientSuite) TestClientLogsNotFound(c *check.C) { + cs.rsp = `{"type":"error","status-code":404,"status":"Not Found","result":{"message":"snap \"foo\" not found","kind":"snap-not-found","value":"foo"}}` + cs.status = 404 + actual, err := testClientLogs(cs, c) + c.Assert(err, check.ErrorMatches, `snap "foo" not found`) + c.Check(actual, check.HasLen, 0) +} + +func (cs *clientSuite) TestClientServiceStart(c *check.C) { + cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}` + + type scenario struct { + names []string + opts client.StartOptions + comment check.CommentInterface + } + + var scenarios []scenario + + for _, names := range [][]string{ + nil, {}, + {"foo"}, + {"foo", "bar", "baz"}, + } { + for _, opts := range []client.StartOptions{ + {Enable: true}, + {Enable: false}, + } { + scenarios = append(scenarios, scenario{ + names: names, + opts: opts, + comment: check.Commentf("{%q; %#v}", names, opts), + }) + } + } + + for _, sc := range scenarios { + id, err := cs.cli.Start(sc.names, sc.opts) + if len(sc.names) == 0 { + c.Check(id, check.Equals, "", sc.comment) + c.Check(err, check.Equals, client.ErrNoNames, sc.comment) + c.Check(cs.req, check.IsNil, sc.comment) // i.e. the request was never done + } else { + c.Assert(err, check.IsNil, sc.comment) + c.Check(id, check.Equals, "24", sc.comment) + c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", sc.comment) + c.Check(cs.req.Method, check.Equals, "POST", sc.comment) + c.Check(cs.req.URL.Query(), check.HasLen, 0, sc.comment) + + inames := make([]interface{}, len(sc.names)) + for i, name := range sc.names { + inames[i] = interface{}(name) + } + + var reqOp map[string]interface{} + c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, sc.comment) + if sc.opts.Enable { + c.Check(len(reqOp), check.Equals, 3, sc.comment) + c.Check(reqOp["enable"], check.Equals, true, sc.comment) + } else { + c.Check(len(reqOp), check.Equals, 2, sc.comment) + c.Check(reqOp["enable"], check.IsNil, sc.comment) + } + c.Check(reqOp["action"], check.Equals, "start", sc.comment) + c.Check(reqOp["names"], check.DeepEquals, inames, sc.comment) + } + } +} + +func (cs *clientSuite) TestClientServiceStop(c *check.C) { + cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}` + + type tT struct { + names []string + opts client.StopOptions + comment check.CommentInterface + } + + var scs []tT + + for _, names := range [][]string{ + nil, {}, + {"foo"}, + {"foo", "bar", "baz"}, + } { + for _, opts := range []client.StopOptions{ + {Disable: true}, + {Disable: false}, + } { + scs = append(scs, tT{ + names: names, + opts: opts, + comment: check.Commentf("{%q; %#v}", names, opts), + }) + } + } + + for _, sc := range scs { + id, err := cs.cli.Stop(sc.names, sc.opts) + if len(sc.names) == 0 { + c.Check(id, check.Equals, "", sc.comment) + c.Check(err, check.Equals, client.ErrNoNames, sc.comment) + c.Check(cs.req, check.IsNil, sc.comment) // i.e. the request was never done + } else { + c.Assert(err, check.IsNil, sc.comment) + c.Check(id, check.Equals, "24", sc.comment) + c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", sc.comment) + c.Check(cs.req.Method, check.Equals, "POST", sc.comment) + c.Check(cs.req.URL.Query(), check.HasLen, 0, sc.comment) + + inames := make([]interface{}, len(sc.names)) + for i, name := range sc.names { + inames[i] = interface{}(name) + } + + var reqOp map[string]interface{} + c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, sc.comment) + if sc.opts.Disable { + c.Check(len(reqOp), check.Equals, 3, sc.comment) + c.Check(reqOp["disable"], check.Equals, true, sc.comment) + } else { + c.Check(len(reqOp), check.Equals, 2, sc.comment) + c.Check(reqOp["disable"], check.IsNil, sc.comment) + } + c.Check(reqOp["action"], check.Equals, "stop", sc.comment) + c.Check(reqOp["names"], check.DeepEquals, inames, sc.comment) + } + } +} + +func (cs *clientSuite) TestClientServiceRestart(c *check.C) { + cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}` + + type tT struct { + names []string + opts client.RestartOptions + comment check.CommentInterface + } + + var scs []tT + + for _, names := range [][]string{ + nil, {}, + {"foo"}, + {"foo", "bar", "baz"}, + } { + for _, opts := range []client.RestartOptions{ + {Reload: true}, + {Reload: false}, + } { + scs = append(scs, tT{ + names: names, + opts: opts, + comment: check.Commentf("{%q; %#v}", names, opts), + }) + } + } + + for _, sc := range scs { + id, err := cs.cli.Restart(sc.names, sc.opts) + if len(sc.names) == 0 { + c.Check(id, check.Equals, "", sc.comment) + c.Check(err, check.Equals, client.ErrNoNames, sc.comment) + c.Check(cs.req, check.IsNil, sc.comment) // i.e. the request was never done + } else { + c.Assert(err, check.IsNil, sc.comment) + c.Check(id, check.Equals, "24", sc.comment) + c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", sc.comment) + c.Check(cs.req.Method, check.Equals, "POST", sc.comment) + c.Check(cs.req.URL.Query(), check.HasLen, 0, sc.comment) + + inames := make([]interface{}, len(sc.names)) + for i, name := range sc.names { + inames[i] = interface{}(name) + } + + var reqOp map[string]interface{} + c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, sc.comment) + if sc.opts.Reload { + c.Check(len(reqOp), check.Equals, 3, sc.comment) + c.Check(reqOp["reload"], check.Equals, true, sc.comment) + } else { + c.Check(len(reqOp), check.Equals, 2, sc.comment) + c.Check(reqOp["reload"], check.IsNil, sc.comment) + } + c.Check(reqOp["action"], check.Equals, "restart", sc.comment) + c.Check(reqOp["names"], check.DeepEquals, inames, sc.comment) + } + } +} diff --git a/client/asserts.go b/client/asserts.go new file mode 100644 index 00000000..73497ab5 --- /dev/null +++ b/client/asserts.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 client + +import ( + "bytes" + "fmt" + "io" + "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 +} + +// AssertionTypes returns a list of assertion type names. +func (client *Client) AssertionTypes() ([]string, error) { + var types struct { + Types []string `json:"types"` + } + _, err := client.doSync("GET", "/v2/assertions", nil, nil, nil, &types) + if err != nil { + return nil, fmt.Errorf("cannot get assertion type names: %v", err) + } + + return types.Types, 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 != 200 { + 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..7732d680 --- /dev/null +++ b/client/asserts_test.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 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) TestClientAssertsTypes(c *C) { + cs.rsp = `{ + "result": { + "types": ["one", "two"] + }, + "status": "OK", + "status-code": 200, + "type": "sync" +}` + typs, err := cs.cli.AssertionTypes() + c.Assert(err, IsNil) + c.Check(typs, DeepEquals, []string{"one", "two"}) +} + +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 = 400 + 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 = 200 + 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 = 200 + _, 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..3a410d6b --- /dev/null +++ b/client/buy.go @@ -0,0 +1,63 @@ +// -*- 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" +) + +// BuyOptions specifies parameters to buy from the store. +type BuyOptions struct { + SnapID string `json:"snap-id"` + Price float64 `json:"price"` + Currency string `json:"currency"` // ISO 4217 code as string +} + +// BuyResult holds the state of a buy attempt. +type BuyResult struct { + State string `json:"state,omitempty"` +} + +func (client *Client) Buy(opts *BuyOptions) (*BuyResult, error) { + if opts == nil { + opts = &BuyOptions{} + } + + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(opts); err != nil { + return nil, err + } + + var result 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..e0a35b7c --- /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..1cba584a --- /dev/null +++ b/client/change_test.go @@ -0,0 +1,233 @@ +// -*- 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) TestClientChangeRestartingState(c *check.C) { + cs.rsp = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false +}, + "maintenance": {"kind": "system-restart", "message": "system is restarting"} +}` + + chg, err := cs.cli.Change("uno") + c.Check(chg, check.NotNil) + c.Check(chg.ID, check.Equals, "uno") + c.Check(err, check.IsNil) + c.Check(cs.cli.Maintenance(), check.ErrorMatches, `system is restarting`) +} + +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..84459dbc --- /dev/null +++ b/client/client.go @@ -0,0 +1,686 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2018 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" + "time" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/jsonutil" +) + +func unixDialer(socketPath string) func(string, string) (net.Conn, error) { + if socketPath == "" { + socketPath = dirs.SnapdSocket + } + 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 + + // DisableAuth controls whether the client should send an + // Authorization header from reading the auth.json data. + DisableAuth bool + + // Interactive controls whether the client runs in interactive mode. + // At present, this only affects whether interactive polkit + // authorisation is requested. + Interactive bool + + // Socket is the path to the unix socket to use + Socket string + + // DisableKeepAlive indicates whether the connections should not be kept + // alive for later reuse + DisableKeepAlive bool +} + +// A Client knows how to talk to the snappy daemon. +type Client struct { + baseURL url.URL + doer doer + + disableAuth bool + interactive bool + + maintenance error + + warningCount int + warningTimestamp time.Time +} + +// New returns a new instance of Client +func New(config *Config) *Client { + if config == nil { + config = &Config{} + } + + // By default talk over an UNIX socket. + if config.BaseURL == "" { + transport := &http.Transport{Dial: unixDialer(config.Socket), DisableKeepAlives: config.DisableKeepAlive} + return &Client{ + baseURL: url.URL{ + Scheme: "http", + Host: "localhost", + }, + doer: &http.Client{Transport: transport}, + disableAuth: config.DisableAuth, + interactive: config.Interactive, + } + } + + 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{Transport: &http.Transport{DisableKeepAlives: config.DisableKeepAlive}}, + disableAuth: config.DisableAuth, + interactive: config.Interactive, + } +} + +// Maintenance returns an error reflecting the daemon maintenance status or nil. +func (client *Client) Maintenance() error { + return client.maintenance +} + +// WarningsSummary returns the number of warnings that are ready to be shown to +// the user, and the timestamp of the most recently added warning (useful for +// silencing the warning alerts, and OKing the returned warnings). +func (client *Client) WarningsSummary() (count int, timestamp time.Time) { + return client.warningCount, client.warningTimestamp +} + +func (client *Client) WhoAmI() (string, error) { + user, err := readAuthData() + if os.IsNotExist(err) { + return "", nil + } + if err != nil { + return "", err + } + + return user.Email, nil +} + +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) +} + +// AllowInteractionHeader is the HTTP request header used to indicate +// that the client is willing to allow interaction. +const AllowInteractionHeader = "X-Allow-Interaction" + +// 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) + } + + if !client.disableAuth { + // set Authorization header if there are user's credentials + err = client.setAuthorization(req) + if err != nil { + return nil, AuthorizationError{err} + } + } + + if client.interactive { + req.Header.Set(AllowInteractionHeader, "true") + } + + 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 + } +} + +type hijacked struct { + do func(*http.Request) (*http.Response, error) +} + +func (h hijacked) Do(req *http.Request) (*http.Response, error) { + return h.do(req) +} + +// Hijack lets the caller take over the raw http request +func (client *Client) Hijack(f func(*http.Request) (*http.Response, error)) { + client.doer = hijacked{f} +} + +// 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 { + if err := decodeInto(rsp.Body, v); err != nil { + return err + } + } + + return nil +} + +func decodeInto(reader io.Reader, v interface{}) error { + dec := json.NewDecoder(reader) + 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 using the "UseNumber" json decoding +// which produces json.Numbers instead of float64 types for numbers. +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(client); 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 := jsonutil.DecodeWithNumber(bytes.NewReader(rsp.Result), v); err != nil { + return nil, fmt.Errorf("cannot unmarshal: %v", err) + } + } + + client.warningCount = rsp.WarningCount + client.warningTimestamp = rsp.WarningTimestamp + + 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) { + _, changeID, err = client.doAsyncFull(method, path, query, headers, body) + return +} + +func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader) (result json.RawMessage, changeID string, err error) { + var rsp response + + if err := client.do(method, path, query, headers, body, &rsp); err != nil { + return nil, "", err + } + if err := rsp.err(client); err != nil { + return nil, "", err + } + if rsp.Type != "async" { + return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type) + } + if rsp.StatusCode != 202 { + return nil, "", fmt.Errorf("operation not accepted") + } + if rsp.Change == "" { + return nil, "", fmt.Errorf("async response without change reference") + } + + return rsp.Result, rsp.Change, nil +} + +type ServerVersion struct { + Version string + Series string + OSID string + OSVersionID string + OnClassic bool + + KernelVersion string +} + +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, + + KernelVersion: sysInfo.KernelVersion, + }, 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"` + + WarningCount int `json:"warning-count"` + WarningTimestamp time.Time `json:"warning-timestamp"` + + ResultInfo + + Maintenance *Error `json:"maintenance"` +} + +// Error is the real value of response.Result when an error occurs. +type Error struct { + Kind string `json:"kind"` + Value interface{} `json:"value"` + 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" + ErrorKindInvalidAuthData = "invalid-auth-data" + ErrorKindTermsNotAccepted = "terms-not-accepted" + ErrorKindNoPaymentMethods = "no-payment-methods" + ErrorKindPaymentDeclined = "payment-declined" + ErrorKindPasswordPolicy = "password-policy" + + ErrorKindSnapAlreadyInstalled = "snap-already-installed" + ErrorKindSnapNotInstalled = "snap-not-installed" + ErrorKindSnapNotFound = "snap-not-found" + ErrorKindAppNotFound = "app-not-found" + ErrorKindSnapLocal = "snap-local" + ErrorKindSnapNeedsDevMode = "snap-needs-devmode" + ErrorKindSnapNeedsClassic = "snap-needs-classic" + ErrorKindSnapNeedsClassicSystem = "snap-needs-classic-system" + ErrorKindSnapNotClassic = "snap-not-classic" + ErrorKindNoUpdateAvailable = "snap-no-update-available" + + ErrorKindRevisionNotAvailable = "snap-revision-not-available" + ErrorKindChannelNotAvailable = "snap-channel-not-available" + ErrorKindArchitectureNotAvailable = "snap-architecture-not-available" + + ErrorKindChangeConflict = "snap-change-conflict" + + ErrorKindNotSnap = "snap-not-a-snap" + + ErrorKindNetworkTimeout = "network-timeout" + ErrorKindDNSFailure = "dns-failure" + + ErrorKindInterfacesUnchanged = "interfaces-unchanged" + + ErrorKindBadQuery = "bad-query" + ErrorKindConfigNoSuchOption = "option-not-found" + + ErrorKindSystemRestart = "system-restart" + ErrorKindDaemonRestart = "daemon-restart" +) + +// IsRetryable returns true if the given error is an error +// that can be retried later. +func IsRetryable(err error) bool { + switch e := err.(type) { + case *Error: + return e.Kind == ErrorKindChangeConflict + } + return false +} + +// 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 +} + +// IsInterfacesUnchangedError returns whether the given error means the requested +// change to interfaces was not made, because there was nothing to do. +func IsInterfacesUnchangedError(err error) bool { + e, ok := err.(*Error) + if !ok || e == nil { + return false + } + return e.Kind == ErrorKindInterfacesUnchanged +} + +// OSRelease contains information about the system extracted from /etc/os-release. +type OSRelease struct { + ID string `json:"id"` + VersionID string `json:"version-id,omitempty"` +} + +// RefreshInfo contains information about refreshes. +type RefreshInfo struct { + // Timer contains the refresh.timer setting. + Timer string `json:"timer,omitempty"` + // Schedule contains the legacy refresh.schedule setting. + Schedule string `json:"schedule,omitempty"` + Last string `json:"last,omitempty"` + Hold string `json:"hold,omitempty"` + Next string `json:"next,omitempty"` +} + +// SysInfo holds system information +type SysInfo struct { + Series string `json:"series,omitempty"` + Version string `json:"version,omitempty"` + BuildID string `json:"build-id"` + OSRelease OSRelease `json:"os-release"` + OnClassic bool `json:"on-classic"` + Managed bool `json:"managed"` + + KernelVersion string `json:"kernel-version,omitempty"` + + Refresh RefreshInfo `json:"refresh,omitempty"` + Confinement string `json:"confinement"` + SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"` +} + +func (rsp *response) err(cli *Client) error { + if cli != nil { + maintErr := rsp.Maintenance + // avoid setting to (*client.Error)(nil) + if maintErr != nil { + cli.maintenance = maintErr + } else { + cli.maintenance = nil + } + } + 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(nil) + 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 +} + +type debugAction struct { + Action string `json:"action"` + Params interface{} `json:"params,omitempty"` +} + +// Debug is only useful when writing test code, it will trigger +// an internal action with the given parameters. +func (client *Client) Debug(action string, params interface{}, result interface{}) error { + body, err := json.Marshal(debugAction{ + Action: action, + Params: params, + }) + if err != nil { + return err + } + + _, err = client.doSync("POST", "/v2/debug", nil, nil, bytes.NewReader(body), result) + return err +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 00000000..25642790 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,560 @@ +// -*- 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 + restore func() +} + +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 = 200 + cs.doCalls = 0 + + dirs.SetRootDir(c.MkDir()) + + cs.restore = client.MockDoRetry(time.Millisecond, 10*time.Millisecond) +} + +func (cs *clientSuite) TearDownTest(c *C) { + os.Unsetenv(client.TestAuthFileEnvKey) + cs.restore() +} + +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) { + 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) TestClientHonorsDisableAuth(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 + cli := client.New(&client.Config{DisableAuth: true}) + cli.SetDoer(cs) + _ = cli.Do("GET", "/this", nil, nil, &v) + authorization := cs.req.Header.Get("Authorization") + c.Check(authorization, Equals, "") +} + +func (cs *clientSuite) TestClientHonorsInteractive(c *C) { + var v string + cli := client.New(&client.Config{Interactive: false}) + cli.SetDoer(cs) + _ = cli.Do("GET", "/this", nil, nil, &v) + interactive := cs.req.Header.Get(client.AllowInteractionHeader) + c.Check(interactive, Equals, "") + + cli = client.New(&client.Config{Interactive: true}) + cli.SetDoer(cs) + _ = cli.Do("GET", "/this", nil, nil, &v) + interactive = cs.req.Header.Get(client.AllowInteractionHeader) + c.Check(interactive, Equals, "true") +} + +func (cs *clientSuite) TestClientWhoAmINobody(c *C) { + email, err := cs.cli.WhoAmI() + c.Assert(err, IsNil) + c.Check(email, Equals, "") +} + +func (cs *clientSuite) TestClientWhoAmIRubbish(c *C) { + c.Assert(ioutil.WriteFile(client.TestStoreAuthFilename(os.Getenv("HOME")), []byte("rubbish"), 0644), IsNil) + + email, err := cs.cli.WhoAmI() + c.Check(err, NotNil) + c.Check(email, Equals, "") +} + +func (cs *clientSuite) TestClientWhoAmISomebody(c *C) { + mockUserData := client.User{ + Email: "foo@example.com", + } + c.Assert(client.TestWriteAuth(mockUserData), IsNil) + + email, err := cs.cli.WhoAmI() + c.Check(err, IsNil) + c.Check(email, Equals, "foo@example.com") +} + +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, + "build-id": "1234", + "confinement": "strict", + "sandbox-features": {"backend": ["feature-1", "feature-2"]}}}` + 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, + Confinement: "strict", + SandboxFeatures: map[string][]string{ + "backend": {"feature-1", "feature-2"}, + }, + BuildID: "1234", + }) +} + +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(&client.Config{ + Socket: dirs.SnapSocket, + }) + 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) TestClientMaintenance(c *C) { + cs.rsp = `{"type":"sync", "result":{"series":"42"}, "maintenance": {"kind": "system-restart", "message": "system is restarting"}}` + _, err := cs.cli.SysInfo() + c.Assert(err, IsNil) + c.Check(cs.cli.Maintenance().(*client.Error), DeepEquals, &client.Error{ + Kind: client.ErrorKindSystemRestart, + Message: "system is restarting", + }) + + cs.rsp = `{"type":"sync", "result":{"series":"42"}}` + _, err = cs.cli.SysInfo() + c.Assert(err, IsNil) + c.Check(cs.cli.Maintenance(), Equals, error(nil)) +} + +func (cs *clientSuite) TestClientAsyncOpMaintenance(c *C) { + cs.rsp = `{"type":"async", "status-code": 202, "change": "42", "maintenance": {"kind": "system-restart", "message": "system is restarting"}}` + _, err := cs.cli.Install("foo", nil) + c.Assert(err, IsNil) + c.Check(cs.cli.Maintenance().(*client.Error), DeepEquals, &client.Error{ + Kind: client.ErrorKindSystemRestart, + Message: "system is restarting", + }) + + cs.rsp = `{"type":"async", "status-code": 202, "change": "42"}` + _, err = cs.cli.Install("foo", nil) + c.Assert(err, IsNil) + c.Check(cs.cli.Maintenance(), Equals, error(nil)) +} + +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) TestIsRetryable(c *C) { + // unhappy + c.Check(client.IsRetryable(nil), Equals, false) + c.Check(client.IsRetryable(errors.New("some-error")), Equals, false) + c.Check(client.IsRetryable(&client.Error{Kind: "something-else"}), Equals, false) + // happy + c.Check(client.IsRetryable(&client.Error{Kind: client.ErrorKindChangeConflict}), Equals, true) +} + +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"}]}` + users, err := cs.cli.Users() + c.Check(err, IsNil) + c.Check(users, DeepEquals, []*client.User{ + {Username: "foo", Email: "foo@example.com"}, + {Username: "bar", Email: "bar@example.com"}, + }) +} + +func (cs *clientSuite) TestDebugEnsureStateSoon(c *C) { + cs.rsp = `{"type": "sync", "result":true}` + err := cs.cli.Debug("ensure-state-soon", nil, nil) + c.Check(err, IsNil) + c.Check(cs.reqs, HasLen, 1) + c.Check(cs.reqs[0].Method, Equals, "POST") + c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug") + data, err := ioutil.ReadAll(cs.reqs[0].Body) + c.Assert(err, IsNil) + c.Check(data, DeepEquals, []byte(`{"action":"ensure-state-soon"}`)) +} + +func (cs *clientSuite) TestDebugGeneric(c *C) { + cs.rsp = `{"type": "sync", "result":["res1","res2"]}` + + var result []string + err := cs.cli.Debug("do-something", []string{"param1", "param2"}, &result) + c.Check(err, IsNil) + c.Check(result, DeepEquals, []string{"res1", "res2"}) + c.Check(cs.reqs, HasLen, 1) + c.Check(cs.reqs[0].Method, Equals, "POST") + c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug") + data, err := ioutil.ReadAll(cs.reqs[0].Body) + c.Assert(err, IsNil) + c.Check(string(data), DeepEquals, `{"action":"do-something","params":["param1","param2"]}`) +} diff --git a/client/conf.go b/client/conf.go new file mode 100644 index 00000000..e7a67530 --- /dev/null +++ b/client/conf.go @@ -0,0 +1,52 @@ +// -*- 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. +// +// Note that the configuration may include json.Numbers. +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..6a499584 --- /dev/null +++ b/client/conf_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 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) TestClientGetConfBigInt(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {"test-key": 1234567890} + }` + 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": json.Number("1234567890")}) +} + +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..d6b83a6f --- /dev/null +++ b/client/export_test.go @@ -0,0 +1,51 @@ +// -*- 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 ( + "encoding/json" + "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 + +func UnmarshalSnapshotAction(body io.Reader) (act snapshotAction, err error) { + err = json.NewDecoder(body).Decode(&act) + return +} diff --git a/client/icons.go b/client/icons.go new file mode 100644 index 00000000..4eb29220 --- /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" + "regexp" +) + +// Icon represents the icon of an installed snap +type Icon struct { + Filename string + Content []byte +} + +var contentDispositionMatcher = regexp.MustCompile(`attachment; filename=(.+)`).FindStringSubmatch + +// 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 != 200 { + return nil, fmt.Errorf("%s: Not Found", errPrefix) + } + + matches := contentDispositionMatcher(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..2dbffb15 --- /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 = 404 + _, 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..4a79d78f --- /dev/null +++ b/client/interfaces.go @@ -0,0 +1,155 @@ +// -*- 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" +) + +// 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"` +} + +// Connections contains information about all plugs, slots and their connections +type Connections struct { + Plugs []Plug `json:"plugs"` + Slots []Slot `json:"slots"` +} + +// Interface holds information about a given interface and its instances. +type Interface struct { + Name string `json:"name,omitempty"` + Summary string `json:"summary,omitempty"` + DocURL string `json:"doc-url,omitempty"` + Plugs []Plug `json:"plugs,omitempty"` + Slots []Slot `json:"slots,omitempty"` +} + +// 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"` +} + +// Connections returns all plugs, slots and their connections. +func (client *Client) Connections() (Connections, error) { + var conns Connections + _, err := client.doSync("GET", "/v2/interfaces", nil, nil, nil, &conns) + return conns, err +} + +// InterfaceOptions represents opt-in elements include in responses. +type InterfaceOptions struct { + Names []string + Doc bool + Plugs bool + Slots bool + Connected bool +} + +func (client *Client) Interfaces(opts *InterfaceOptions) ([]*Interface, error) { + query := url.Values{} + if opts != nil && len(opts.Names) > 0 { + query.Set("names", strings.Join(opts.Names, ",")) // Return just those specific interfaces. + } + if opts != nil { + if opts.Doc { + query.Set("doc", "true") // Return documentation of each selected interface. + } + if opts.Plugs { + query.Set("plugs", "true") // Return plugs of each selected interface. + } + if opts.Slots { + query.Set("slots", "true") // Return slots of each selected interface. + } + } + // NOTE: Presence of "select" triggers the use of the new response format. + if opts != nil && opts.Connected { + query.Set("select", "connected") // Return just the connected interfaces. + } else { + query.Set("select", "all") // Return all interfaces. + } + var interfaces []*Interface + _, err := client.doSync("GET", "/v2/interfaces", query, nil, nil, &interfaces) + + return interfaces, err +} + +// 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..38b7d275 --- /dev/null +++ b/client/interfaces_test.go @@ -0,0 +1,299 @@ +// -*- 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) TestClientInterfacesOptionEncoding(c *check.C) { + // Choose some options + _, _ = cs.cli.Interfaces(&client.InterfaceOptions{ + Names: []string{"a", "b"}, + Doc: true, + Plugs: true, + Slots: true, + Connected: true, + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") + c.Check(cs.req.URL.RawQuery, check.Equals, + "doc=true&names=a%2Cb&plugs=true&select=connected&slots=true") +} + +func (cs *clientSuite) TestClientInterfacesAll(c *check.C) { + // Ask for a summary of all interfaces. + cs.rsp = `{ + "type": "sync", + "result": [ + {"name": "iface-a", "summary": "the A iface"}, + {"name": "iface-b", "summary": "the B iface"}, + {"name": "iface-c", "summary": "the C iface"} + ] + }` + ifaces, err := cs.cli.Interfaces(nil) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") + // This uses the select=all query option to indicate that new response + // format should be used. The same API endpoint is used by the Interfaces + // and by the Connections functions an the absence or presence of the + // select query option decides what kind of result should be returned + // (legacy or modern). + c.Check(cs.req.URL.RawQuery, check.Equals, "select=all") + c.Assert(err, check.IsNil) + c.Check(ifaces, check.DeepEquals, []*client.Interface{ + {Name: "iface-a", Summary: "the A iface"}, + {Name: "iface-b", Summary: "the B iface"}, + {Name: "iface-c", Summary: "the C iface"}, + }) +} + +func (cs *clientSuite) TestClientInterfacesConnected(c *check.C) { + // Ask for for a summary of connected interfaces. + cs.rsp = `{ + "type": "sync", + "result": [ + {"name": "iface-a", "summary": "the A iface"}, + {"name": "iface-c", "summary": "the C iface"} + ] + }` + ifaces, err := cs.cli.Interfaces(&client.InterfaceOptions{ + Connected: true, + }) + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") + // This uses select=connected to ignore interfaces that just sit on some + // snap but are not connected to anything. + c.Check(cs.req.URL.RawQuery, check.Equals, "select=connected") + c.Assert(err, check.IsNil) + c.Check(ifaces, check.DeepEquals, []*client.Interface{ + {Name: "iface-a", Summary: "the A iface"}, + // interface b was not connected so it doesn't get listed. + {Name: "iface-c", Summary: "the C iface"}, + }) +} + +func (cs *clientSuite) TestClientInterfacesSelectedDetails(c *check.C) { + // Ask for single element and request docs, plugs and slots. + cs.rsp = `{ + "type": "sync", + "result": [ + { + "name": "iface-a", + "summary": "the A iface", + "doc-url": "http://example.org/ifaces/a", + "plugs": [{ + "snap": "consumer", + "plug": "plug", + "interface": "iface-a" + }], + "slots": [{ + "snap": "producer", + "slot": "slot", + "interface": "iface-a" + }] + } + ] + }` + opts := &client.InterfaceOptions{Names: []string{"iface-a"}, Doc: true, Plugs: true, Slots: true} + ifaces, err := cs.cli.Interfaces(opts) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") + // This enables documentation, plugs, slots, chooses a specific interface + // (iface-a), and uses select=all to indicate that new response is desired. + c.Check(cs.req.URL.RawQuery, check.Equals, + "doc=true&names=iface-a&plugs=true&select=all&slots=true") + c.Assert(err, check.IsNil) + c.Check(ifaces, check.DeepEquals, []*client.Interface{ + { + Name: "iface-a", + Summary: "the A iface", + DocURL: "http://example.org/ifaces/a", + Plugs: []client.Plug{{Snap: "consumer", Name: "plug", Interface: "iface-a"}}, + Slots: []client.Slot{{Snap: "producer", Name: "slot", Interface: "iface-a"}}, + }, + }) +} + +func (cs *clientSuite) TestClientInterfacesMultiple(c *check.C) { + // Ask for multiple interfaces. + cs.rsp = `{ + "type": "sync", + "result": [ + {"name": "iface-a", "summary": "the A iface"}, + {"name": "iface-b", "summary": "the B iface"} + ] + }` + ifaces, err := cs.cli.Interfaces(&client.InterfaceOptions{Names: []string{"iface-a", "iface-b"}}) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") + // This chooses a specific interfaces (iface-a, iface-b) + c.Check(cs.req.URL.RawQuery, check.Equals, "names=iface-a%2Ciface-b&select=all") + c.Assert(err, check.IsNil) + c.Check(ifaces, check.DeepEquals, []*client.Interface{ + {Name: "iface-a", Summary: "the A iface"}, + {Name: "iface-b", Summary: "the B iface"}, + }) +} + +func (cs *clientSuite) TestClientConnectionsCallsEndpoint(c *check.C) { + _, _ = cs.cli.Connections() + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") +} + +func (cs *clientSuite) TestClientConnections(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"} + ] + } + ] + } + }` + conns, err := cs.cli.Connections() + c.Assert(err, check.IsNil) + c.Check(conns, check.DeepEquals, client.Connections{ + 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..97b982cd --- /dev/null +++ b/client/login.go @@ -0,0 +1,155 @@ +// -*- 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" + + "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 = "SNAPD_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, gid, err := osutil.UidGid(real) + 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..dafbe518 --- /dev/null +++ b/client/login_test.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_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/testutil" +) + +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) + c.Check(outfile, testutil.FileEquals, `{"username":"the-user-name","macaroon":"the-root-macaroon","discharges":["discharge-macaroon"]}`) +} + +func (cs *clientSuite) TestClientLoginWhenLoggedIn(c *check.C) { + cs.rsp = `{"type": "sync", "result": + {"username": "the-user-name", + "email": "zed@bar.com", + "macaroon": "the-root-macaroon", + "discharges": ["discharge-macaroon"]}}` + + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + err := ioutil.WriteFile(outfile, []byte(`{"email":"foo@bar.com","macaroon":"macaroon"}`), 0600) + c.Assert(err, check.IsNil) + c.Assert(cs.cli.LoggedInUser(), check.DeepEquals, &client.User{ + Email: "foo@bar.com", + Macaroon: "macaroon", + }) + + user, err := cs.cli.Login("username", "pass", "") + expected := &client.User{ + Email: "zed@bar.com", + Username: "the-user-name", + Macaroon: "the-root-macaroon", + Discharges: []string{"discharge-macaroon"}, + } + c.Check(err, check.IsNil) + c.Check(user, check.DeepEquals, expected) + c.Check(cs.req.Header.Get("Authorization"), check.Matches, `Macaroon root="macaroon"`) + + c.Assert(cs.cli.LoggedInUser(), check.DeepEquals, expected) + + c.Check(osutil.FileExists(outfile), check.Equals, true) + c.Check(outfile, testutil.FileEquals, `{"username":"the-user-name","email":"zed@bar.com","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) + c.Check(outfile, testutil.FileEquals, `{"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..94b81f56 --- /dev/null +++ b/client/packages.go @@ -0,0 +1,240 @@ +// -*- 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 ( + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + "time" + + "github.com/snapcore/snapd/snap" +) + +// Snap holds the data for a snap as obtained from snapd. +type Snap struct { + ID string `json:"id"` + Title string `json:"title,omitempty"` + Summary string `json:"summary"` + Description string `json:"description"` + DownloadSize int64 `json:"download-size,omitempty"` + Icon string `json:"icon,omitempty"` + InstalledSize int64 `json:"installed-size,omitempty"` + InstallDate time.Time `json:"install-date,omitempty"` + Name string `json:"name"` + Publisher *snap.StoreAccount `json:"publisher,omitempty"` + // Developer is also the publisher's username for historic reasons. + Developer string `json:"developer"` + Status string `json:"status"` + Type string `json:"type"` + Base string `json:"base,omitempty"` + Version string `json:"version"` + Channel string `json:"channel"` + TrackingChannel string `json:"tracking-channel,omitempty"` + IgnoreValidation bool `json:"ignore-validation"` + 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,omitempty"` + Apps []AppInfo `json:"apps,omitempty"` + Broken string `json:"broken,omitempty"` + Contact string `json:"contact"` + License string `json:"license,omitempty"` + CommonIDs []string `json:"common-ids,omitempty"` + MountedFrom string `json:"mounted-from,omitempty"` + + Prices map[string]float64 `json:"prices,omitempty"` + Screenshots []snap.ScreenshotInfo `json:"screenshots,omitempty"` + Media snap.MediaInfos `json:"media,omitempty"` + + // The flattended channel map with $track/$risk + Channels map[string]*snap.ChannelSnapInfo `json:"channels,omitempty"` + + // The ordered list of tracks that contains channels + Tracks []string `json:"tracks,omitempty"` +} + +func (s *Snap) MarshalJSON() ([]byte, error) { + type auxSnap Snap // use auxiliary type so that Go does not call Snap.MarshalJSON() + // separate type just for marshalling + m := struct { + auxSnap + InstallDate *time.Time `json:"install-date,omitempty"` + }{ + auxSnap: auxSnap(*s), + } + if !s.InstallDate.IsZero() { + m.InstallDate = &s.InstallDate + } + return json.Marshal(&m) +} + +// 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 + Scope 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") + } + if len(names) > 0 { + q.Add("snaps", strings.Join(names, ",")) + } + + snaps, _, err := client.snapsFromPath("/v2/snaps", q) + if err != nil { + return nil, err + } + + if len(snaps) == 0 { + return nil, ErrNoSnapsInstalled + } + + return snaps, 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) + } + if opts.Scope != "" { + q.Set("scope", opts.Scope) + } + + 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 e, ok := err.(*Error); ok { + return nil, nil, e + } + 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..1718ed83 --- /dev/null +++ b/client/packages_test.go @@ -0,0 +1,335 @@ +// -*- 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 ( + "encoding/json" + "fmt" + "net/url" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/snap" +) + +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) TestClientFindWithScopeSetsQuery(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{ + Scope: "mouthwash", + }) + 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{""}, "scope": []string{"mouthwash"}, + }) +} + +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", + "title": "Title", + "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, + "license": "GPL-3.0", + "name": "hello-world", + "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "resource": "/v2/snaps/hello-world.canonical", + "status": "available", + "type": "app", + "version": "1.0.18", + "confinement": "strict", + "private": true, + "common-ids": ["org.funky.snap"] + }], + "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", + Title: "Title", + 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, + License: "GPL-3.0", + Name: "hello-world", + Developer: "canonical", + Publisher: &snap.StoreAccount{ + ID: "canonical", + Username: "canonical", + DisplayName: "Canonical", + Validation: "verified", + }, + Status: client.StatusAvailable, + Type: client.TypeApp, + Version: "1.0.18", + Confinement: client.StrictConfinement, + Private: true, + DevMode: false, + CommonIDs: []string{"org.funky.snap"}, + }}) +} + +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", + "title": "Title", + "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", + "license": "GPL-3.0", + "name": "chatroom", + "developer": "ogra", + "publisher": { + "id": "ogra-id", + "username": "ogra", + "display-name": "Ogra", + "validation": "unproven" + }, + "resource": "/v2/snaps/chatroom.ogra", + "status": "active", + "type": "app", + "version": "0.1-8", + "revision": 42, + "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"} + ], + "media": [ + {"type": "icon", "url":"http://example.com/icon.png"}, + {"type": "screenshot", "url":"http://example.com/shot1.png", "width":640, "height":480}, + {"type": "screenshot", "url":"http://example.com/shot2.png"} + ], + "common-ids": ["org.funky.snap"] + } + }` + 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", + Title: "Title", + 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), + License: "GPL-3.0", + Name: "chatroom", + Developer: "ogra", + Publisher: &snap.StoreAccount{ + ID: "ogra-id", + Username: "ogra", + DisplayName: "Ogra", + Validation: "unproven", + }, + Status: client.StatusActive, + Type: client.TypeApp, + Version: "0.1-8", + Revision: snap.R(42), + Confinement: client.StrictConfinement, + Private: true, + DevMode: true, + TryMode: true, + Screenshots: []snap.ScreenshotInfo{ + {URL: "http://example.com/shot1.png", Width: 640, Height: 480}, + {URL: "http://example.com/shot2.png"}, + }, + Media: []snap.MediaInfo{ + {Type: "icon", URL: "http://example.com/icon.png"}, + {Type: "screenshot", URL: "http://example.com/shot1.png", Width: 640, Height: 480}, + {Type: "screenshot", URL: "http://example.com/shot2.png"}, + }, + CommonIDs: []string{"org.funky.snap"}, + }) +} + +func (cs *clientSuite) TestAppInfoNoServiceNoDaemon(c *check.C) { + buf, err := json.MarshalIndent(client.AppInfo{Name: "hello"}, "\t", "\t") + c.Assert(err, check.IsNil) + c.Check(string(buf), check.Equals, `{ + "name": "hello" + }`) +} + +func (cs *clientSuite) TestAppInfoServiceDaemon(c *check.C) { + buf, err := json.MarshalIndent(client.AppInfo{ + Snap: "foo", + Name: "hello", + Daemon: "daemon", + Enabled: true, + Active: false, + }, "\t", "\t") + c.Assert(err, check.IsNil) + c.Check(string(buf), check.Equals, `{ + "snap": "foo", + "name": "hello", + "daemon": "daemon", + "enabled": true + }`) +} + +func (cs *clientSuite) TestAppInfoNilNotService(c *check.C) { + var app *client.AppInfo + c.Check(app.IsService(), check.Equals, false) +} + +func (cs *clientSuite) TestAppInfoNoDaemonNotService(c *check.C) { + var app *client.AppInfo + c.Assert(json.Unmarshal([]byte(`{"name": "hello"}`), &app), check.IsNil) + c.Check(app.Name, check.Equals, "hello") + c.Check(app.IsService(), check.Equals, false) +} + +func (cs *clientSuite) TestAppInfoEmptyDaemonNotService(c *check.C) { + var app *client.AppInfo + c.Assert(json.Unmarshal([]byte(`{"name": "hello", "daemon": ""}`), &app), check.IsNil) + c.Check(app.Name, check.Equals, "hello") + c.Check(app.IsService(), check.Equals, false) +} + +func (cs *clientSuite) TestAppInfoDaemonIsService(c *check.C) { + var app *client.AppInfo + + c.Assert(json.Unmarshal([]byte(`{"name": "hello", "daemon": "x"}`), &app), check.IsNil) + c.Check(app.Name, check.Equals, "hello") + c.Check(app.IsService(), check.Equals, true) +} diff --git a/client/snap_op.go b/client/snap_op.go new file mode 100644 index 00000000..3040fa1c --- /dev/null +++ b/client/snap_op.go @@ -0,0 +1,304 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" +) + +type SnapOptions struct { + Amend bool `json:"amend,omitempty"` + 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"` + Unaliased bool `json:"unaliased,omitempty"` + + Users []string `json:"users,omitempty"` +} + +func writeFieldBool(mw *multipart.Writer, key string, val bool) error { + if !val { + return nil + } + return mw.WriteField(key, "true") +} + +func (opts *SnapOptions) writeModeFields(mw *multipart.Writer) error { + fields := []struct { + f string + b bool + }{ + {"devmode", opts.DevMode}, + {"classic", opts.Classic}, + {"jailmode", opts.JailMode}, + {"dangerous", opts.Dangerous}, + } + for _, o := range fields { + if err := writeFieldBool(mw, o.f, o.b); err != nil { + return err + } + } + + return nil +} + +func (opts *SnapOptions) writeOptionFields(mw *multipart.Writer) error { + return writeFieldBool(mw, "unaliased", opts.Unaliased) +} + +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"` + Users []string `json:"users,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) +} + +// Switch moves the snap to a different channel without a refresh +func (client *Client) Switch(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("switch", name, options) +} + +// SnapshotMany snapshots many snaps (all, if names empty) for many users (all, if users is empty). +func (client *Client) SnapshotMany(names []string, users []string) (setID uint64, changeID string, err error) { + result, changeID, err := client.doMultiSnapActionFull("snapshot", names, &SnapOptions{Users: users}) + if err != nil { + return 0, "", err + } + if len(result) == 0 { + return 0, "", fmt.Errorf("server result does not contain snapshot set identifier") + } + var x struct { + SetID uint64 `json:"set-id"` + } + if err := json.Unmarshal(result, &x); err != nil { + return 0, "", err + } + return x.SetID, changeID, nil +} + +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) + } + _, changeID, err = client.doMultiSnapActionFull(actionName, snaps, options) + + return changeID, err +} + +func (client *Client) doMultiSnapActionFull(actionName string, snaps []string, options *SnapOptions) (result json.RawMessage, changeID string, err error) { + action := multiActionData{ + Action: actionName, + Snaps: snaps, + } + if options != nil { + action.Users = options.Users + } + data, err := json.Marshal(&action) + if err != nil { + return nil, "", fmt.Errorf("cannot marshal multi-snap action: %s", err) + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + + return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data)) +} + +// InstallPath sideloads the snap with the given path under optional provided name, +// returning the UUID of the background operation upon success. +func (client *Client) InstallPath(path, name 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", + Name: name, + 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) + options.writeModeFields(mw) + 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{} + } + fields := []struct { + name string + value string + }{ + {"action", action.Action}, + {"name", action.Name}, + {"snap-path", action.SnapPath}, + {"channel", action.Channel}, + } + for _, s := range fields { + if s.value == "" { + continue + } + if err := mw.WriteField(s.name, s.value); err != nil { + pw.CloseWithError(err) + return + } + } + + if err := action.writeModeFields(mw); err != nil { + pw.CloseWithError(err) + return + } + + if err := action.writeOptionFields(mw); 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..b22e21c4 --- /dev/null +++ b/client/snap_op_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 client_test + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime" + "mime/multipart" + "path/filepath" + + "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"}, + {(*client.Client).Switch, "switch"}, +} + +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)) + } + _, _, err := cs.cli.SnapshotMany(nil, nil) + c.Check(err, check.ErrorMatches, `.*fail`) +} + +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)) + } + _, _, err := cs.cli.SnapshotMany(nil, nil) + c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`) +} + +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 { + // Note body is essentially the same as TestClientMultiSnapshot; keep in sync + 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) TestClientMultiSnapshot(c *check.C) { + // Note body is essentially the same as TestClientMultiOpSnap; keep in sync + cs.rsp = `{ + "result": {"set-id": 42}, + "change": "d728", + "status-code": 202, + "type": "async" + }` + setID, changeID, err := cs.cli.SnapshotMany([]string{pkgName}, nil) + c.Assert(err, check.IsNil) + c.Check(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + jsonBody := make(map[string]interface{}) + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil) + c.Check(jsonBody["action"], check.Equals, "snapshot") + c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName}) + c.Check(jsonBody, check.HasLen, 2) + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps") + c.Check(setID, check.Equals, uint64(42)) + c.Check(changeID, check.Equals, "d728") +} + +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.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) TestClientOpInstallPathInstance(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, "foo_bar", 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=\"name\"\r\n\r\nfoo_bar\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 (cs *clientSuite) TestClientOpInstallUnaliased(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{ + Unaliased: true, + } + + _, err = cs.cli.Install("foo", &opts) + c.Assert(err, check.IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + jsonBody := make(map[string]interface{}) + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil, check.Commentf("body: %v", string(body))) + c.Check(jsonBody["unaliased"], check.Equals, true, check.Commentf("body: %v", string(body))) + + _, 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=\"unaliased\"\r\n\r\ntrue\r\n.*") +} + +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{ + {Classic: false, DevMode: false, JailMode: false}, + {Classic: false, DevMode: false, JailMode: true}, + {Classic: false, DevMode: true, JailMode: true}, + {Classic: false, DevMode: true, JailMode: false}, + {Classic: true, DevMode: false, JailMode: false}, + {Classic: true, DevMode: false, JailMode: true}, + {Classic: true, DevMode: true, JailMode: true}, + {Classic: true, DevMode: true, JailMode: false}, + } { + comment := check.Commentf("when Classic:%t DevMode:%t JailMode:%t", opts.Classic, opts.DevMode, opts.JailMode) + 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, comment) + mr := multipart.NewReader(cs.req.Body, params["boundary"]) + formData := formToMap(c, mr) + c.Check(formData["action"], check.Equals, "try", comment) + c.Check(formData["snap-path"], check.Equals, snapdir, comment) + expectedLength := 2 + if opts.Classic { + c.Check(formData["classic"], check.Equals, "true", comment) + expectedLength++ + } + if opts.DevMode { + c.Check(formData["devmode"], check.Equals, "true", comment) + expectedLength++ + } + if opts.JailMode { + c.Check(formData["jailmode"], check.Equals, "true", comment) + expectedLength++ + } + c.Check(len(formData), check.Equals, expectedLength) + + c.Check(cs.req.Method, check.Equals, "POST", comment) + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps"), comment) + c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*", comment) + c.Check(id, check.Equals, "66b3", comment) + } +} + +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/client/snapshot.go b/client/snapshot.go new file mode 100644 index 00000000..2e705668 --- /dev/null +++ b/client/snapshot.go @@ -0,0 +1,175 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "errors" + "fmt" + "net/url" + "strconv" + "strings" + "time" + + "github.com/snapcore/snapd/snap" +) + +var ( + ErrSnapshotSetNotFound = errors.New("no snapshot set with the given ID") + ErrSnapshotSnapsNotFound = errors.New("no snapshot for the requested snaps found in the set with the given ID") +) + +// A snapshotAction is used to request an operation on a snapshot. +type snapshotAction struct { + SetID uint64 `json:"set"` + Action string `json:"action"` + Snaps []string `json:"snaps,omitempty"` + Users []string `json:"users,omitempty"` +} + +// A Snapshot is a collection of archives with a simple metadata json file +// (and hashsums of everything). +type Snapshot struct { + // SetID is the ID of the snapshot set (a snapshot set is the result of a "snap save" invocation) + SetID uint64 `json:"set"` + // the time this snapshot's data collection was started + Time time.Time `json:"time"` + + // information about the snap this data is for + Snap string `json:"snap"` + Revision snap.Revision `json:"revision"` + SnapID string `json:"snap-id,omitempty"` + Epoch snap.Epoch `json:"epoch,omitempty"` + Summary string `json:"summary"` + Version string `json:"version"` + + // the snap's configuration at snapshot time + Conf map[string]interface{} `json:"conf,omitempty"` + + // the hash of the archives' data, keyed by archive path + // (either 'archive.tgz' for the system archive, or + // user/.tgz for each user) + SHA3_384 map[string]string `json:"sha3-384"` + // the sum of the archive sizes + Size int64 `json:"size,omitempty"` + // if the snapshot failed to open this will be the reason why + Broken string `json:"broken,omitempty"` +} + +// IsValid checks whether the snapshot is missing information that +// should be there for a snapshot that's just been opened. +func (sh *Snapshot) IsValid() bool { + return !(sh == nil || sh.SetID == 0 || sh.Snap == "" || sh.Revision.Unset() || len(sh.SHA3_384) == 0 || sh.Time.IsZero()) +} + +// A SnapshotSet is a set of snapshots created by a single "snap save". +type SnapshotSet struct { + ID uint64 `json:"id"` + Snapshots []*Snapshot `json:"snapshots"` +} + +// Time returns the earliest time in the set. +func (ss SnapshotSet) Time() time.Time { + if len(ss.Snapshots) == 0 { + return time.Time{} + } + mint := ss.Snapshots[0].Time + for _, sh := range ss.Snapshots { + if sh.Time.Before(mint) { + mint = sh.Time + } + } + return mint +} + +// Size returns the sum of the set's sizes. +func (ss SnapshotSet) Size() int64 { + var sum int64 + for _, sh := range ss.Snapshots { + sum += sh.Size + } + return sum +} + +// SnapshotSets lists the snapshot sets in the system that belong to the +// given set (if non-zero) and are for the given snaps (if non-empty). +func (client *Client) SnapshotSets(setID uint64, snapNames []string) ([]SnapshotSet, error) { + q := make(url.Values) + if setID > 0 { + q.Add("set", strconv.FormatUint(setID, 10)) + } + if len(snapNames) > 0 { + q.Add("snaps", strings.Join(snapNames, ",")) + } + + var snapshotSets []SnapshotSet + _, err := client.doSync("GET", "/v2/snapshots", q, nil, nil, &snapshotSets) + return snapshotSets, err +} + +// ForgetSnapshots permanently removes the snapshot set, limited to the +// given snaps (if non-empty). +func (client *Client) ForgetSnapshots(setID uint64, snaps []string) (changeID string, err error) { + return client.snapshotAction(&snapshotAction{ + SetID: setID, + Action: "forget", + Snaps: snaps, + }) +} + +// CheckSnapshots verifies the archive checksums in the given snapshot set. +// +// If snaps or users are non-empty, limit to checking only those +// archives of the snapshot. +func (client *Client) CheckSnapshots(setID uint64, snaps []string, users []string) (changeID string, err error) { + return client.snapshotAction(&snapshotAction{ + SetID: setID, + Action: "check", + Snaps: snaps, + Users: users, + }) +} + +// RestoreSnapshots extracts the given snapshot set. +// +// If snaps or users are non-empty, limit to checking only those +// archives of the snapshot. +func (client *Client) RestoreSnapshots(setID uint64, snaps []string, users []string) (changeID string, err error) { + return client.snapshotAction(&snapshotAction{ + SetID: setID, + Action: "restore", + Snaps: snaps, + Users: users, + }) +} + +func (client *Client) snapshotAction(action *snapshotAction) (changeID string, err error) { + data, err := json.Marshal(action) + if err != nil { + return "", fmt.Errorf("cannot marshal snapshot action: %v", err) + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + + return client.doAsync("POST", "/v2/snapshots", nil, headers, bytes.NewBuffer(data)) +} diff --git a/client/snapshot_test.go b/client/snapshot_test.go new file mode 100644 index 00000000..361161e3 --- /dev/null +++ b/client/snapshot_test.go @@ -0,0 +1,138 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 ( + "net/url" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/snap" +) + +func (cs *clientSuite) TestClientSnapshotIsValid(c *check.C) { + now := time.Now() + revno := snap.R(1) + sums := map[string]string{"user/foo.tgz": "some long hash"} + c.Check((&client.Snapshot{ + SetID: 42, + Time: now, + Snap: "asnap", + Revision: revno, + SHA3_384: sums, + }).IsValid(), check.Equals, true) + + for desc, snapshot := range map[string]*client.Snapshot{ + "nil": nil, + "empty": {}, + "no id": { /*SetID: 42,*/ Time: now, Snap: "asnap", Revision: revno, SHA3_384: sums}, + "no time": {SetID: 42 /*Time: now,*/, Snap: "asnap", Revision: revno, SHA3_384: sums}, + "no snap": {SetID: 42, Time: now /*Snap: "asnap",*/, Revision: revno, SHA3_384: sums}, + "no rev": {SetID: 42, Time: now, Snap: "asnap" /*Revision: revno,*/, SHA3_384: sums}, + "no sums": {SetID: 42, Time: now, Snap: "asnap", Revision: revno /*SHA3_384: sums*/}, + } { + c.Check(snapshot.IsValid(), check.Equals, false, check.Commentf("%s", desc)) + } + +} + +func (cs *clientSuite) TestClientSnapshotSetTime(c *check.C) { + // if set is empty, it doesn't explode (and returns the zero time) + c.Check(client.SnapshotSet{}.Time().IsZero(), check.Equals, true) + // if not empty, returns the earliest one + c.Check(client.SnapshotSet{Snapshots: []*client.Snapshot{ + {Time: time.Unix(3, 0)}, + {Time: time.Unix(1, 0)}, + {Time: time.Unix(2, 0)}, + }}.Time(), check.DeepEquals, time.Unix(1, 0)) +} + +func (cs *clientSuite) TestClientSnapshotSetSize(c *check.C) { + // if set is empty, doesn't explode (and returns 0) + c.Check(client.SnapshotSet{}.Size(), check.Equals, int64(0)) + // if not empty, returns the sum + c.Check(client.SnapshotSet{Snapshots: []*client.Snapshot{ + {Size: 1}, + {Size: 2}, + {Size: 3}, + }}.Size(), check.DeepEquals, int64(6)) +} + +func (cs *clientSuite) TestClientSnapshotSets(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": [{"id": 1}, {"id":2}] +}` + sets, err := cs.cli.SnapshotSets(42, []string{"foo", "bar"}) + c.Assert(err, check.IsNil) + c.Check(sets, check.DeepEquals, []client.SnapshotSet{{ID: 1}, {ID: 2}}) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snapshots") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ + "set": []string{"42"}, + "snaps": []string{"foo,bar"}, + }) +} + +func (cs *clientSuite) testClientSnapshotActionFull(c *check.C, action string, users []string, f func() (string, error)) { + cs.rsp = `{ + "status-code": 202, + "type": "async", + "change": "1too3" + }` + id, err := f() + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "1too3") + + c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") + + act, err := client.UnmarshalSnapshotAction(cs.req.Body) + c.Assert(err, check.IsNil) + c.Check(act.SetID, check.Equals, uint64(42)) + c.Check(act.Action, check.Equals, action) + c.Check(act.Snaps, check.DeepEquals, []string{"asnap", "bsnap"}) + c.Check(act.Users, check.DeepEquals, users) + + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snapshots") + c.Check(cs.req.URL.Query(), check.HasLen, 0) +} + +func (cs *clientSuite) TestClientForgetSnapshot(c *check.C) { + cs.testClientSnapshotActionFull(c, "forget", nil, func() (string, error) { + return cs.cli.ForgetSnapshots(42, []string{"asnap", "bsnap"}) + }) +} + +func (cs *clientSuite) testClientSnapshotAction(c *check.C, action string, f func(uint64, []string, []string) (string, error)) { + cs.testClientSnapshotActionFull(c, action, []string{"auser", "buser"}, func() (string, error) { + return f(42, []string{"asnap", "bsnap"}, []string{"auser", "buser"}) + }) +} + +func (cs *clientSuite) TestClientCheckSnapshots(c *check.C) { + cs.testClientSnapshotAction(c, "check", cs.cli.CheckSnapshots) +} + +func (cs *clientSuite) TestClientRestoreSnapshots(c *check.C) { + cs.testClientSnapshotAction(c, "restore", cs.cli.RestoreSnapshots) +} diff --git a/client/warnings.go b/client/warnings.go new file mode 100644 index 00000000..32d0890a --- /dev/null +++ b/client/warnings.go @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "time" +) + +// A Warning is a short messages that's meant to alert about system events. +// There'll only ever be one Warning with the same message, and it can be +// silenced for a while before repeating. After a (supposedly longer) while +// it'll go away on its own (unless it recurrs). +type Warning struct { + Message string `json:"message"` + FirstAdded time.Time `json:"first-added"` + LastAdded time.Time `json:"last-added"` + LastShown time.Time `json:"last-shown,omitempty"` + ExpireAfter time.Duration `json:"expire-after,omitempty"` + RepeatAfter time.Duration `json:"repeat-after,omitempty"` +} + +type jsonWarning struct { + Warning + ExpireAfter string `json:"expire-after,omitempty"` + RepeatAfter string `json:"repeat-after,omitempty"` +} + +// WarningsOptions contains options for querying snapd for warnings +// supported options: +// - All: return all warnings, instead of only the un-okayed ones. +type WarningsOptions struct { + All bool +} + +// Warnings returns the list of un-okayed warnings. +func (client *Client) Warnings(opts WarningsOptions) ([]*Warning, error) { + var jws []*jsonWarning + q := make(url.Values) + if opts.All { + q.Add("select", "all") + } + _, err := client.doSync("GET", "/v2/warnings", q, nil, nil, &jws) + + ws := make([]*Warning, len(jws)) + for i, jw := range jws { + ws[i] = &jw.Warning + ws[i].ExpireAfter, _ = time.ParseDuration(jw.ExpireAfter) + ws[i].RepeatAfter, _ = time.ParseDuration(jw.RepeatAfter) + } + + return ws, err +} + +type warningsAction struct { + Action string `json:"action"` + Timestamp time.Time `json:"timestamp"` +} + +// Okay asks snapd to chill about the warnings that would have been returned by +// Warnings at the given time. +func (client *Client) Okay(t time.Time) error { + var body bytes.Buffer + var op = warningsAction{Action: "okay", Timestamp: t} + if err := json.NewEncoder(&body).Encode(op); err != nil { + return err + } + _, err := client.doSync("POST", "/v2/warnings", nil, nil, &body, nil) + return err +} diff --git a/client/warnings_test.go b/client/warnings_test.go new file mode 100644 index 00000000..6eff5ee4 --- /dev/null +++ b/client/warnings_test.go @@ -0,0 +1,121 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func (cs *clientSuite) testWarnings(c *check.C, all bool) { + t1 := time.Date(2018, 9, 19, 12, 41, 18, 505007495, time.UTC) + t2 := time.Date(2018, 9, 19, 12, 44, 19, 680362867, time.UTC) + cs.rsp = `{ + "result": [ + { + "expire-after": "672h0m0s", + "first-added": "2018-09-19T12:41:18.505007495Z", + "last-added": "2018-09-19T12:41:18.505007495Z", + "message": "hello world number one", + "repeat-after": "24h0m0s" + }, + { + "expire-after": "672h0m0s", + "first-added": "2018-09-19T12:44:19.680362867Z", + "last-added": "2018-09-19T12:44:19.680362867Z", + "message": "hello world number two", + "repeat-after": "24h0m0s" + } + ], + "status": "OK", + "status-code": 200, + "type": "sync", + "warning-count": 2, + "warning-timestamp": "2018-09-19T12:44:19.680362867Z" + }` + + ws, err := cs.cli.Warnings(client.WarningsOptions{All: all}) + c.Assert(err, check.IsNil) + c.Check(ws, check.DeepEquals, []*client.Warning{ + { + Message: "hello world number one", + FirstAdded: t1, + LastAdded: t1, + ExpireAfter: time.Hour * 24 * 28, + RepeatAfter: time.Hour * 24, + }, + { + Message: "hello world number two", + FirstAdded: t2, + LastAdded: t2, + ExpireAfter: time.Hour * 24 * 28, + RepeatAfter: time.Hour * 24, + }, + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/warnings") + query := cs.req.URL.Query() + if all { + c.Check(query, check.HasLen, 1) + c.Check(query.Get("select"), check.Equals, "all") + } else { + c.Check(query, check.HasLen, 0) + } + + // this could be done at the end of any sync method + count, stamp := cs.cli.WarningsSummary() + c.Check(count, check.Equals, 2) + c.Check(stamp, check.Equals, t2) +} + +func (cs *clientSuite) TestWarningsAll(c *check.C) { + cs.testWarnings(c, true) +} + +func (cs *clientSuite) TestWarnings(c *check.C) { + cs.testWarnings(c, false) +} + +func (cs *clientSuite) TestOkay(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": { } + }` + t0 := time.Now() + err := cs.cli.Okay(t0) + c.Assert(err, check.IsNil) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Query(), check.HasLen, 0) + var body map[string]interface{} + c.Assert(json.NewDecoder(cs.req.Body).Decode(&body), check.IsNil) + c.Check(body, check.HasLen, 2) + c.Check(body["action"], check.Equals, "okay") + c.Check(body["timestamp"], check.Equals, t0.Format(time.RFC3339Nano)) + + // note there's no warnings summary in the response + count, stamp := cs.cli.WarningsSummary() + c.Check(count, check.Equals, 0) + c.Check(stamp, check.Equals, time.Time{}) +} diff --git a/cmd/.indent.pro b/cmd/.indent.pro new file mode 100644 index 00000000..f410d3c6 --- /dev/null +++ b/cmd/.indent.pro @@ -0,0 +1,34 @@ +-nbad +-bap +-nbc +-bbo +-hnl +-br +-brs +-c33 +-cd33 +-ncdb +-ce +-ci4 +-cli0 +-d0 +-di1 +-nfc1 +-i8 +-ip0 +-l80 +-lp +-npcs +-nprs +-npsl +-sai +-saf +-saw +-ncs +-nsc +-sob +-nfca +-cp33 +-ss +-ts8 +-il1 diff --git a/cmd/Makefile.am b/cmd/Makefile.am new file mode 100644 index 00000000..de905ad5 --- /dev/null +++ b/cmd/Makefile.am @@ -0,0 +1,491 @@ + +EXTRA_DIST = VERSION snap-confine/PORTING +CLEANFILES = +TESTS = +libexec_PROGRAMS = +dist_man_MANS = +noinst_PROGRAMS = +noinst_LIBRARIES = + +CHECK_CFLAGS = -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes \ + -Wno-missing-field-initializers -Wno-unused-parameter + +# Make all warnings errors when building for unit tests +if WITH_UNIT_TESTS +CHECK_CFLAGS += -Werror +endif + +subdirs = \ + libsnap-confine-private \ + snap-confine \ + snap-discard-ns \ + snap-gdb-shim \ + snap-update-ns \ + snapd-env-generator \ + snapd-generator \ + system-shutdown + +# Run check-syntax when checking +# TODO: conver those to autotools-style tests later +check: check-unit-tests + +# Force particular coding style on all source and header files. +.PHONY: check-syntax-c +check-syntax-c: + echo "WARNING: check-syntax-c produces different results for different version of indent" + echo "Your version of indent: `indent --version`" + @d=`mktemp -d`; \ + trap 'rm -rf $d' EXIT; \ + for f in $(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch])) ; do \ + out="$$d/`basename $$f.out`"; \ + echo "Checking $$f ... "; \ + HOME=$(srcdir) indent "$$f" -o "$$out"; \ + diff -Naur "$$f" "$$out" || exit 1; \ + done; + +.PHONY: check-unit-tests +if WITH_UNIT_TESTS +check-unit-tests: snap-confine/unit-tests system-shutdown/unit-tests libsnap-confine-private/unit-tests + $(HAVE_VALGRIND) ./libsnap-confine-private/unit-tests + SNAP_DEVICE_HELPER=$(srcdir)/snap-confine/snap-device-helper $(HAVE_VALGRIND) ./snap-confine/unit-tests + $(HAVE_VALGRIND) ./system-shutdown/unit-tests +else +check-unit-tests: + echo "unit tests are disabled (rebuild with --enable-unit-tests)" +endif + +new_format = \ + snap-confine/seccomp-support-ext.c \ + snap-confine/seccomp-support-ext.h \ + snap-discard-ns/snap-discard-ns.c +.PHONY: fmt +fmt:: $(filter $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch]))) + clang-format -style='{BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 120}' -i $^ + +fmt:: $(filter-out $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch]))) + HOME=$(srcdir) indent $^ + +# 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-debug snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns snap-seccomp/snap-seccomp snap-discard-ns/snap-discard-ns + sudo install -D -m 6755 snap-confine/snap-confine-debug $(DESTDIR)$(libexecdir)/snap-confine + sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real + sudo install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/ + sudo apparmor_parser -r snap-confine/snap-confine.apparmor + sudo install -m 755 snap-update-ns/snap-update-ns $(DESTDIR)$(libexecdir)/snap-update-ns + sudo install -m 755 snap-discard-ns/snap-discard-ns $(DESTDIR)$(libexecdir)/snap-discard-ns + sudo install -m 755 snap-seccomp/snap-seccomp $(DESTDIR)$(libexecdir)/snap-seccomp + +# for the hack target also: +snap-update-ns/snap-update-ns: snap-update-ns/*.go snap-update-ns/*.[ch] + cd snap-update-ns && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -i -v +snap-seccomp/snap-seccomp: snap-seccomp/*.go + cd snap-seccomp && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -i -v + +## +## libsnap-confine-private.a +## + +noinst_LIBRARIES += libsnap-confine-private.a + +libsnap_confine_private_a_SOURCES = \ + libsnap-confine-private/apparmor-support.c \ + libsnap-confine-private/apparmor-support.h \ + libsnap-confine-private/cgroup-freezer-support.c \ + libsnap-confine-private/cgroup-freezer-support.h \ + libsnap-confine-private/classic.c \ + libsnap-confine-private/classic.h \ + libsnap-confine-private/cleanup-funcs.c \ + libsnap-confine-private/cleanup-funcs.h \ + libsnap-confine-private/error.c \ + libsnap-confine-private/error.h \ + libsnap-confine-private/fault-injection.c \ + libsnap-confine-private/fault-injection.h \ + libsnap-confine-private/feature.c \ + libsnap-confine-private/feature.h \ + libsnap-confine-private/locking.c \ + libsnap-confine-private/locking.h \ + libsnap-confine-private/mount-opt.c \ + libsnap-confine-private/mount-opt.h \ + libsnap-confine-private/mountinfo.c \ + libsnap-confine-private/mountinfo.h \ + libsnap-confine-private/privs.c \ + libsnap-confine-private/privs.h \ + libsnap-confine-private/secure-getenv.c \ + libsnap-confine-private/secure-getenv.h \ + libsnap-confine-private/snap.c \ + libsnap-confine-private/snap.h \ + libsnap-confine-private/string-utils.c \ + libsnap-confine-private/string-utils.h \ + libsnap-confine-private/tool.c \ + libsnap-confine-private/tool.h \ + libsnap-confine-private/utils.c \ + libsnap-confine-private/utils.h +libsnap_confine_private_a_CFLAGS = $(CHECK_CFLAGS) + +noinst_LIBRARIES += libsnap-confine-private-debug.a +libsnap_confine_private_debug_a_SOURCES = $(libsnap_confine_private_a_SOURCES) +libsnap_confine_private_debug_a_CFLAGS = $(CHECK_CFLAGS) -DSNAP_CONFINE_DEBUG_BUILD=1 + +if WITH_UNIT_TESTS +noinst_PROGRAMS += libsnap-confine-private/unit-tests +libsnap_confine_private_unit_tests_SOURCES = \ + libsnap-confine-private/classic-test.c \ + libsnap-confine-private/cleanup-funcs-test.c \ + libsnap-confine-private/error-test.c \ + libsnap-confine-private/fault-injection-test.c \ + libsnap-confine-private/feature-test.c \ + libsnap-confine-private/locking-test.c \ + libsnap-confine-private/mount-opt-test.c \ + libsnap-confine-private/mountinfo-test.c \ + libsnap-confine-private/privs-test.c \ + libsnap-confine-private/secure-getenv-test.c \ + libsnap-confine-private/snap-test.c \ + libsnap-confine-private/string-utils-test.c \ + libsnap-confine-private/test-utils-test.c \ + libsnap-confine-private/test-utils.c \ + libsnap-confine-private/unit-tests-main.c \ + libsnap-confine-private/unit-tests.c \ + libsnap-confine-private/unit-tests.h \ + libsnap-confine-private/utils-test.c +libsnap_confine_private_unit_tests_CFLAGS = $(CHECK_CFLAGS) $(GLIB_CFLAGS) +libsnap_confine_private_unit_tests_LDADD = $(GLIB_LIBS) +libsnap_confine_private_unit_tests_CFLAGS += -D_ENABLE_FAULT_INJECTION +libsnap_confine_private_unit_tests_STATIC = + +if STATIC_LIBCAP +libsnap_confine_private_unit_tests_STATIC += -lcap +else +libsnap_confine_private_unit_tests_LDADD += -lcap +endif # STATIC_LIBCAP + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +libsnap-confine-private/unit-tests$(EXEEXT): $(libsnap_confine_private_unit_tests_OBJECTS) $(libsnap_confine_private_unit_tests_DEPENDENCIES) $(EXTRA_libsnap_confine_private_unit_tests_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f libsnap-confine-private/unit-tests$(EXEEXT) + $(AM_V_CCLD)$(libsnap_confine_private_unit_tests_LINK) $(libsnap_confine_private_unit_tests_OBJECTS) $(libsnap_confine_private_unit_tests_LDADD) $(LIBS) + +libsnap-confine-private/unit-tests$(EXEEXT): LIBS += -Wl,-Bstatic $(libsnap_confine_private_unit_tests_STATIC) -Wl,-Bdynamic +endif # WITH_UNIT_TESTS + +## +## decode-mount-opts +## + +noinst_PROGRAMS += decode-mount-opts/decode-mount-opts + +decode_mount_opts_decode_mount_opts_SOURCES = \ + decode-mount-opts/decode-mount-opts.c +decode_mount_opts_decode_mount_opts_LDADD = libsnap-confine-private.a +decode_mount_opts_decode_mount_opts_STATIC = + +if STATIC_LIBCAP +decode_mount_opts_decode_mount_opts_STATIC += -lcap +else +decode_mount_opts_decode_mount_opts_LDADD += -lcap +endif # STATIC_LIBCAP + +# XXX: this makes automake generate decode_mount_opts_decode_mount_opts_LINK +decode_mount_opts_decode_mount_opts_CFLAGS = -D_fake + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +decode-mount-opts/decode-mount-opts$(EXEEXT): $(decode_mount_opts_decode_mount_opts_OBJECTS) $(decode_mount_opts_decode_mount_opts_DEPENDENCIES) $(EXTRA_decode_mount_opts_decode_mount_opts_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f decode-mount-opts/decode-mount-opts$(EXEEXT) + $(AM_V_CCLD)$(decode_mount_opts_decode_mount_opts_LINK) $(decode_mount_opts_decode_mount_opts_OBJECTS) $(decode_mount_opts_decode_mount_opts_LDADD) $(LIBS) + +decode-mount-opts/decode-mount-opts$(EXEEXT): LIBS += -Wl,-Bstatic $(decode_mount_opts_decode_mount_opts_STATIC) -Wl,-Bdynamic + +## +## snap-confine +## + +libexec_PROGRAMS += snap-confine/snap-confine +if HAVE_RST2MAN +dist_man_MANS += snap-confine/snap-confine.8 +CLEANFILES += snap-confine/snap-confine.8 +endif +EXTRA_DIST += snap-confine/snap-confine.rst +EXTRA_DIST += snap-confine/snap-confine.apparmor.in + +snap_confine_snap_confine_SOURCES = \ + snap-confine/cookie-support.c \ + snap-confine/cookie-support.h \ + snap-confine/mount-support-nvidia.c \ + snap-confine/mount-support-nvidia.h \ + snap-confine/mount-support.c \ + snap-confine/mount-support.h \ + snap-confine/ns-support.c \ + snap-confine/ns-support.h \ + snap-confine/snap-confine-args.c \ + snap-confine/snap-confine-args.h \ + snap-confine/snap-confine.c \ + snap-confine/udev-support.c \ + snap-confine/udev-support.h \ + snap-confine/user-support.c \ + snap-confine/user-support.h + +snap_confine_snap_confine_CFLAGS = $(CHECK_CFLAGS) $(AM_CFLAGS) -DLIBEXECDIR=\"$(libexecdir)\" -DNATIVE_LIBDIR=\"$(libdir)\" +snap_confine_snap_confine_LDFLAGS = $(AM_LDFLAGS) +snap_confine_snap_confine_LDADD = libsnap-confine-private.a +snap_confine_snap_confine_CFLAGS += $(LIBUDEV_CFLAGS) +snap_confine_snap_confine_LDADD += $(snap_confine_snap_confine_extra_libs) +# _STATIC is where we collect statically linked in libraries +snap_confine_snap_confine_STATIC = +# use a separate variable instead of snap_confine_snap_confine_LDADD to collect +# all external libraries, this way it can be reused in +# snap_confine_snap_confine_debug_LDADD withouth applying any text +# transformations +snap_confine_snap_confine_extra_libs = $(LIBUDEV_LIBS) + +if STATIC_LIBCAP +snap_confine_snap_confine_STATIC += -lcap +else +snap_confine_snap_confine_extra_libs += -lcap +endif # STATIC_LIBCAP + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +snap-confine/snap-confine$(EXEEXT): $(snap_confine_snap_confine_OBJECTS) $(snap_confine_snap_confine_DEPENDENCIES) $(EXTRA_snap_confine_snap_confine_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f snap-confine/snap-confine$(EXEEXT) + $(AM_V_CCLD)$(snap_confine_snap_confine_LINK) $(snap_confine_snap_confine_OBJECTS) $(snap_confine_snap_confine_LDADD) $(LIBS) + +snap-confine/snap-confine$(EXEEXT): LIBS += -Wl,-Bstatic $(snap_confine_snap_confine_STATIC) -Wl,-Bdynamic -pthread + +# This is here to help fix rpmlint hardening issue. +# https://en.opensuse.org/openSUSE:Packaging_checks#non-position-independent-executable +snap_confine_snap_confine_CFLAGS += $(SUID_CFLAGS) +snap_confine_snap_confine_LDFLAGS += $(SUID_LDFLAGS) + +if SECCOMP +snap_confine_snap_confine_SOURCES += \ + snap-confine/seccomp-support-ext.c \ + snap-confine/seccomp-support-ext.h \ + snap-confine/seccomp-support.c \ + snap-confine/seccomp-support.h +snap_confine_snap_confine_CFLAGS += $(SECCOMP_CFLAGS) +if STATIC_LIBSECCOMP +snap_confine_snap_confine_STATIC += $(shell $(PKG_CONFIG) --static --libs libseccomp) +else +snap_confine_snap_confine_extra_libs += $(SECCOMP_LIBS) +endif # STATIC_LIBSECCOMP +endif # SECCOMP + +if APPARMOR +snap_confine_snap_confine_CFLAGS += $(APPARMOR_CFLAGS) +if STATIC_LIBAPPARMOR +snap_confine_snap_confine_STATIC += $(shell $(PKG_CONFIG) --static --libs libapparmor) +else +snap_confine_snap_confine_extra_libs += $(APPARMOR_LIBS) +endif # STATIC_LIBAPPARMOR +endif # APPARMOR + +# an extra build that has additional debugging enabled at compile time + +noinst_PROGRAMS += snap-confine/snap-confine-debug +snap_confine_snap_confine_debug_SOURCES = $(snap_confine_snap_confine_SOURCES) +snap_confine_snap_confine_debug_CFLAGS = $(snap_confine_snap_confine_CFLAGS) +snap_confine_snap_confine_debug_LDFLAGS = $(snap_confine_snap_confine_LDFLAGS) +snap_confine_snap_confine_debug_LDADD = libsnap-confine-private-debug.a $(snap_confine_snap_confine_extra_libs) +snap_confine_snap_confine_debug_CFLAGS += -DSNAP_CONFINE_DEBUG_BUILD=1 +snap_confine_snap_confine_debug_STATIC = $(snap_confine_snap_confine_STATIC) + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +snap-confine/snap-confine-debug$(EXEEXT): $(snap_confine_snap_confine_debug_OBJECTS) $(snap_confine_snap_confine_debug_DEPENDENCIES) $(EXTRA_snap_confine_snap_confine_debug_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f snap-confine/snap-confine-debug$(EXEEXT) + $(AM_V_CCLD)$(snap_confine_snap_confine_debug_LINK) $(snap_confine_snap_confine_debug_OBJECTS) $(snap_confine_snap_confine_debug_LDADD) $(LIBS) + +snap-confine/snap-confine-debug$(EXEEXT): LIBS += -Wl,-Bstatic $(snap_confine_snap_confine_debug_STATIC) -Wl,-Bdynamic -pthread + +if WITH_UNIT_TESTS +noinst_PROGRAMS += snap-confine/unit-tests +snap_confine_unit_tests_SOURCES = \ + libsnap-confine-private/test-utils.c \ + libsnap-confine-private/unit-tests-main.c \ + libsnap-confine-private/unit-tests.c \ + libsnap-confine-private/unit-tests.h \ + snap-confine/cookie-support-test.c \ + snap-confine/mount-support-test.c \ + snap-confine/ns-support-test.c \ + snap-confine/snap-confine-args-test.c \ + snap-confine/snap-device-helper-test.c +snap_confine_unit_tests_CFLAGS = $(snap_confine_snap_confine_CFLAGS) $(GLIB_CFLAGS) +snap_confine_unit_tests_LDADD = $(snap_confine_snap_confine_LDADD) $(GLIB_LIBS) +snap_confine_unit_tests_LDFLAGS = $(snap_confine_snap_confine_LDFLAGS) +snap_confine_unit_tests_STATIC = $(snap_confine_snap_confine_STATIC) + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +snap-confine/unit-tests$(EXEEXT): $(snap_confine_unit_tests_OBJECTS) $(snap_confine_unit_tests_DEPENDENCIES) $(EXTRA_snap_confine_unit_tests_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f snap-confine/unit-tests$(EXEEXT) + $(AM_V_CCLD)$(snap_confine_unit_tests_LINK) $(snap_confine_unit_tests_OBJECTS) $(snap_confine_unit_tests_LDADD) $(LIBS) + +snap-confine/unit-tests$(EXEEXT): LIBS += -Wl,-Bstatic $(snap_confine_unit_tests_STATIC) -Wl,-Bdynamic -pthread +endif # WITH_UNIT_TESTS + +if HAVE_RST2MAN +%.8: %.rst + $(HAVE_RST2MAN) $^ > $@ +endif + +snap-confine/snap-confine.apparmor: snap-confine/snap-confine.apparmor.in Makefile + sed -e 's,[@]LIBEXECDIR[@],$(libexecdir),g' -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@ + +# Install 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 +install-data-local:: snap-confine/snap-confine.apparmor +if APPARMOR + install -d -m 755 $(DESTDIR)/etc/apparmor.d/ + install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine +endif + install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/ + +# NOTE: The 'void' directory *has to* be chmod 000 +install-data-local:: + install -d -m 000 $(DESTDIR)/var/lib/snapd/void + +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 u+s,g+s (setuid and setgid) + chmod 6755 $(DESTDIR)$(libexecdir)/snap-confine +endif + +## +## snap-mgmt +## + +libexec_SCRIPTS = snap-mgmt/snap-mgmt +CLEANFILES += snap-mgmt/$(am__dirstamp) snap-mgmt/snap-mgmt + +snap-mgmt/$(am__dirstamp): + mkdir -p $$(dirname $@) + touch $@ + +snap-mgmt/snap-mgmt: snap-mgmt/snap-mgmt.sh.in Makefile snap-mgmt/$(am__dirstamp) + sed -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@ + + +## +## ubuntu-core-launcher +## + +install-exec-hook:: + install -d -m 755 $(DESTDIR)$(bindir) + ln -sf $(libexecdir)/snap-confine $(DESTDIR)$(bindir)/ubuntu-core-launcher + +## +## snap-device-helper +## + +EXTRA_DIST += \ + snap-confine/snap-device-helper + +# 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 support script for udev rules +install-exec-local:: + install -d -m 755 $(DESTDIR)$(libexecdir) + install -m 755 $(srcdir)/snap-confine/snap-device-helper $(DESTDIR)$(libexecdir) + +## +## snap-discard-ns +## + +libexec_PROGRAMS += snap-discard-ns/snap-discard-ns +if HAVE_RST2MAN +dist_man_MANS += snap-discard-ns/snap-discard-ns.8 +CLEANFILES += snap-discard-ns/snap-discard-ns.8 +endif +EXTRA_DIST += snap-discard-ns/snap-discard-ns.rst + +snap_discard_ns_snap_discard_ns_SOURCES = \ + snap-discard-ns/snap-discard-ns.c +snap_discard_ns_snap_discard_ns_CFLAGS = $(CHECK_CFLAGS) $(AM_CFLAGS) +snap_discard_ns_snap_discard_ns_LDFLAGS = $(AM_LDFLAGS) +snap_discard_ns_snap_discard_ns_LDADD = libsnap-confine-private.a +snap_discard_ns_snap_discard_ns_STATIC = + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +snap-discard-ns/snap-discard-ns$(EXEEXT): $(snap_discard_ns_snap_discard_ns_OBJECTS) $(snap_discard_ns_snap_discard_ns_DEPENDENCIES) $(EXTRA_snap_discard_ns_snap_discard_ns_DEPENDENCIES) snap-discard-ns/$(am__dirstamp) + @rm -f snap-discard-ns/snap-discard-ns$(EXEEXT) + $(AM_V_CCLD)$(snap_discard_ns_snap_discard_ns_LINK) $(snap_discard_ns_snap_discard_ns_OBJECTS) $(snap_discard_ns_snap_discard_ns_LDADD) $(LIBS) + +snap-discard-ns/snap-discard-ns$(EXEEXT): LIBS += -Wl,-Bstatic $(snap_discard_ns_snap_discard_ns_STATIC) -Wl,-Bdynamic -pthread + +## +## system-shutdown +## + +libexec_PROGRAMS += system-shutdown/system-shutdown + +system_shutdown_system_shutdown_SOURCES = \ + system-shutdown/system-shutdown-utils.c \ + system-shutdown/system-shutdown-utils.h \ + system-shutdown/system-shutdown.c +system_shutdown_system_shutdown_LDADD = libsnap-confine-private.a +system_shutdown_system_shutdown_CFLAGS = $(CHECK_CFLAGS) $(filter-out -fPIE -pie,$(CFLAGS)) -static +system_shutdown_system_shutdown_LDFLAGS = $(filter-out -fPIE -pie,$(LDFLAGS)) -static + +if WITH_UNIT_TESTS +noinst_PROGRAMS += system-shutdown/unit-tests +system_shutdown_unit_tests_SOURCES = \ + libsnap-confine-private/unit-tests-main.c \ + libsnap-confine-private/unit-tests.c \ + system-shutdown/system-shutdown-utils-test.c +system_shutdown_unit_tests_LDADD = libsnap-confine-private.a +system_shutdown_unit_tests_CFLAGS = $(GLIB_CFLAGS) +system_shutdown_unit_tests_LDADD += $(GLIB_LIBS) +endif + +## +## snap-gdb-shim +## + +libexec_PROGRAMS += snap-gdb-shim/snap-gdb-shim + +snap_gdb_shim_snap_gdb_shim_SOURCES = \ + snap-gdb-shim/snap-gdb-shim.c + +snap_gdb_shim_snap_gdb_shim_LDADD = libsnap-confine-private.a + +## +## snapd-generator +## + +systemdsystemgeneratordir = $(SYSTEMD_SYSTEM_GENERATOR_DIR) +systemdsystemgenerator_PROGRAMS = snapd-generator/snapd-generator + +snapd_generator_snapd_generator_SOURCES = snapd-generator/main.c +snapd_generator_snapd_generator_LDADD = libsnap-confine-private.a + +## +## snapd-env-generator +## + +systemdsystemenvgeneratordir=$(SYSTEMD_SYSTEM_ENV_GENERATOR_DIR) +systemdsystemenvgenerator_PROGRAMS = snapd-env-generator/snapd-env-generator + +snapd_env_generator_snapd_env_generator_SOURCES = snapd-env-generator/main.c +snapd_env_generator_snapd_env_generator_LDADD = libsnap-confine-private.a +EXTRA_DIST += snapd-env-generator/snapd-env-generator.rst + +if HAVE_RST2MAN +dist_man_MANS += snapd-env-generator/snapd-env-generator.8 +CLEANFILES += snapd-env-generator/snapd-env-generator.8 +endif + +## +## snapd-apparmor +## + +EXTRA_DIST += snapd-apparmor/snapd-apparmor + +install-exec-local:: + install -d -m 755 $(DESTDIR)$(libexecdir) +if APPARMOR + install -m 755 $(srcdir)/snapd-apparmor/snapd-apparmor $(DESTDIR)$(libexecdir) +endif diff --git a/cmd/appinfo.go b/cmd/appinfo.go new file mode 100644 index 00000000..5e60dc29 --- /dev/null +++ b/cmd/appinfo.go @@ -0,0 +1,133 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 ( + "fmt" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/systemd" +) + +func ClientAppInfoNotes(app *client.AppInfo) string { + if !app.IsService() { + return "-" + } + + var notes = make([]string, 0, 2) + var seenTimer, seenSocket bool + for _, act := range app.Activators { + switch act.Type { + case "timer": + seenTimer = true + case "socket": + seenSocket = true + } + } + if seenTimer { + notes = append(notes, "timer-activated") + } + if seenSocket { + notes = append(notes, "socket-activated") + } + if len(notes) == 0 { + return "-" + } + return strings.Join(notes, ",") +} + +func ClientAppInfosFromSnapAppInfos(apps []*snap.AppInfo) ([]client.AppInfo, error) { + // TODO: pass in an actual notifier here instead of null + // (Status doesn't _need_ it, but benefits from it) + sysd := systemd.New(dirs.GlobalRootDir, progress.Null) + + out := make([]client.AppInfo, 0, len(apps)) + for _, app := range apps { + appInfo := client.AppInfo{ + Snap: app.Snap.InstanceName(), + Name: app.Name, + CommonID: app.CommonID, + } + if fn := app.DesktopFile(); osutil.FileExists(fn) { + appInfo.DesktopFile = fn + } + + appInfo.Daemon = app.Daemon + if !app.IsService() || !app.Snap.IsActive() { + out = append(out, appInfo) + continue + } + + // collect all services for a single call to systemctl + serviceNames := make([]string, 0, 1+len(app.Sockets)+1) + serviceNames = append(serviceNames, app.ServiceName()) + + sockSvcFileToName := make(map[string]string, len(app.Sockets)) + for _, sock := range app.Sockets { + sockUnit := filepath.Base(sock.File()) + sockSvcFileToName[sockUnit] = sock.Name + serviceNames = append(serviceNames, sockUnit) + } + if app.Timer != nil { + timerUnit := filepath.Base(app.Timer.File()) + serviceNames = append(serviceNames, timerUnit) + } + + // sysd.Status() makes sure that we get only the units we asked + // for and raises an error otherwise + sts, err := sysd.Status(serviceNames...) + if err != nil { + return nil, fmt.Errorf("cannot get status of services of app %q: %v", app.Name, err) + } + if len(sts) != len(serviceNames) { + return nil, fmt.Errorf("cannot get status of services of app %q: expected %v results, got %v", app.Name, len(serviceNames), len(sts)) + } + for _, st := range sts { + switch filepath.Ext(st.UnitName) { + case ".service": + appInfo.Enabled = st.Enabled + appInfo.Active = st.Active + case ".timer": + appInfo.Activators = append(appInfo.Activators, client.AppActivator{ + Name: app.Name, + Enabled: st.Enabled, + Active: st.Active, + Type: "timer", + }) + case ".socket": + appInfo.Activators = append(appInfo.Activators, client.AppActivator{ + Name: sockSvcFileToName[st.UnitName], + Enabled: st.Enabled, + Active: st.Active, + Type: "socket", + }) + } + } + out = append(out, appInfo) + } + + return out, nil +} diff --git a/cmd/appinfo_test.go b/cmd/appinfo_test.go new file mode 100644 index 00000000..41bf9295 --- /dev/null +++ b/cmd/appinfo_test.go @@ -0,0 +1,71 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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_test + +import ( + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" +) + +func (*cmdSuite) TestAppStatusNotes(c *check.C) { + ai := client.AppInfo{} + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-") + + ai = client.AppInfo{ + Daemon: "oneshot", + } + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-") + + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "timer"}, + }, + } + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated") + + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "socket"}, + }, + } + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "socket-activated") + + // check that the output is stable regardless of the order of activators + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "timer"}, + {Type: "socket"}, + }, + } + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated") + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "socket"}, + {Type: "timer"}, + }, + } + c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated") +} diff --git a/cmd/autogen.sh b/cmd/autogen.sh new file mode 100755 index 00000000..5bd17968 --- /dev/null +++ b/cmd/autogen.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# Welcome to the Happy Maintainer's Utility Script +# +# Set BUILD_DIR to the directory where the build will happen, otherwise $PWD +# will be used +set -eux + +BUILD_DIR=${BUILD_DIR:-.} +selfdir=$(dirname "$0") +SRC_DIR=$(readlink -f "$selfdir") + +# We need the VERSION file to configure +if [ ! -e VERSION ]; then + ( cd .. && ./mkversion.sh ) +fi + +# Sanity check, are we in the right directory? +test -f configure.ac + +# Regenerate the build system +rm -f config.status +autoreconf -i -f + +# Configure the build +extra_opts= +# shellcheck disable=SC1091 +. /etc/os-release +case "$ID" in + arch) + extra_opts="--libexecdir=/usr/lib/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-apparmor --enable-nvidia-biarch --enable-merged-usr" + ;; + debian) + extra_opts="--libexecdir=/usr/lib/snapd" + ;; + ubuntu) + extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-multiarch --enable-static-libcap --enable-static-libapparmor --enable-static-libseccomp --with-host-arch-triplet=$(dpkg-architecture -qDEB_HOST_MULTIARCH)" + if [ "$(dpkg-architecture -qDEB_HOST_ARCH)" = "amd64" ]; then + extra_opts="$extra_opts --with-host-arch-32bit-triplet=$(dpkg-architecture -ai386 -qDEB_HOST_MULTIARCH)" + fi + ;; + fedora|centos|rhel) + extra_opts="--libexecdir=/usr/libexec/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-merged-usr --disable-apparmor" + ;; + opensuse|opensuse-tumbleweed) + extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-biarch --with-32bit-libdir=/usr/lib --enable-merged-usr" + ;; + solus) + extra_opts="--enable-nvidia-biarch" + ;; +esac + +echo "Configuring in build directory $BUILD_DIR with: $extra_opts" +mkdir -p "$BUILD_DIR" && cd "$BUILD_DIR" +# shellcheck disable=SC2086 +"${SRC_DIR}/configure" --enable-maintainer-mode --prefix=/usr $extra_opts "$@" diff --git a/cmd/cmd_linux.go b/cmd/cmd_linux.go new file mode 100644 index 00000000..60b6f182 --- /dev/null +++ b/cmd/cmd_linux.go @@ -0,0 +1,214 @@ +// -*- 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 ( + "bytes" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/snapcore/snapd/dirs" + "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 reExecKey = "SNAP_REEXEC" + +var ( + // snapdSnap is the place to look for the snapd snap; we will re-exec + // here + snapdSnap = "/snap/snapd/current" + + // coreSnap is the place to look for the core snap; we will re-exec + // here if there is no snapd snap + coreSnap = "/snap/core/current" + + // selfExe is the path to a symlink pointing to the current executable + selfExe = "/proc/self/exe" + + syscallExec = syscall.Exec + osReadlink = os.Readlink +) + +// distroSupportsReExec returns true if the distribution we are running on can use re-exec. +// +// This is true by default except for a "core/all" snap system where it makes +// no sense and in certain distributions that we don't want to enable re-exec +// yet because of missing validation or other issues. +func distroSupportsReExec() bool { + if !release.OnClassic { + return false + } + if !release.DistroLike("debian", "ubuntu") { + logger.Debugf("re-exec not supported on distro %q yet", release.ReleaseInfo.ID) + return false + } + return true +} + +// coreSupportsReExec returns true if the given core snap should be used as re-exec target. +// +// Ensure we do not use older version of snapd, look for info file and ignore +// version of core that do not yet have it. +func coreSupportsReExec(corePath string) bool { + fullInfo := filepath.Join(corePath, filepath.Join(dirs.CoreLibExecDir, "info")) + content, err := ioutil.ReadFile(fullInfo) + if err != nil { + if !os.IsNotExist(err) { + logger.Noticef("cannot open snapd info file %q: %s", fullInfo, err) + } + return false + } + + if !bytes.HasPrefix(content, []byte("VERSION=")) { + idx := bytes.Index(content, []byte("\nVERSION=")) + if idx < 0 { + logger.Noticef("cannot find snapd version information in %q", content) + return false + } + content = content[idx+1:] + } + content = content[8:] + idx := bytes.IndexByte(content, '\n') + if idx > -1 { + content = content[:idx] + } + ver := string(content) + // > 0 means our Version is bigger than the version of snapd in core + res, err := strutil.VersionCompare(Version, ver) + if err != nil { + logger.Debugf("cannot version compare %q and %q: %v", Version, ver, err) + return false + } + if res > 0 { + logger.Debugf("core snap (at %q) is older (%q) than distribution package (%q)", corePath, ver, Version) + return false + } + return true +} + +// InternalToolPath returns the path of an internal snapd tool. The tool +// *must* be located inside /usr/lib/snapd/. +// +// The return value is either the path of the tool in the current distribution +// or in the core snap (or the ubuntu-core snap). This handles spiritual +// "re-exec" where we run the tool from the core snap if the environment allows +// us to do so. +func InternalToolPath(tool string) string { + distroTool := filepath.Join(dirs.DistroLibExecDir, tool) + + // find the internal path relative to the running snapd, this + // ensure we don't rely on the state of the system (like + // having a valid "current" symlink). + exe, err := osReadlink("/proc/self/exe") + if err != nil { + logger.Noticef("cannot read /proc/self/exe: %v, using tool outside core", err) + return distroTool + } + + // ensure we never use this helper from anything but + if !strings.HasSuffix(exe, "/snapd") && !strings.HasSuffix(exe, ".test") { + log.Panicf("InternalToolPath can only be used from snapd, got: %s", exe) + } + + if !strings.HasPrefix(exe, dirs.SnapMountDir) { + logger.Debugf("exe doesn't have snap mount dir prefix: %q vs %q", exe, dirs.SnapMountDir) + return distroTool + } + + // if we are re-execed, then the tool is at the same location + // as snapd + return filepath.Join(filepath.Dir(exe), tool) +} + +// mustUnsetenv will unset the given environment key or panic if it +// cannot do that +func mustUnsetenv(key string) { + if err := os.Unsetenv(key); err != nil { + log.Panicf("cannot unset %s: %s", key, err) + } +} + +// ExecInSnapdOrCoreSnap makes sure you're executing the binary that ships in +// the snapd/core snap. +func ExecInSnapdOrCoreSnap() { + // Which executable are we? + exe, err := os.Readlink(selfExe) + if err != nil { + logger.Noticef("cannot read /proc/self/exe: %v", err) + return + } + + // Special case for snapd re-execing from 2.21. In this + // version of snap/snapd we did set SNAP_REEXEC=0 when we + // re-execed. In this case we need to unset the reExecKey to + // ensure that subsequent run of snap/snapd (e.g. when using + // classic confinement) will *not* prevented from re-execing. + if strings.HasPrefix(exe, dirs.SnapMountDir) && !osutil.GetenvBool(reExecKey, true) { + mustUnsetenv(reExecKey) + return + } + + // If we are asked not to re-execute use distribution packages. This is + // "spiritual" re-exec so use the same environment variable to decide. + if !osutil.GetenvBool(reExecKey, true) { + logger.Debugf("re-exec disabled by user") + return + } + + // Did we already re-exec? + if strings.HasPrefix(exe, dirs.SnapMountDir) { + return + } + + // If the distribution doesn't support re-exec or run-from-core then don't do it. + if !distroSupportsReExec() { + return + } + + // Is this executable in the core snap too? + corePath := snapdSnap + full := filepath.Join(snapdSnap, exe) + if !osutil.FileExists(full) { + corePath = coreSnap + full = filepath.Join(coreSnap, exe) + if !osutil.FileExists(full) { + return + } + } + + // If the core snap doesn't support re-exec or run-from-core then don't do it. + if !coreSupportsReExec(corePath) { + return + } + + logger.Debugf("restarting into %q", full) + panic(syscallExec(full, os.Args, os.Environ())) +} diff --git a/cmd/cmd_linux_test.go b/cmd/cmd_linux_test.go new file mode 100644 index 00000000..2e7d846b --- /dev/null +++ b/cmd/cmd_linux_test.go @@ -0,0 +1,117 @@ +package cmd + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/snapcore/snapd/dirs" +) + +const dataOK = `one line +another line +yadda yadda +VERSION=42 +potatoes +` + +const dataNOK = `a line +another +this is a very long line +that wasn't long what are you talking about long lines are like, so long you need to add things like commas to them for them to even make sense +a short one +and another +what is this +why +no +stop +` + +const dataHuge = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Quisque euismod ac elit ac auctor. +Proin malesuada diam ac tellus maximus aliquam. +Aenean tincidunt mi et tortor bibendum fringilla. +Phasellus finibus, urna id convallis vestibulum, metus metus venenatis massa, et efficitur nisi elit in massa. +Mauris at nisl leo. +Nulla ullamcorper risus venenatis massa venenatis, ac finibus lacus aliquam. +Nunc tempor convallis cursus. +Maecenas id rhoncus orci, eget pretium eros. + +Donec et consectetur lacus. +Nam nec mattis elit, id sollicitudin magna. +Aenean sit amet diam vitae tellus finibus tristique. +Duis et pharetra tortor, id pharetra erat. +Suspendisse commodo venenatis blandit. +Morbi tellus est, iaculis et tincidunt nec, semper ut ipsum. +Mauris quis condimentum risus. +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Mauris gravida turpis ut urna laoreet, sit amet tempor odio porttitor. + +Aliquam nibh libero, venenatis ac vehicula at, blandit id odio. +Etiam malesuada consectetur porta. +Fusce consectetur ligula et metus interdum sollicitudin. +Pellentesque odio neque, pharetra et gravida non, vestibulum nec lorem. +Sed condimentum velit ex, sit amet viverra lectus aliquet quis. +Aliquam tincidunt eu elit at condimentum. +Donec feugiat urna tortor, pellentesque tincidunt quam congue eu. + +Phasellus vel libero molestie, semper erat at, suscipit nisi. +Nullam euismod neque ut turpis molestie, eu fringilla elit volutpat. +Phasellus maximus, urna eget porta congue, diam enim volutpat diam, nec ultrices lorem risus ac metus. +Vivamus convallis eros non nunc pretium bibendum. +Maecenas consectetur metus metus. +Morbi scelerisque urna at arcu tristique feugiat. +Vestibulum condimentum odio sed tortor vulputate, eget hendrerit mi consequat. +Integer egestas finibus augue, ac scelerisque ex pretium aliquam. +Aliquam erat volutpat. +Suspendisse a nulla ultrices, porttitor tellus ut, bibendum diam. +In nibh dui, tempus eget vestibulum in, euismod in ex. +In tempus felis lectus. + +Maecenas suscipit turpis eget velit molestie, quis luctus nibh placerat. +Nulla semper eleifend nisi ut dignissim. +Donec eu massa maximus, blandit massa ac, lobortis risus. +Donec id condimentum libero, vel fringilla diam. +Praesent ultrices, ante congue sollicitudin sagittis, orci ex maximus ipsum, at convallis nunc nisl nec lorem. +Duis iaculis finibus fermentum. +Curabitur quis pharetra metus. +Donec nisl ipsum, faucibus vitae odio sed, mattis feugiat nisl. +Pellentesque nec justo in magna volutpat accumsan. +Pellentesque porttitor justo non velit porta rhoncus. +Nulla ut lectus quis lectus rutrum dignissim. +Pellentesque posuere sagittis felis, quis varius purus pharetra eu. +Nam blandit diam ullamcorper, auctor massa at, aliquet dui. +Aliquam erat volutpat. +Nullam sit amet augue nec diam sollicitudin ullamcorper a vitae neque. +VERSION=42 +` + +func benchmarkCSRE(b *testing.B, data string) { + tempdir, err := ioutil.TempDir("", "") + if err != nil { + b.Fatalf("tempdir: %v", err) + } + defer os.RemoveAll(tempdir) + if err = os.MkdirAll(filepath.Join(tempdir, dirs.CoreLibExecDir), 0755); err != nil { + b.Fatalf("mkdirall: %v", err) + } + + if err = ioutil.WriteFile(filepath.Join(tempdir, dirs.CoreLibExecDir, "info"), []byte(data), 0600); err != nil { + b.Fatalf("%v", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + coreSupportsReExec(tempdir) + } +} + +func BenchmarkCSRE_fakeOK(b *testing.B) { benchmarkCSRE(b, dataOK) } +func BenchmarkCSRE_fakeNOK(b *testing.B) { benchmarkCSRE(b, dataNOK) } +func BenchmarkCSRE_fakeHuge(b *testing.B) { benchmarkCSRE(b, dataHuge) } + +func BenchmarkCSRE_real(b *testing.B) { + for i := 0; i < b.N; i++ { + coreSupportsReExec("/snap/core/current") + } +} diff --git a/cmd/cmd_other.go b/cmd/cmd_other.go new file mode 100644 index 00000000..64c4f441 --- /dev/null +++ b/cmd/cmd_other.go @@ -0,0 +1,28 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build !linux + +/* + * Copyright (C) 2018 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 + +// ExecInSnapdOrCoreSnap makes sure you're executing the binary that ships in +// the snapd/core snap. +// On this OS this is a stub. +func ExecInSnapdOrCoreSnap() { + return +} diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 00000000..c73dcfae --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,307 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package cmd_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/release" +) + +func Test(t *testing.T) { TestingT(t) } + +type cmdSuite struct { + restoreExec func() + restoreLogger func() + execCalled int + lastExecArgv0 string + lastExecArgv []string + lastExecEnvv []string + fakeroot string + snapdPath string + corePath string +} + +var _ = Suite(&cmdSuite{}) + +func (s *cmdSuite) SetUpTest(c *C) { + s.restoreExec = cmd.MockSyscallExec(s.syscallExec) + _, s.restoreLogger = logger.MockLogger() + s.execCalled = 0 + s.lastExecArgv0 = "" + s.lastExecArgv = nil + s.lastExecEnvv = nil + s.fakeroot = c.MkDir() + dirs.SetRootDir(s.fakeroot) + s.snapdPath = filepath.Join(dirs.SnapMountDir, "/snapd/42") + s.corePath = filepath.Join(dirs.SnapMountDir, "/core/21") + c.Assert(os.MkdirAll(filepath.Join(s.fakeroot, "proc/self"), 0755), IsNil) +} + +func (s *cmdSuite) TearDownTest(c *C) { + s.restoreExec() + s.restoreLogger() +} + +func (s *cmdSuite) syscallExec(argv0 string, argv []string, envv []string) (err error) { + s.execCalled++ + s.lastExecArgv0 = argv0 + s.lastExecArgv = argv + s.lastExecEnvv = envv + return fmt.Errorf(">exec of %q in tests<", argv0) +} + +func (s *cmdSuite) fakeCoreVersion(c *C, coreDir, version string) { + p := filepath.Join(coreDir, "/usr/lib/snapd") + c.Assert(os.MkdirAll(p, 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(p, "info"), []byte("VERSION="+version), 0644), IsNil) +} + +func (s *cmdSuite) fakeInternalTool(c *C, coreDir, toolName string) string { + s.fakeCoreVersion(c, coreDir, "42") + p := filepath.Join(coreDir, "/usr/lib/snapd", toolName) + c.Assert(ioutil.WriteFile(p, nil, 0755), IsNil) + + return p +} + +func (s *cmdSuite) mockReExecingEnv() func() { + restore := []func(){ + release.MockOnClassic(true), + release.MockReleaseInfo(&release.OS{ID: "ubuntu"}), + cmd.MockCoreSnapdPaths(s.corePath, s.snapdPath), + cmd.MockVersion("2"), + } + + return func() { + for i := len(restore) - 1; i >= 0; i-- { + restore[i]() + } + } +} + +func (s *cmdSuite) mockReExecFor(c *C, coreDir, toolName string) func() { + selfExe := filepath.Join(s.fakeroot, "proc/self/exe") + restore := []func(){ + s.mockReExecingEnv(), + cmd.MockSelfExe(selfExe), + } + s.fakeInternalTool(c, coreDir, toolName) + c.Assert(os.Symlink(filepath.Join("/usr/lib/snapd", toolName), selfExe), IsNil) + + return func() { + for i := len(restore) - 1; i >= 0; i-- { + restore[i]() + } + } +} + +func (s *cmdSuite) TestDistroSupportsReExec(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + // Some distributions don't support re-execution yet. + for _, id := range []string{"fedora", "centos", "rhel", "opensuse", "suse", "poky"} { + restore = release.MockReleaseInfo(&release.OS{ID: id}) + defer restore() + c.Check(cmd.DistroSupportsReExec(), Equals, false, Commentf("ID: %q", id)) + } + + // While others do. + for _, id := range []string{"debian", "ubuntu"} { + restore = release.MockReleaseInfo(&release.OS{ID: id}) + defer restore() + c.Check(cmd.DistroSupportsReExec(), Equals, true, Commentf("ID: %q", id)) + } +} + +func (s *cmdSuite) TestNonClassicDistroNoSupportsReExec(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + // no distro supports re-exec when not on classic :-) + for _, id := range []string{ + "fedora", "centos", "rhel", "opensuse", "suse", "poky", + "debian", "ubuntu", "arch", "archlinux", + } { + restore = release.MockReleaseInfo(&release.OS{ID: id}) + defer restore() + c.Check(cmd.DistroSupportsReExec(), Equals, false, Commentf("ID: %q", id)) + } +} + +func (s *cmdSuite) TestCoreSupportsReExecNoInfo(c *C) { + // there's no snapd/info in a just-created tmpdir :-p + c.Check(cmd.CoreSupportsReExec(c.MkDir()), Equals, false) +} + +func (s *cmdSuite) TestCoreSupportsReExecBadInfo(c *C) { + // can't read snapd/info if it's a directory + p := s.snapdPath + "/usr/lib/snapd/info" + c.Assert(os.MkdirAll(p, 0755), IsNil) + + c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) +} + +func (s *cmdSuite) TestCoreSupportsReExecBadInfoContent(c *C) { + // can't understand snapd/info if all it holds are potatoes + p := s.snapdPath + "/usr/lib/snapd" + c.Assert(os.MkdirAll(p, 0755), IsNil) + c.Assert(ioutil.WriteFile(p+"/info", []byte("potatoes"), 0644), IsNil) + + c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) +} + +func (s *cmdSuite) TestCoreSupportsReExecBadVersion(c *C) { + // can't understand snapd/info if all its version is gibberish + s.fakeCoreVersion(c, s.snapdPath, "0:") + + c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) +} + +func (s *cmdSuite) TestCoreSupportsReExecOldVersion(c *C) { + // can't re-exec if core version is too old + defer cmd.MockVersion("2")() + s.fakeCoreVersion(c, s.snapdPath, "0") + + c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) +} + +func (s *cmdSuite) TestCoreSupportsReExec(c *C) { + defer cmd.MockVersion("2")() + s.fakeCoreVersion(c, s.snapdPath, "9999") + + c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, true) +} + +func (s *cmdSuite) TestInternalToolPathNoReexec(c *C) { + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.DistroLibExecDir, "snapd"), nil + }) + defer restore() + + c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.DistroLibExecDir, "potato")) +} + +func (s *cmdSuite) TestInternalToolPathWithReexec(c *C) { + s.fakeInternalTool(c, s.snapdPath, "potato") + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join(s.snapdPath, "/usr/lib/snapd/snapd"), nil + }) + defer restore() + + c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.SnapMountDir, "snapd/42/usr/lib/snapd/potato")) +} + +func (s *cmdSuite) TestInternalToolPathFromIncorrectHelper(c *C) { + restore := cmd.MockOsReadlink(func(string) (string, error) { + return "/usr/bin/potato", nil + }) + defer restore() + + c.Check(func() { cmd.InternalToolPath("potato") }, PanicMatches, "InternalToolPath can only be used from snapd, got: /usr/bin/potato") +} + +func (s *cmdSuite) TestExecInSnapdOrCoreSnap(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() + + c.Check(cmd.ExecInSnapdOrCoreSnap, PanicMatches, `>exec of "[^"]+/potato" in tests<`) + c.Check(s.execCalled, Equals, 1) + c.Check(s.lastExecArgv0, Equals, filepath.Join(s.snapdPath, "/usr/lib/snapd/potato")) + c.Check(s.lastExecArgv, DeepEquals, os.Args) +} + +func (s *cmdSuite) TestExecInOldCoreSnap(c *C) { + defer s.mockReExecFor(c, s.corePath, "potato")() + + c.Check(cmd.ExecInSnapdOrCoreSnap, PanicMatches, `>exec of "[^"]+/potato" in tests<`) + c.Check(s.execCalled, Equals, 1) + c.Check(s.lastExecArgv0, Equals, filepath.Join(s.corePath, "/usr/lib/snapd/potato")) + c.Check(s.lastExecArgv, DeepEquals, os.Args) +} + +func (s *cmdSuite) TestExecInSnapdOrCoreSnapBailsNoCoreSupport(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() + + // no "info" -> no core support: + c.Assert(os.Remove(filepath.Join(s.snapdPath, "/usr/lib/snapd/info")), IsNil) + + cmd.ExecInSnapdOrCoreSnap() + c.Check(s.execCalled, Equals, 0) +} + +func (s *cmdSuite) TestExecInSnapdOrCoreSnapMissingExe(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() + + // missing exe: + c.Assert(os.Remove(filepath.Join(s.snapdPath, "/usr/lib/snapd/potato")), IsNil) + + cmd.ExecInSnapdOrCoreSnap() + c.Check(s.execCalled, Equals, 0) +} + +func (s *cmdSuite) TestExecInSnapdOrCoreSnapBadSelfExe(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() + + // missing self/exe: + c.Assert(os.Remove(filepath.Join(s.fakeroot, "proc/self/exe")), IsNil) + + cmd.ExecInSnapdOrCoreSnap() + c.Check(s.execCalled, Equals, 0) +} + +func (s *cmdSuite) TestExecInSnapdOrCoreSnapBailsNoDistroSupport(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() + + // no distro support: + defer release.MockOnClassic(false)() + + cmd.ExecInSnapdOrCoreSnap() + c.Check(s.execCalled, Equals, 0) +} + +func (s *cmdSuite) TestExecInSnapdOrCoreSnapNoDouble(c *C) { + selfExe := filepath.Join(s.fakeroot, "proc/self/exe") + err := os.Symlink(filepath.Join(s.fakeroot, "/snap/core/42/usr/lib/snapd"), selfExe) + c.Assert(err, IsNil) + cmd.MockSelfExe(selfExe) + + cmd.ExecInSnapdOrCoreSnap() + c.Check(s.execCalled, Equals, 0) +} + +func (s *cmdSuite) TestExecInSnapdOrCoreSnapDisabled(c *C) { + defer s.mockReExecFor(c, s.snapdPath, "potato")() + + os.Setenv("SNAP_REEXEC", "0") + defer os.Unsetenv("SNAP_REEXEC") + + cmd.ExecInSnapdOrCoreSnap() + c.Check(s.execCalled, Equals, 0) +} diff --git a/cmd/configure.ac b/cmd/configure.ac new file mode 100644 index 00000000..e3aa0265 --- /dev/null +++ b/cmd/configure.ac @@ -0,0 +1,240 @@ +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 subdir-objects]) +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. + +# check for large file support +AC_SYS_LARGEFILE + +# Checks for header files. +AC_CHECK_HEADERS([fcntl.h limits.h stdlib.h string.h sys/mount.h unistd.h]) +AC_CHECK_HEADERS([sys/quota.h], [], [AC_MSG_ERROR(sys/quota.h unavailable)]) +AC_CHECK_HEADERS([xfs/xqm.h], [], [AC_MSG_ERROR(xfs/xqm.h unavailable)], +[[#define _GNU_SOURCE +#define _FILE_OFFSET_BITS 64 +#include +]]) + +# 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]) + +# Check if libcap is available. +# PKG_CHECK_MODULES([LIBCAP], [libcap]) + +# Enable special support for hosts with proprietary nvidia drivers on Ubuntu. +AC_ARG_ENABLE([nvidia-multiarch], + AS_HELP_STRING([--enable-nvidia-multiarch], [Support for proprietary nvidia drivers (Ubuntu/Debian)]), + [case "${enableval}" in + yes) enable_nvidia_multiarch=yes ;; + no) enable_nvidia_multiarch=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-nvidia-multiarch]) + esac], [enable_nvidia_multiarch=no]) +AM_CONDITIONAL([NVIDIA_MULTIARCH], [test "x$enable_nvidia_multiarch" = "xyes"]) + +AS_IF([test "x$enable_nvidia_multiarch" = "xyes"], [ + AC_DEFINE([NVIDIA_MULTIARCH], [1], + [Support for proprietary nvidia drivers (Ubuntu/Debian)])]) + +# Enable special support for hosts with proprietary nvidia drivers on Arch. +AC_ARG_ENABLE([nvidia-biarch], + AS_HELP_STRING([--enable-nvidia-biarch], [Support for proprietary nvidia drivers (bi-arch distributions)]), + [case "${enableval}" in + yes) enable_nvidia_biarch=yes ;; + no) enable_nvidia_biarch=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-nvidia-biarch]) + esac], [enable_nvidia_biarch=no]) +AM_CONDITIONAL([NVIDIA_BIARCH], [test "x$enable_nvidia_biarch" = "xyes"]) + +AS_IF([test "x$enable_nvidia_biarch" = "xyes"], [ + AC_DEFINE([NVIDIA_BIARCH], [1], + [Support for proprietary nvidia drivers (bi-arch distributions)])]) + +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_PROGS([HAVE_RST2MAN],[rst2man rst2man.py]) +AS_IF([test "x$HAVE_RST2MAN" = "x"], [AC_MSG_WARN(["cannot find the rst2man tool, install python-docutils or similar"])]) +AM_CONDITIONAL([HAVE_RST2MAN], [test "x${HAVE_RST2MAN}" != "x"]) + +AC_PATH_PROG([HAVE_VALGRIND],[valgrind]) +AM_CONDITIONAL([HAVE_VALGRIND], [test "x${HAVE_VALGRIND}" != "x"]) +AS_IF([test "x$HAVE_VALGRIND" = "x"], [AC_MSG_WARN(["cannot find the valgrind tool, will not run unit tests through valgrind"])]) + +AC_ARG_ENABLE([static-libcap], + AS_HELP_STRING([--enable-static-libcap], [Link libcap statically]), + [case "${enableval}" in + yes) enable_static_libcap=yes ;; + no) enable_static_libcap=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libcap]) + esac], [enable_static_libcap=no]) +AM_CONDITIONAL([STATIC_LIBCAP], [test "x$enable_static_libcap" = "xyes"]) + +AC_ARG_ENABLE([static-libapparmor], + AS_HELP_STRING([--enable-static-libapparmor], [Link libapparmor statically]), + [case "${enableval}" in + yes) enable_static_libapparmor=yes ;; + no) enable_static_libapparmor=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libapparmor]) + esac], [enable_static_libapparmor=no]) +AM_CONDITIONAL([STATIC_LIBAPPARMOR], [test "x$enable_static_libapparmor" = "xyes"]) + +AC_ARG_ENABLE([static-libseccomp], + AS_HELP_STRING([--enable-static-libseccomp], [Link libseccomp statically]), + [case "${enableval}" in + yes) enable_static_libseccomp=yes ;; + no) enable_static_libseccomp=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libseccomp]) + esac], [enable_static_libseccomp=no]) +AM_CONDITIONAL([STATIC_LIBSECCOMP], [test "x$enable_static_libseccomp" = "xyes"]) + +LIB32_DIR="${prefix}/lib32" +AC_ARG_WITH([32bit-libdir], + AS_HELP_STRING([--with-32bit-libdir=DIR], [Use an alternate lib32 directory]), + [LIB32_DIR="$withval"]) +AC_SUBST(LIB32_DIR) +AC_DEFINE_UNQUOTED([LIB32_DIR], "${LIB32_DIR}", [Location of the lib32 directory]) + +AC_ARG_WITH([host-arch-triplet], + AS_HELP_STRING([--with-host-arch-triplet=triplet], [Arch triplet for host libraries]), + [HOST_ARCH_TRIPLET="$withval"]) +AC_SUBST(HOST_ARCH_TRIPLET) +AC_DEFINE_UNQUOTED([HOST_ARCH_TRIPLET], "${HOST_ARCH_TRIPLET}", [Arch triplet for host libraries]) + +AC_ARG_WITH([host-arch-32bit-triplet], + AS_HELP_STRING([--with-host-arch-32bit-triplet=triplet], [Arch triplet for 32bit libraries]), + [HOST_ARCH32_TRIPLET="$withval"]) +AC_SUBST(HOST_ARCH32_TRIPLET) +AC_DEFINE_UNQUOTED([HOST_ARCH32_TRIPLET], "${HOST_ARCH32_TRIPLET}", [Arch triplet for 32bit libraries]) + +SYSTEMD_SYSTEM_GENERATOR_DIR="$($PKG_CONFIG --variable=systemdsystemgeneratordir systemd)" +AS_IF([test "x$SYSTEMD_SYSTEM_GENERATOR_DIR" = "x"], [SYSTEMD_SYSTEM_GENERATOR_DIR=/lib/systemd/system-generators]) +AC_SUBST(SYSTEMD_SYSTEM_GENERATOR_DIR) + +# FIXME: get this via something like pkgconf once it is defined there +SYSTEMD_SYSTEM_ENV_GENERATOR_DIR="${prefix}/lib/systemd/system-environment-generators" +AC_SUBST(SYSTEMD_SYSTEM_ENV_GENERATOR_DIR) + +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT diff --git a/cmd/decode-mount-opts/decode-mount-opts.c b/cmd/decode-mount-opts/decode-mount-opts.c new file mode 100644 index 00000000..383aa03b --- /dev/null +++ b/cmd/decode-mount-opts/decode-mount-opts.c @@ -0,0 +1,38 @@ +/* + * 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 "../libsnap-confine-private/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; + } + char buf[1000] = {0}; + printf("%#lx is %s\n", mountflags, sc_mount_opt2str(buf, sizeof buf, mountflags)); + return 0; +} diff --git a/cmd/export_test.go b/cmd/export_test.go new file mode 100644 index 00000000..cd66ca55 --- /dev/null +++ b/cmd/export_test.go @@ -0,0 +1,60 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package cmd + +var ( + DistroSupportsReExec = distroSupportsReExec + CoreSupportsReExec = coreSupportsReExec +) + +func MockCoreSnapdPaths(newCoreSnap, newSnapdSnap string) func() { + oldOldCore := coreSnap + oldNewCore := snapdSnap + snapdSnap = newSnapdSnap + coreSnap = newCoreSnap + return func() { + snapdSnap = oldNewCore + coreSnap = oldOldCore + } +} + +func MockSelfExe(newSelfExe string) func() { + oldSelfExe := selfExe + selfExe = newSelfExe + return func() { + selfExe = oldSelfExe + } +} + +func MockSyscallExec(f func(argv0 string, argv []string, envv []string) (err error)) func() { + oldSyscallExec := syscallExec + syscallExec = f + return func() { + syscallExec = oldSyscallExec + } +} + +func MockOsReadlink(f func(string) (string, error)) func() { + realOsReadlink := osReadlink + osReadlink = f + return func() { + osReadlink = realOsReadlink + } +} diff --git a/cmd/libsnap-confine-private/apparmor-support.c b/cmd/libsnap-confine-private/apparmor-support.c new file mode 100644 index 00000000..eac0912d --- /dev/null +++ b/cmd/libsnap-confine-private/apparmor-support.c @@ -0,0 +1,141 @@ +/* + * 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 "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/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"); + break; + 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 SC_CLEANUP(sc_cleanup_string) = NULL; + char *mode = NULL; + if (aa_getcon(&label, &mode) < 0) { + die("cannot query current apparmor profile"); + } + debug("apparmor label on snap-confine is: %s", label); + debug("apparmor mode is: %s", mode); + // 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 + if (apparmor->mode == SC_AA_NOT_APPLICABLE) { + return; + } + 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->mode == SC_AA_NOT_APPLICABLE) { + return; + } + 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/libsnap-confine-private/apparmor-support.h b/cmd/libsnap-confine-private/apparmor-support.h new file mode 100644 index 00000000..b90f285c --- /dev/null +++ b/cmd/libsnap-confine-private/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/libsnap-confine-private/cgroup-freezer-support.c b/cmd/libsnap-confine-private/cgroup-freezer-support.c new file mode 100644 index 00000000..a5cfa047 --- /dev/null +++ b/cmd/libsnap-confine-private/cgroup-freezer-support.c @@ -0,0 +1,149 @@ +// For AT_EMPTY_PATH and O_PATH +#define _GNU_SOURCE + +#include "cgroup-freezer-support.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "cleanup-funcs.h" +#include "string-utils.h" +#include "utils.h" + +static const char *freezer_cgroup_dir = "/sys/fs/cgroup/freezer"; + +void sc_cgroup_freezer_join(const char *snap_name, pid_t pid) +{ + // Format the name of the cgroup hierarchy. + char buf[PATH_MAX] = { 0 }; + sc_must_snprintf(buf, sizeof buf, "snap.%s", snap_name); + + // Open the freezer cgroup directory. + int cgroup_fd SC_CLEANUP(sc_cleanup_close) = -1; + cgroup_fd = open(freezer_cgroup_dir, + O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (cgroup_fd < 0) { + die("cannot open freezer cgroup (%s)", freezer_cgroup_dir); + } + // Create the freezer hierarchy for the given snap. + if (mkdirat(cgroup_fd, buf, 0755) < 0 && errno != EEXIST) { + die("cannot create freezer cgroup hierarchy for snap %s", + snap_name); + } + // Open the hierarchy directory for the given snap. + int hierarchy_fd SC_CLEANUP(sc_cleanup_close) = -1; + hierarchy_fd = openat(cgroup_fd, buf, + O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (hierarchy_fd < 0) { + die("cannot open freezer cgroup hierarchy for snap %s", + snap_name); + } + // Since we may be running from a setuid but not setgid executable, ensure + // that the group and owner of the hierarchy directory is root.root. + if (fchownat(hierarchy_fd, "", 0, 0, AT_EMPTY_PATH) < 0) { + die("cannot change owner of freezer cgroup hierarchy for snap %s to root.root", snap_name); + } + // Open the tasks file. + int tasks_fd SC_CLEANUP(sc_cleanup_close) = -1; + tasks_fd = openat(hierarchy_fd, "tasks", + O_WRONLY | O_NOFOLLOW | O_CLOEXEC); + if (tasks_fd < 0) { + die("cannot open tasks file for freezer cgroup hierarchy for snap %s", snap_name); + } + // Write the process (task) number to the tasks file. Linux task IDs are + // limited to 2^29 so a long int is enough to represent it. + // See include/linux/threads.h in the kernel source tree for details. + int n = sc_must_snprintf(buf, sizeof buf, "%ld", (long)pid); + if (write(tasks_fd, buf, n) < n) { + die("cannot move process %ld to freezer cgroup hierarchy for snap %s", (long)pid, snap_name); + } + debug("moved process %ld to freezer cgroup hierarchy for snap %s", + (long)pid, snap_name); +} + +bool sc_cgroup_freezer_occupied(const char *snap_name) +{ + // Format the name of the cgroup hierarchy. + char buf[PATH_MAX] = { 0 }; + sc_must_snprintf(buf, sizeof buf, "snap.%s", snap_name); + + // Open the freezer cgroup directory. + int cgroup_fd SC_CLEANUP(sc_cleanup_close) = -1; + cgroup_fd = open(freezer_cgroup_dir, + O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (cgroup_fd < 0) { + die("cannot open freezer cgroup (%s)", freezer_cgroup_dir); + } + // Open the proc directory. + int proc_fd SC_CLEANUP(sc_cleanup_close) = -1; + proc_fd = open("/proc", O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (proc_fd < 0) { + die("cannot open /proc"); + } + // Open the hierarchy directory for the given snap. + int hierarchy_fd SC_CLEANUP(sc_cleanup_close) = -1; + hierarchy_fd = openat(cgroup_fd, buf, + O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (hierarchy_fd < 0) { + if (errno == ENOENT) { + return false; + } + die("cannot open freezer cgroup hierarchy for snap %s", + snap_name); + } + // Open the "cgroup.procs" file. Alternatively we could open the "tasks" + // file and see per-thread data but we don't need that. + int cgroup_procs_fd SC_CLEANUP(sc_cleanup_close) = -1; + cgroup_procs_fd = openat(hierarchy_fd, "cgroup.procs", + O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (cgroup_procs_fd < 0) { + die("cannot open cgroup.procs file for freezer cgroup hierarchy for snap %s", snap_name); + } + + FILE *cgroup_procs SC_CLEANUP(sc_cleanup_file) = NULL; + cgroup_procs = fdopen(cgroup_procs_fd, "r"); + if (cgroup_procs == NULL) { + die("cannot convert tasks file descriptor to FILE"); + } + cgroup_procs_fd = -1; // cgroup_procs_fd will now be closed by fclose. + + char *line_buf SC_CLEANUP(sc_cleanup_string) = NULL; + size_t line_buf_size = 0; + ssize_t num_read; + struct stat statbuf; + do { + num_read = getline(&line_buf, &line_buf_size, cgroup_procs); + if (num_read < 0 && errno != 0) { + die("cannot read next PID belonging to snap %s", + snap_name); + } + if (num_read <= 0) { + break; + } else { + if (line_buf[num_read - 1] == '\n') { + line_buf[num_read - 1] = '\0'; + } else { + die("could not find newline in cgroup.procs"); + } + } + debug("found process id: %s\n", line_buf); + + if (fstatat(proc_fd, line_buf, &statbuf, AT_SYMLINK_NOFOLLOW) < + 0) { + // The process may have died already. + if (errno != ENOENT) { + die("cannot stat /proc/%s", line_buf); + } + } + debug("found process %s belonging to user %d", + line_buf, statbuf.st_uid); + return true; + } while (num_read > 0); + + return false; +} diff --git a/cmd/libsnap-confine-private/cgroup-freezer-support.h b/cmd/libsnap-confine-private/cgroup-freezer-support.h new file mode 100644 index 00000000..259dca6d --- /dev/null +++ b/cmd/libsnap-confine-private/cgroup-freezer-support.h @@ -0,0 +1,35 @@ +#ifndef SC_CGROUP_FREEZER_SUPPORT_H +#define SC_CGROUP_FREEZER_SUPPORT_H + +#include +#include "error.h" + +/** + * Join the freezer cgroup for the given snap. + * + * This function adds the specified task to the freezer cgroup specific to the + * given snap. The name of the cgroup is "snap.$snap_name". + * + * Interestingly we don't need to actually freeze the processes. The group + * allows us to track processes belonging to a given snap. This makes the + * measurement "are any processes of this snap still alive" very simple. + * + * The "tasks" file belonging to the cgroup contains the set of all the + * processes that originate from the given snap. Examining that file one can + * reliably determine if the set is empty or not. + * + * For more details please review: + * https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt +**/ +void sc_cgroup_freezer_join(const char *snap_name, pid_t pid); + +/** + * Check if a freezer cgroup for given snap has any processes belonging to a given user. + * + * This function examines the freezer cgroup called "snap.$snap_name" and looks + * at each of its processes. If any process exists then the function returns true. +**/ +// TODO: Support per user filtering for eventual per-user mount namespaces +bool sc_cgroup_freezer_occupied(const char *snap_name); + +#endif diff --git a/cmd/libsnap-confine-private/classic-test.c b/cmd/libsnap-confine-private/classic-test.c new file mode 100644 index 00000000..cf6e5bcc --- /dev/null +++ b/cmd/libsnap-confine-private/classic-test.c @@ -0,0 +1,204 @@ +/* + * Copyright (C) 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 "classic.h" +#include "classic.c" + +#include + +/* restore_os_release is an internal helper for mock_os_release */ +static void restore_os_release(gpointer * old) +{ + unlink(os_release); + os_release = (const char *)old; +} + +/* mock_os_release replaces the presence and contents of /etc/os-release + as seen by classic.c. The mocked value may be NULL to have the code refer + to an absent file. */ +static void mock_os_release(const char *mocked) +{ + const char *old = os_release; + if (mocked != NULL) { + os_release = "os-release.test"; + g_file_set_contents(os_release, mocked, -1, NULL); + } else { + os_release = "os-release.missing"; + } + g_test_queue_destroy((GDestroyNotify) restore_os_release, + (gpointer) old); +} + +/* restore_meta_snap_yaml is an internal helper for mock_meta_snap_yaml */ +static void restore_meta_snap_yaml(gpointer * old) +{ + unlink(meta_snap_yaml); + meta_snap_yaml = (const char *)old; +} + +/* mock_meta_snap_yaml replaces the presence and contents of /meta/snap.yaml + as seen by classic.c. The mocked value may be NULL to have the code refer + to an absent file. */ +static void mock_meta_snap_yaml(const char *mocked) +{ + const char *old = meta_snap_yaml; + if (mocked != NULL) { + meta_snap_yaml = "snap-yaml.test"; + g_file_set_contents(meta_snap_yaml, mocked, -1, NULL); + } else { + meta_snap_yaml = "snap-yaml.missing"; + } + g_test_queue_destroy((GDestroyNotify) restore_meta_snap_yaml, + (gpointer) old); +} + +static const char *os_release_classic = "" + "NAME=\"Ubuntu\"\n" + "VERSION=\"17.04 (Zesty Zapus)\"\n" "ID=ubuntu\n" "ID_LIKE=debian\n"; + +static void test_is_on_classic(void) +{ + mock_os_release(os_release_classic); + mock_meta_snap_yaml(NULL); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); +} + +static const char *os_release_core16 = "" + "NAME=\"Ubuntu Core\"\n" "VERSION_ID=\"16\"\n" "ID=ubuntu-core\n"; + +static const char *meta_snap_yaml_core16 = "" + "name: core\n" + "version: 16-something\n" "type: core\n" "architectures: [amd64]\n"; + +static void test_is_on_core_on16(void) +{ + mock_os_release(os_release_core16); + mock_meta_snap_yaml(meta_snap_yaml_core16); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE16); +} + +static const char *os_release_core18 = "" + "NAME=\"Ubuntu Core\"\n" "VERSION_ID=\"18\"\n" "ID=ubuntu-core\n"; + +static const char *meta_snap_yaml_core18 = "" + "name: core18\n" "type: base\n" "architectures: [amd64]\n"; + +static void test_is_on_core_on18(void) +{ + mock_os_release(os_release_core18); + mock_meta_snap_yaml(meta_snap_yaml_core18); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); +} + +const char *os_release_core20 = "" + "NAME=\"Ubuntu Core\"\n" "VERSION_ID=\"20\"\n" "ID=ubuntu-core\n"; + +static const char *meta_snap_yaml_core20 = "" + "name: core20\n" "type: base\n" "architectures: [amd64]\n"; + +static void test_is_on_core_on20(void) +{ + mock_os_release(os_release_core20); + mock_meta_snap_yaml(meta_snap_yaml_core20); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); +} + +static const char *os_release_classic_with_long_line = "" + "NAME=\"Ubuntu\"\n" + "VERSION=\"17.04 (Zesty Zapus)\"\n" + "ID=ubuntu\n" + "ID_LIKE=debian\n" + "LONG=line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line."; + +static void test_is_on_classic_with_long_line(void) +{ + mock_os_release(os_release_classic_with_long_line); + mock_meta_snap_yaml(NULL); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); +} + +static const char *os_release_fedora_base = "" + "NAME=Fedora\nID=fedora\nVARIANT_ID=snappy\n"; + +static const char *meta_snap_yaml_fedora_base = "" + "name: fedora29\n" "type: base\n" "architectures: [amd64]\n"; + +static void test_is_on_fedora_base(void) +{ + mock_os_release(os_release_fedora_base); + mock_meta_snap_yaml(meta_snap_yaml_fedora_base); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); +} + +static const char *os_release_fedora_ws = "" + "NAME=Fedora\nID=fedora\nVARIANT_ID=workstation\n"; + +static void test_is_on_fedora_ws(void) +{ + mock_os_release(os_release_fedora_ws); + mock_meta_snap_yaml(NULL); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); +} + +static const char *os_release_custom = "" + "NAME=\"Custom Distribution\"\nID=custom\n"; + +static const char *meta_snap_yaml_custom = "" + "name: custom\n" + "version: rolling\n" + "summary: Runtime environment based on Custom Distribution\n" + "type: base\n" "architectures: [amd64]\n"; + +static void test_is_on_custom_base(void) +{ + mock_os_release(os_release_custom); + + /* Without /meta/snap.yaml we treat "Custom Distribution" as classic. */ + mock_meta_snap_yaml(NULL); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CLASSIC); + + /* With /meta/snap.yaml we treat it as core instead. */ + mock_meta_snap_yaml(meta_snap_yaml_custom); + g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); +} + +static void test_should_use_normal_mode(void) +{ + g_assert_false(sc_should_use_normal_mode(SC_DISTRO_CORE16, "core")); + g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CORE_OTHER, "core")); + g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CLASSIC, "core")); + + g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CORE16, "core18")); + g_assert_true(sc_should_use_normal_mode + (SC_DISTRO_CORE_OTHER, "core18")); + g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CLASSIC, "core18")); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/classic/on-classic", test_is_on_classic); + g_test_add_func("/classic/on-classic-with-long-line", + test_is_on_classic_with_long_line); + g_test_add_func("/classic/on-core-on16", test_is_on_core_on16); + g_test_add_func("/classic/on-core-on18", test_is_on_core_on18); + g_test_add_func("/classic/on-core-on20", test_is_on_core_on20); + g_test_add_func("/classic/on-fedora-base", test_is_on_fedora_base); + g_test_add_func("/classic/on-fedora-ws", test_is_on_fedora_ws); + g_test_add_func("/classic/on-custom-base", test_is_on_custom_base); + g_test_add_func("/classic/should-use-normal-mode", + test_should_use_normal_mode); +} diff --git a/cmd/libsnap-confine-private/classic.c b/cmd/libsnap-confine-private/classic.c new file mode 100644 index 00000000..57f681c3 --- /dev/null +++ b/cmd/libsnap-confine-private/classic.c @@ -0,0 +1,63 @@ +#include "config.h" +#include "classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" + +#include +#include +#include +#include + +static const char *os_release = "/etc/os-release"; +static const char *meta_snap_yaml = "/meta/snap.yaml"; + +sc_distro sc_classify_distro(void) +{ + FILE *f SC_CLEANUP(sc_cleanup_file) = fopen(os_release, "r"); + if (f == NULL) { + return SC_DISTRO_CLASSIC; + } + + bool is_core = false; + int core_version = 0; + char buf[255] = { 0 }; + + while (fgets(buf, sizeof buf, f) != NULL) { + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') { + buf[len - 1] = '\0'; + } + if (sc_streq(buf, "ID=\"ubuntu-core\"") + || sc_streq(buf, "ID=ubuntu-core")) { + is_core = true; + } else if (sc_streq(buf, "VERSION_ID=\"16\"") + || sc_streq(buf, "VERSION_ID=16")) { + core_version = 16; + } else if (sc_streq(buf, "VARIANT_ID=\"snappy\"") + || sc_streq(buf, "VARIANT_ID=snappy")) { + is_core = true; + } + } + + if (!is_core) { + /* Since classic systems don't have a /meta/snap.yaml file the simple + presence of that file qualifies as SC_DISTRO_CORE_OTHER. */ + if (access(meta_snap_yaml, F_OK) == 0) { + is_core = true; + } + } + + if (is_core) { + if (core_version == 16) { + return SC_DISTRO_CORE16; + } + return SC_DISTRO_CORE_OTHER; + } else { + return SC_DISTRO_CLASSIC; + } +} + +bool sc_should_use_normal_mode(sc_distro distro, const char *base_snap_name) +{ + return distro != SC_DISTRO_CORE16 || !sc_streq(base_snap_name, "core"); +} diff --git a/cmd/libsnap-confine-private/classic.h b/cmd/libsnap-confine-private/classic.h new file mode 100644 index 00000000..fe10add7 --- /dev/null +++ b/cmd/libsnap-confine-private/classic.h @@ -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 . + * + */ +#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" + +typedef enum sc_distro { + SC_DISTRO_CORE16, // As present in both "core" and later on in "core16" + SC_DISTRO_CORE_OTHER, // Any core distribution. + SC_DISTRO_CLASSIC, // Any classic distribution. +} sc_distro; + +sc_distro sc_classify_distro(void); + +bool sc_should_use_normal_mode(sc_distro distro, const char *base_snap_name); + +#endif diff --git a/cmd/libsnap-confine-private/cleanup-funcs-test.c b/cmd/libsnap-confine-private/cleanup-funcs-test.c new file mode 100644 index 00000000..3565892f --- /dev/null +++ b/cmd/libsnap-confine-private/cleanup-funcs-test.c @@ -0,0 +1,44 @@ +/* + * 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 + +static int called = 0; + +static void cleanup_fn(int *ptr) +{ + called = 1; +} + +// Test that cleanup functions are applied as expected +static void test_cleanup_sanity(void) +{ + { + int test SC_CLEANUP(cleanup_fn); + test = 0; + test++; + } + g_assert_cmpint(called, ==, 1); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/cleanup/sanity", test_cleanup_sanity); +} diff --git a/cmd/libsnap-confine-private/cleanup-funcs.c b/cmd/libsnap-confine-private/cleanup-funcs.c new file mode 100644 index 00000000..44005e70 --- /dev/null +++ b/cmd/libsnap-confine-private/cleanup-funcs.c @@ -0,0 +1,56 @@ +/* + * 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) +{ + if (ptr != NULL) { + free(*ptr); + } +} + +void sc_cleanup_file(FILE ** ptr) +{ + if (ptr != NULL && *ptr != NULL) { + fclose(*ptr); + } +} + +void sc_cleanup_endmntent(FILE ** ptr) +{ + if (ptr != NULL && *ptr != NULL) { + endmntent(*ptr); + } +} + +void sc_cleanup_closedir(DIR ** ptr) +{ + if (ptr != NULL && *ptr != NULL) { + closedir(*ptr); + } +} + +void sc_cleanup_close(int *ptr) +{ + if (ptr != NULL && *ptr != -1) { + close(*ptr); + } +} diff --git a/cmd/libsnap-confine-private/cleanup-funcs.h b/cmd/libsnap-confine-private/cleanup-funcs.h new file mode 100644 index 00000000..5bfe214b --- /dev/null +++ b/cmd/libsnap-confine-private/cleanup-funcs.h @@ -0,0 +1,74 @@ +/* + * 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 +#include +#include + +// SC_CLEANUP will run the given cleanup function when the variable next +// to it goes out of scope. +#define SC_CLEANUP(n) __attribute__((cleanup(n))) + +/** + * 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); + +/** + * 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/libsnap-confine-private/error-test.c b/cmd/libsnap-confine-private/error-test.c new file mode 100644 index 00000000..87cf705b --- /dev/null +++ b/cmd/libsnap-confine-private/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(void) +{ + 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(void) +{ + 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(void) +{ + // 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(void) +{ + // 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(void) +{ + // 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(void) +{ + // 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(void) +{ + // 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(void) +{ + // 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(void) +{ + // 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(void) +{ + // 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(void) +{ + // 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(void) +{ + // 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(void) +{ + // 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(void) +{ + // 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(void) +{ + 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/libsnap-confine-private/error.c b/cmd/libsnap-confine-private/error.c new file mode 100644 index 00000000..21faaf76 --- /dev/null +++ b/cmd/libsnap-confine-private/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/libsnap-confine-private/error.h b/cmd/libsnap-confine-private/error.h new file mode 100644 index 00000000..71db201d --- /dev/null +++ b/cmd/libsnap-confine-private/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/libsnap-confine-private/fault-injection-test.c b/cmd/libsnap-confine-private/fault-injection-test.c new file mode 100644 index 00000000..1d62f593 --- /dev/null +++ b/cmd/libsnap-confine-private/fault-injection-test.c @@ -0,0 +1,63 @@ +/* + * Copyright (C) 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 "fault-injection.h" +#include "fault-injection.c" + +#include +#include + +static bool broken(struct sc_fault_state *state, void *ptr) +{ + return true; +} + +static bool broken_alter_msg(struct sc_fault_state *state, void *ptr) +{ + char **s = ptr; + *s = "broken"; + return true; +} + +static void test_fault_injection(void) +{ + g_assert_false(sc_faulty("foo", NULL)); + + sc_break("foo", broken); + g_assert_true(sc_faulty("foo", NULL)); + + sc_reset_faults(); + g_assert_false(sc_faulty("foo", NULL)); + + const char *msg = NULL; + if (!sc_faulty("foo", &msg)) { + msg = "working"; + } + g_assert_cmpstr(msg, ==, "working"); + + sc_break("foo", broken_alter_msg); + if (!sc_faulty("foo", &msg)) { + msg = "working"; + } + g_assert_cmpstr(msg, ==, "broken"); + sc_reset_faults(); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/fault-injection", test_fault_injection); +} diff --git a/cmd/libsnap-confine-private/fault-injection.c b/cmd/libsnap-confine-private/fault-injection.c new file mode 100644 index 00000000..c8486843 --- /dev/null +++ b/cmd/libsnap-confine-private/fault-injection.c @@ -0,0 +1,78 @@ +/* + * Copyright (C) 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 "fault-injection.h" + +#ifdef _ENABLE_FAULT_INJECTION + +#include +#include + +struct sc_fault { + const char *name; + struct sc_fault *next; + sc_fault_fn fn; + struct sc_fault_state state; +}; + +static struct sc_fault *sc_faults = NULL; + +bool sc_faulty(const char *name, void *ptr) +{ + for (struct sc_fault * fault = sc_faults; fault != NULL; + fault = fault->next) { + if (strcmp(name, fault->name) == 0) { + bool is_faulty = fault->fn(&fault->state, ptr); + fault->state.ncalls++; + return is_faulty; + } + } + return false; +} + +void sc_break(const char *name, sc_fault_fn fn) +{ + struct sc_fault *fault = calloc(1, sizeof *fault); + if (fault == NULL) { + abort(); + } + fault->name = name; + fault->next = sc_faults; + fault->fn = fn; + fault->state.ncalls = 0; + sc_faults = fault; +} + +void sc_reset_faults(void) +{ + struct sc_fault *next_fault; + for (struct sc_fault * fault = sc_faults; fault != NULL; + fault = next_fault) { + next_fault = fault->next; + free(fault); + } + sc_faults = NULL; +} + +#else // ifndef _ENABLE_FAULT_INJECTION + +bool sc_faulty(const char *name, void *ptr) +{ + return false; +} + +#endif // ifndef _ENABLE_FAULT_INJECTION diff --git a/cmd/libsnap-confine-private/fault-injection.h b/cmd/libsnap-confine-private/fault-injection.h new file mode 100644 index 00000000..cd7c573a --- /dev/null +++ b/cmd/libsnap-confine-private/fault-injection.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 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_FAULT_INJECTION_H +#define SNAP_CONFINE_FAULT_INJECTION_H + +#include + +/** + * Check for an injected fault. + * + * The name of the fault must match what was passed to sc_break(). The second + * argument can be modified by the fault callback function. The return value + * indicates if a fault was injected. It is assumed that once a fault was + * injected the passed pointer was used to modify the state in useful way. + * + * When the pre-processor macro _ENABLE_FAULT_INJECTION is not defined this + * function always returns false and does nothing at all. + **/ +bool sc_faulty(const char *name, void *ptr); + +#ifdef _ENABLE_FAULT_INJECTION + +struct sc_fault_state; + +typedef bool(*sc_fault_fn) (struct sc_fault_state * state, void *ptr); + +struct sc_fault_state { + int ncalls; +}; + +/** + * Inject a fault for testing. + * + * The name of the fault must match the expected calls to sc_faulty(). The + * second argument is a callback that is invoked each time sc_faulty() is + * called. It is designed to inspect an argument passed to sc_faulty() and as + * well as the state of the fault injection point and return a boolean + * indicating that a fault has occurred. + * + * After testing faults should be reset using sc_reset_faults(). + **/ + +void sc_break(const char *name, sc_fault_fn fn); + +/** + * Remove all the injected faults. + **/ +void sc_reset_faults(void); + +#endif // ifndef _ENABLE_FAULT_INJECTION + +#endif diff --git a/cmd/libsnap-confine-private/feature-test.c b/cmd/libsnap-confine-private/feature-test.c new file mode 100644 index 00000000..57a58e5e --- /dev/null +++ b/cmd/libsnap-confine-private/feature-test.c @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 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 "feature.h" +#include "feature.c" + +#include + +#include + +#include "string-utils.h" +#include "test-utils.h" + +static char *sc_testdir(void) +{ + char *d = g_dir_make_tmp(NULL, NULL); + g_assert_nonnull(d); + g_test_queue_free(d); + g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, d); + return d; +} + +// Set the feature flag directory to given value, useful for cleanup handlers. +static void set_feature_flag_dir(const char *dir) +{ + feature_flag_dir = dir; +} + +// Mock the location of the feature flag directory. +static void sc_mock_feature_flag_dir(const char *d) +{ + g_test_queue_destroy((GDestroyNotify) set_feature_flag_dir, + (void *)feature_flag_dir); + set_feature_flag_dir(d); +} + +static void test_feature_enabled__missing_dir(void) +{ + const char *d = sc_testdir(); + char subd[PATH_MAX]; + sc_must_snprintf(subd, sizeof subd, "%s/absent", d); + sc_mock_feature_flag_dir(subd); + g_assert(!sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE)); +} + +static void test_feature_enabled__missing_file(void) +{ + const char *d = sc_testdir(); + sc_mock_feature_flag_dir(d); + g_assert(!sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE)); +} + +static void test_feature_enabled__present_file(void) +{ + const char *d = sc_testdir(); + sc_mock_feature_flag_dir(d); + char pname[PATH_MAX]; + sc_must_snprintf(pname, sizeof pname, "%s/per-user-mount-namespace", d); + g_file_set_contents(pname, "", -1, NULL); + + g_assert(sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE)); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/feature/missing_dir", + test_feature_enabled__missing_dir); + g_test_add_func("/feature/missing_file", + test_feature_enabled__missing_file); + g_test_add_func("/feature/present_file", + test_feature_enabled__present_file); +} diff --git a/cmd/libsnap-confine-private/feature.c b/cmd/libsnap-confine-private/feature.c new file mode 100644 index 00000000..a5dbe411 --- /dev/null +++ b/cmd/libsnap-confine-private/feature.c @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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 . + * + */ + +#define _GNU_SOURCE + +#include "feature.h" + +#include +#include +#include +#include +#include + +#include "cleanup-funcs.h" +#include "utils.h" + +static const char *feature_flag_dir = "/var/lib/snapd/features"; + +bool sc_feature_enabled(sc_feature_flag flag) +{ + const char *file_name; + switch (flag) { + case SC_PER_USER_MOUNT_NAMESPACE: + file_name = "per-user-mount-namespace"; + break; + default: + die("unknown feature flag code %d", flag); + } + + int dirfd SC_CLEANUP(sc_cleanup_close) = -1; + dirfd = open(feature_flag_dir, O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_PATH); + if (dirfd < 0 && errno == ENOENT) { + return false; + } + if (dirfd < 0) { + die("cannot open path %s", feature_flag_dir); + } + + struct stat file_info; + if (fstatat(dirfd, file_name, &file_info, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno == ENOENT) { + return false; + } + die("cannot inspect file %s/%s", feature_flag_dir, file_name); + } + + return S_ISREG(file_info.st_mode); +} diff --git a/cmd/libsnap-confine-private/feature.h b/cmd/libsnap-confine-private/feature.h new file mode 100644 index 00000000..41b6a26b --- /dev/null +++ b/cmd/libsnap-confine-private/feature.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 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_FEATURE_H +#define SNAP_CONFINE_FEATURE_H + +#include + +typedef enum sc_feature_flag { + SC_PER_USER_MOUNT_NAMESPACE, +} sc_feature_flag; + +/** + * sc_feature_enabled returns true if a given feature flag has been activated + * by the user via "snap set core experimental.xxx=true". This is determined by + * testing the presence of a file in /var/lib/snapd/features/ that is named + * after the flag name. +**/ +bool sc_feature_enabled(sc_feature_flag flag); + +#endif diff --git a/cmd/libsnap-confine-private/locking-test.c b/cmd/libsnap-confine-private/locking-test.c new file mode 100644 index 00000000..21111ff1 --- /dev/null +++ b/cmd/libsnap-confine-private/locking-test.c @@ -0,0 +1,143 @@ +/* + * Copyright (C) 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 "locking.h" +#include "locking.c" + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/test-utils.h" + +#include + +#include +#include + +// Set alternate locking directory +static void sc_set_lock_dir(const char *dir) +{ + sc_lock_dir = dir; +} + +// A variant of unsetenv that is compatible with GDestroyNotify +static void my_unsetenv(const char *k) +{ + unsetenv(k); +} + +// Use temporary directory for locking. +// +// The directory is automatically reset to the real value at the end of the +// test. +static const char *sc_test_use_fake_lock_dir(void) +{ + char *lock_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. + lock_dir = getenv("SNAP_CONFINE_LOCK_DIR"); + g_assert_nonnull(lock_dir); + } else { + lock_dir = g_dir_make_tmp(NULL, NULL); + g_assert_nonnull(lock_dir); + g_test_queue_free(lock_dir); + g_assert_cmpint(setenv("SNAP_CONFINE_LOCK_DIR", lock_dir, 0), + ==, 0); + g_test_queue_destroy((GDestroyNotify) my_unsetenv, + "SNAP_CONFINE_LOCK_DIR"); + g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, lock_dir); + } + g_test_queue_destroy((GDestroyNotify) sc_set_lock_dir, SC_LOCK_DIR); + sc_set_lock_dir(lock_dir); + return lock_dir; +} + +// Check that locking a namespace actually flock's the mutex with LOCK_EX +static void test_sc_lock_unlock(void) +{ + const char *lock_dir = sc_test_use_fake_lock_dir(); + int fd = sc_lock_generic("foo", 123); + // Construct the name of the lock file + char *lock_file SC_CLEANUP(sc_cleanup_string) = NULL; + lock_file = g_strdup_printf("%s/foo.123.lock", lock_dir); + // 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 SC_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 lock. + sc_unlock(fd); + // Re-attempt the locking operation. This time it should succeed. + err = flock(lock_fd, LOCK_EX | LOCK_NB); + g_assert_cmpint(err, ==, 0); +} + +// Check that holding a lock is properly detected. +static void test_sc_verify_snap_lock__locked(void) +{ + (void)sc_test_use_fake_lock_dir(); + int fd = sc_lock_snap("foo"); + sc_verify_snap_lock("foo"); + sc_unlock(fd); +} + +// Check that holding a lock is properly detected. +static void test_sc_verify_snap_lock__unlocked(void) +{ + (void)sc_test_use_fake_lock_dir(); + if (g_test_subprocess()) { + sc_verify_snap_lock("foo"); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("unexpectedly managed to acquire exclusive lock over snap foo\n"); +} + +static void test_sc_enable_sanity_timeout(void) +{ + if (g_test_subprocess()) { + sc_enable_sanity_timeout(); + debug("waiting..."); + usleep(7 * G_USEC_PER_SEC); + debug("woke up"); + sc_disable_sanity_timeout(); + return; + } + g_test_trap_subprocess(NULL, 1 * G_USEC_PER_SEC, + G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr ("sanity timeout expired: Interrupted system call\n"); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/locking/sc_lock_unlock", test_sc_lock_unlock); + g_test_add_func("/locking/sc_enable_sanity_timeout", + test_sc_enable_sanity_timeout); + g_test_add_func("/locking/sc_verify_snap_lock__locked", + test_sc_verify_snap_lock__locked); + g_test_add_func("/locking/sc_verify_snap_lock__unlocked", + test_sc_verify_snap_lock__unlocked); +} diff --git a/cmd/libsnap-confine-private/locking.c b/cmd/libsnap-confine-private/locking.c new file mode 100644 index 00000000..61720b71 --- /dev/null +++ b/cmd/libsnap-confine-private/locking.c @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2017-2018 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 "locking.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +// SANITY_TIMEOUT is the timeout in seconds that is used when +// "sc_enable_sanity_timeout()" is called +static const int SANITY_TIMEOUT = 30; + +/** + * 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; +} + +void sc_enable_sanity_timeout(void) +{ + 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 gets interrupted. + act.sa_flags = 0; + if (sigaction(SIGALRM, &act, NULL) < 0) { + die("cannot install signal handler for SIGALRM"); + } + alarm(SANITY_TIMEOUT); + debug("sanity timeout initialized and set for %i seconds", + SANITY_TIMEOUT); +} + +void sc_disable_sanity_timeout(void) +{ + 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"); +} + +#define SC_LOCK_DIR "/run/snapd/lock" + +static const char *sc_lock_dir = SC_LOCK_DIR; + +static int get_lock_directory(void) +{ + // Create (if required) and open the lock directory. + debug("creating lock directory %s (if missing)", sc_lock_dir); + if (sc_nonfatal_mkpath(sc_lock_dir, 0755) < 0) { + die("cannot create lock directory %s", sc_lock_dir); + } + debug("opening lock directory %s", sc_lock_dir); + int dir_fd = + open(sc_lock_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW); + if (dir_fd < 0) { + die("cannot open lock directory"); + } + return dir_fd; +} + +static void get_lock_name(char *lock_fname, size_t size, const char *scope, + uid_t uid) +{ + if (uid == 0) { + // The root user doesn't have a per-user mount namespace. + // Doing so would be confusing for services which use $SNAP_DATA + // as home, and not in $SNAP_USER_DATA. + sc_must_snprintf(lock_fname, size, "%s.lock", scope ? : ""); + } else { + sc_must_snprintf(lock_fname, size, "%s.%d.lock", + scope ? : "", uid); + } +} + +static int open_lock(const char *scope, uid_t uid) +{ + int dir_fd SC_CLEANUP(sc_cleanup_close) = -1; + char lock_fname[PATH_MAX] = { 0 }; + int lock_fd; + + dir_fd = get_lock_directory(); + get_lock_name(lock_fname, sizeof lock_fname, scope, uid); + + // Open the lock file and acquire an exclusive lock. + debug("opening lock file: %s/%s", sc_lock_dir, lock_fname); + lock_fd = openat(dir_fd, lock_fname, + O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); + if (lock_fd < 0) { + die("cannot open lock file: %s/%s", sc_lock_dir, lock_fname); + } + return lock_fd; +} + +static int sc_lock_generic(const char *scope, uid_t uid) +{ + int lock_fd = open_lock(scope, uid); + sc_enable_sanity_timeout(); + debug("acquiring exclusive lock (scope %s, uid %d)", + scope ? : "(global)", uid); + if (flock(lock_fd, LOCK_EX) < 0) { + sc_disable_sanity_timeout(); + close(lock_fd); + die("cannot acquire exclusive lock (scope %s, uid %d)", + scope ? : "(global)", uid); + } else { + sc_disable_sanity_timeout(); + } + return lock_fd; +} + +int sc_lock_global(void) +{ + return sc_lock_generic(NULL, 0); +} + +int sc_lock_snap(const char *snap_name) +{ + return sc_lock_generic(snap_name, 0); +} + +void sc_verify_snap_lock(const char *snap_name) +{ + int lock_fd, retval; + + lock_fd = open_lock(snap_name, 0); + debug("trying to verify whether exclusive lock over snap %s is held", + snap_name); + retval = flock(lock_fd, LOCK_EX | LOCK_NB); + if (retval == 0) { + /* We managed to grab the lock, the lock was not held! */ + flock(lock_fd, LOCK_UN); + close(lock_fd); + errno = 0; + die("unexpectedly managed to acquire exclusive lock over snap %s", snap_name); + } + if (retval < 0 && errno != EWOULDBLOCK) { + die("cannot verify exclusive lock over snap %s", snap_name); + } + /* We tried but failed to grab the lock because the file is already locked. + * Good, this is what we expected. */ +} + +int sc_lock_snap_user(const char *snap_name, uid_t uid) +{ + return sc_lock_generic(snap_name, uid); +} + +void sc_unlock(int lock_fd) +{ + // Release the lock and finish. + debug("releasing lock %d", lock_fd); + if (flock(lock_fd, LOCK_UN) < 0) { + die("cannot release lock %d", lock_fd); + } + close(lock_fd); +} diff --git a/cmd/libsnap-confine-private/locking.h b/cmd/libsnap-confine-private/locking.h new file mode 100644 index 00000000..b900528a --- /dev/null +++ b/cmd/libsnap-confine-private/locking.h @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2017-2018 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_LOCKING_H +#define SNAP_CONFINE_LOCKING_H + +// Include config.h which pulls in _GNU_SOURCE which in turn allows sys/types.h +// to define O_PATH. Since locking.h is included from locking.c this is +// required to see O_PATH there. +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +/** + * Obtain a flock-based, exclusive, globally scoped, lock. + * + * The actual lock is placed in "/run/snap/ns/.lock" + * + * If the lock cannot be acquired for three seconds (via + * sc_enable_sanity_timeout) then the function fails and the process dies. + * + * The return value needs to be passed to sc_unlock(), there is no need to + * check for errors as the function will die() on any problem. + **/ +int sc_lock_global(void); + +/** + * Obtain a flock-based, exclusive, snap-scoped, lock. + * + * The actual lock is placed in "/run/snapd/ns/$SNAP_NAME.lock" + * It should be acquired only when holding the global lock. + * + * If the lock cannot be acquired for three seconds (via + * sc_enable_sanity_timeout) then the function fails and the process dies. + * + * The return value needs to be passed to sc_unlock(), there is no need to + * check for errors as the function will die() on any problem. + **/ +int sc_lock_snap(const char *snap_name); + +/** + * Verify that a flock-based, exclusive, snap-scoped, lock is held. + * + * If the lock is not held the process dies. The details about the lock + * are exactly the same as for sc_lock_snap(). + **/ +void sc_verify_snap_lock(const char *snap_name); + +/** + * Obtain a flock-based, exclusive, snap-scoped, lock. + * + * The actual lock is placed in "/run/snapd/ns/$SNAP_NAME.$UID.lock" + * It should be acquired only when holding the snap-specific lock. + * + * If the lock cannot be acquired for three seconds (via + * sc_enable_sanity_timeout) then the function fails and the process dies. + * The return value needs to be passed to sc_unlock(), there is no need to + * check for errors as the function will die() on any problem. + **/ +int sc_lock_snap_user(const char *snap_name, uid_t uid); + +/** + * Release a flock-based lock. + * + * All kinds of locks can be unlocked the same way. This function simply + * unlocks the lock and closes the file descriptor. + **/ +void sc_unlock(int lock_fd); + +/** + * 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. + **/ +void sc_enable_sanity_timeout(void); + +/** + * 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. + **/ +void sc_disable_sanity_timeout(void); + +#endif // SNAP_CONFINE_LOCKING_H diff --git a/cmd/libsnap-confine-private/mount-opt-test.c b/cmd/libsnap-confine-private/mount-opt-test.c new file mode 100644 index 00000000..440d62a6 --- /dev/null +++ b/cmd/libsnap-confine-private/mount-opt-test.c @@ -0,0 +1,343 @@ +/* + * 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 + +#include + +static void test_sc_mount_opt2str(void) +{ + char buf[1000] = { 0 }; + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, 0), ==, ""); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_RDONLY), ==, "ro"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOSUID), ==, + "nosuid"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NODEV), ==, + "nodev"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOEXEC), ==, + "noexec"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SYNCHRONOUS), ==, + "sync"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REMOUNT), ==, + "remount"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_MANDLOCK), ==, + "mand"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_DIRSYNC), ==, + "dirsync"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOATIME), ==, + "noatime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NODIRATIME), ==, + "nodiratime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_BIND), ==, "bind"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_BIND), ==, + "rbind"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_MOVE), ==, "move"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SILENT), ==, + "silent"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_POSIXACL), ==, + "acl"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_UNBINDABLE), ==, + "unbindable"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_PRIVATE), ==, + "private"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_PRIVATE), + ==, "rprivate"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SLAVE), ==, + "slave"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_SLAVE), + ==, "rslave"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SHARED), ==, + "shared"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_SHARED), + ==, "rshared"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_RELATIME), ==, + "relatime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_KERNMOUNT), ==, + "kernmount"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_I_VERSION), ==, + "iversion"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_STRICTATIME), ==, + "strictatime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_LAZYTIME), ==, + "lazytime"); + // MS_NOSEC is not defined in userspace + // MS_BORN is not defined in userspace + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_ACTIVE), ==, + "active"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOUSER), ==, + "nouser"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, 0x300), ==, "0x300"); + // random compositions do work + g_assert_cmpstr(sc_mount_opt2str + (buf, sizeof buf, MS_RDONLY | MS_NOEXEC | MS_BIND), ==, + "ro,noexec,bind"); +} + +static void test_sc_mount_cmd(void) +{ + char cmd[10000] = { 0 }; + + // Typical mount + sc_mount_cmd(cmd, sizeof cmd, "/dev/sda3", "/mnt", "ext4", MS_RDONLY, + NULL); + g_assert_cmpstr(cmd, ==, "mount -t ext4 -o ro /dev/sda3 /mnt"); + + // Bind mount + sc_mount_cmd(cmd, sizeof cmd, "/source", "/target", NULL, MS_BIND, + NULL); + g_assert_cmpstr(cmd, ==, "mount --bind /source /target"); + + // + recursive + sc_mount_cmd(cmd, sizeof cmd, "/source", "/target", NULL, + MS_BIND | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --rbind /source /target"); + + // Shared subtree mount + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_SHARED, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-shared /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_SLAVE, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-slave /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_PRIVATE, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-private /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_UNBINDABLE, + NULL); + g_assert_cmpstr(cmd, ==, "mount --make-unbindable /place"); + + // + recursive + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, + MS_SHARED | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-rshared /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_SLAVE | MS_REC, + NULL); + g_assert_cmpstr(cmd, ==, "mount --make-rslave /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, + MS_PRIVATE | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-rprivate /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, + MS_UNBINDABLE | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-runbindable /place"); + + // Move + sc_mount_cmd(cmd, sizeof cmd, "/from", "/to", NULL, MS_MOVE, NULL); + g_assert_cmpstr(cmd, ==, "mount --move /from /to"); + + // Monster (invalid but let's format it) + char from[PATH_MAX] = { 0 }; + char to[PATH_MAX] = { 0 }; + for (int i = 1; i < PATH_MAX - 1; ++i) { + from[i] = 'a'; + to[i] = 'b'; + } + from[0] = '/'; + to[0] = '/'; + from[PATH_MAX - 1] = 0; + to[PATH_MAX - 1] = 0; + int opts = MS_BIND | MS_MOVE | MS_SHARED | MS_SLAVE | MS_PRIVATE | + MS_UNBINDABLE | MS_REC | MS_RDONLY | MS_NOSUID | MS_NODEV | + MS_NOEXEC | MS_SYNCHRONOUS | MS_REMOUNT | MS_MANDLOCK | MS_DIRSYNC | + MS_NOATIME | MS_NODIRATIME | MS_BIND | MS_SILENT | MS_POSIXACL | + MS_RELATIME | MS_KERNMOUNT | MS_I_VERSION | MS_STRICTATIME | + MS_LAZYTIME; + const char *fstype = "fstype"; + sc_mount_cmd(cmd, sizeof cmd, from, to, fstype, opts, NULL); + const char *expected = + "mount -t fstype " + "--rbind --move --make-rshared --make-rslave --make-rprivate --make-runbindable " + "-o ro,nosuid,nodev,noexec,sync,remount,mand,dirsync,noatime,nodiratime,silent," + "acl,relatime,kernmount,iversion,strictatime,lazytime " + "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " + "/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + g_assert_cmpstr(cmd, ==, expected); +} + +static void test_sc_umount_cmd(void) +{ + char cmd[1000] = { 0 }; + + // Typical umount + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", 0); + g_assert_cmpstr(cmd, ==, "umount /mnt/foo"); + + // Force + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", MNT_FORCE); + g_assert_cmpstr(cmd, ==, "umount --force /mnt/foo"); + + // Detach + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", MNT_DETACH); + g_assert_cmpstr(cmd, ==, "umount --lazy /mnt/foo"); + + // Expire + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", MNT_EXPIRE); + g_assert_cmpstr(cmd, ==, "umount --expire /mnt/foo"); + + // O_NOFOLLOW variant for umount + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", UMOUNT_NOFOLLOW); + g_assert_cmpstr(cmd, ==, "umount --no-follow /mnt/foo"); + + // Everything at once + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", + MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW); + g_assert_cmpstr(cmd, ==, + "umount --force --lazy --expire --no-follow /mnt/foo"); +} + +static bool broken_mount(struct sc_fault_state *state, void *ptr) +{ + errno = EACCES; + return true; +} + +static void test_sc_do_mount(gconstpointer snap_debug) +{ + if (g_test_subprocess()) { + sc_break("mount", broken_mount); + if (GPOINTER_TO_INT(snap_debug) == 1) { + g_setenv("SNAP_CONFINE_DEBUG", "1", true); + } + sc_do_mount("/foo", "/bar", "ext4", MS_RDONLY, NULL); + + g_test_message("expected sc_do_mount not to return"); + sc_reset_faults(); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + if (GPOINTER_TO_INT(snap_debug) == 0) { + g_test_trap_assert_stderr + ("cannot perform operation: mount -t ext4 -o ro /foo /bar: Permission denied\n"); + } else { + /* with snap_debug the debug output hides the actual mount commands *but* + * they are still shown if there was an error + */ + g_test_trap_assert_stderr + ("DEBUG: performing operation: (disabled) use debug build to see details\n" + "cannot perform operation: mount -t ext4 -o ro /foo /bar: Permission denied\n"); + } +} + +static bool broken_umount(struct sc_fault_state *state, void *ptr) +{ + errno = EACCES; + return true; +} + +static void test_sc_do_umount(gconstpointer snap_debug) +{ + if (g_test_subprocess()) { + sc_break("umount", broken_umount); + if (GPOINTER_TO_INT(snap_debug) == 1) { + g_setenv("SNAP_CONFINE_DEBUG", "1", true); + } + sc_do_umount("/foo", MNT_DETACH); + + g_test_message("expected sc_do_umount not to return"); + sc_reset_faults(); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + if (GPOINTER_TO_INT(snap_debug) == 0) { + g_test_trap_assert_stderr + ("cannot perform operation: umount --lazy /foo: Permission denied\n"); + } else { + /* with snap_debug the debug output hides the actual mount commands *but* + * they are still shown if there was an error + */ + g_test_trap_assert_stderr + ("DEBUG: performing operation: (disabled) use debug build to see details\n" + "cannot perform operation: umount --lazy /foo: Permission denied\n"); + } +} + +static bool missing_mount(struct sc_fault_state *state, void *ptr) +{ + errno = ENOENT; + return true; +} + +static void test_sc_do_optional_mount_missing(void) +{ + sc_break("mount", missing_mount); + bool ok = sc_do_optional_mount("/foo", "/bar", "ext4", MS_RDONLY, NULL); + g_assert_false(ok); + sc_reset_faults(); +} + +static void test_sc_do_optional_mount_failure(gconstpointer snap_debug) +{ + if (g_test_subprocess()) { + sc_break("mount", broken_mount); + if (GPOINTER_TO_INT(snap_debug) == 1) { + g_setenv("SNAP_CONFINE_DEBUG", "1", true); + } + (void)sc_do_optional_mount("/foo", "/bar", "ext4", MS_RDONLY, + NULL); + + g_test_message("expected sc_do_mount not to return"); + sc_reset_faults(); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + if (GPOINTER_TO_INT(snap_debug) == 0) { + g_test_trap_assert_stderr + ("cannot perform operation: mount -t ext4 -o ro /foo /bar: Permission denied\n"); + } else { + /* with snap_debug the debug output hides the actual mount commands *but* + * they are still shown if there was an error + */ + g_test_trap_assert_stderr + ("DEBUG: performing operation: (disabled) use debug build to see details\n" + "cannot perform operation: mount -t ext4 -o ro /foo /bar: Permission denied\n"); + } +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/mount/sc_mount_opt2str", test_sc_mount_opt2str); + g_test_add_func("/mount/sc_mount_cmd", test_sc_mount_cmd); + g_test_add_func("/mount/sc_umount_cmd", test_sc_umount_cmd); + g_test_add_data_func("/mount/sc_do_mount", GINT_TO_POINTER(0), + test_sc_do_mount); + g_test_add_data_func("/mount/sc_do_umount", GINT_TO_POINTER(0), + test_sc_do_umount); + g_test_add_data_func("/mount/sc_do_mount_with_debug", + GINT_TO_POINTER(1), test_sc_do_mount); + g_test_add_data_func("/mount/sc_do_umount_with_debug", + GINT_TO_POINTER(1), test_sc_do_umount); + g_test_add_func("/mount/sc_do_optional_mount_missing", + test_sc_do_optional_mount_missing); + g_test_add_data_func("/mount/sc_do_optional_mount_failure", + GINT_TO_POINTER(0), + test_sc_do_optional_mount_failure); + g_test_add_data_func("/mount/sc_do_optional_mount_failure_with_debug", + GINT_TO_POINTER(1), + test_sc_do_optional_mount_failure); +} diff --git a/cmd/libsnap-confine-private/mount-opt.c b/cmd/libsnap-confine-private/mount-opt.c new file mode 100644 index 00000000..2b2c7a1a --- /dev/null +++ b/cmd/libsnap-confine-private/mount-opt.c @@ -0,0 +1,338 @@ +/* + * 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 +#include +#include +#include + +#include "fault-injection.h" +#include "privs.h" +#include "string-utils.h" +#include "utils.h" + +const char *sc_mount_opt2str(char *buf, size_t buf_size, unsigned long flags) +{ + unsigned long used = 0; + sc_string_init(buf, buf_size); + +#define F(FLAG, TEXT) do { \ + if (flags & (FLAG)) { \ + sc_string_append(buf, buf_size, #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) { + sc_string_append(buf, buf_size, "rbind,"); + used |= MS_REC; + } else { + sc_string_append(buf, buf_size, "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) { + sc_string_append(buf, buf_size, "rprivate,"); + used |= MS_REC; + } else { + sc_string_append(buf, buf_size, "private,"); + } + flags ^= MS_PRIVATE; + } + if (flags & MS_SLAVE) { + if (flags & MS_REC) { + sc_string_append(buf, buf_size, "rslave,"); + used |= MS_REC; + } else { + sc_string_append(buf, buf_size, "slave,"); + } + flags ^= MS_SLAVE; + } + if (flags & MS_SHARED) { + if (flags & MS_REC) { + sc_string_append(buf, buf_size, "rshared,"); + used |= MS_REC; + } else { + sc_string_append(buf, buf_size, "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] = { 0 }; + sc_must_snprintf(of, sizeof of, "%#lx", flags); + sc_string_append(buf, buf_size, of); + } + // Chop the excess comma from the end. + size_t len = strnlen(buf, buf_size); + if (len > 0 && buf[len - 1] == ',') { + buf[len - 1] = 0; + } + return buf; +} + +const char *sc_mount_cmd(char *buf, size_t buf_size, const char *source, const char + *target, const char *fs_type, unsigned long mountflags, const + void *data) +{ + sc_string_init(buf, buf_size); + sc_string_append(buf, buf_size, "mount"); + + // Add filesysystem type if it's there and doesn't have the special value "none" + if (fs_type != NULL && strncmp(fs_type, "none", 5) != 0) { + sc_string_append(buf, buf_size, " -t "); + sc_string_append(buf, buf_size, fs_type); + } + // Check for some special, dedicated options, that aren't represented with + // the generic mount option argument (mount -o ...), by collecting those + // options that we will display as command line arguments in + // used_special_flags. This is used below to filter out these arguments + // from mount_flags when calling sc_mount_opt2str(). + int used_special_flags = 0; + + // Bind-ounts (bind) + if (mountflags & MS_BIND) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --rbind"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --bind"); + } + used_special_flags |= MS_BIND; + } + // Moving mount point location (move) + if (mountflags & MS_MOVE) { + sc_string_append(buf, buf_size, " --move"); + used_special_flags |= MS_MOVE; + } + // Shared subtree operations (shared, slave, private, unbindable). + if (MS_SHARED & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-rshared"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-shared"); + } + used_special_flags |= MS_SHARED; + } + + if (MS_SLAVE & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-rslave"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-slave"); + } + used_special_flags |= MS_SLAVE; + } + + if (MS_PRIVATE & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-rprivate"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-private"); + } + used_special_flags |= MS_PRIVATE; + } + + if (MS_UNBINDABLE & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-runbindable"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-unbindable"); + } + used_special_flags |= MS_UNBINDABLE; + } + // If regular option syntax exists then use it. + if (mountflags & ~used_special_flags) { + char opts_buf[1000] = { 0 }; + sc_mount_opt2str(opts_buf, sizeof opts_buf, mountflags & + ~used_special_flags); + sc_string_append(buf, buf_size, " -o "); + sc_string_append(buf, buf_size, opts_buf); + } + // Add source and target locations + if (source != NULL && strncmp(source, "none", 5) != 0) { + sc_string_append(buf, buf_size, " "); + sc_string_append(buf, buf_size, source); + } + if (target != NULL && strncmp(target, "none", 5) != 0) { + sc_string_append(buf, buf_size, " "); + sc_string_append(buf, buf_size, target); + } + + return buf; +} + +const char *sc_umount_cmd(char *buf, size_t buf_size, const char *target, + int flags) +{ + sc_string_init(buf, buf_size); + sc_string_append(buf, buf_size, "umount"); + + if (flags & MNT_FORCE) { + sc_string_append(buf, buf_size, " --force"); + } + + if (flags & MNT_DETACH) { + sc_string_append(buf, buf_size, " --lazy"); + } + if (flags & MNT_EXPIRE) { + // NOTE: there's no real command line option for MNT_EXPIRE + sc_string_append(buf, buf_size, " --expire"); + } + if (flags & UMOUNT_NOFOLLOW) { + // NOTE: there's no real command line option for UMOUNT_NOFOLLOW + sc_string_append(buf, buf_size, " --no-follow"); + } + if (target != NULL) { + sc_string_append(buf, buf_size, " "); + sc_string_append(buf, buf_size, target); + } + + return buf; +} + +#ifndef SNAP_CONFINE_DEBUG_BUILD +static const char *use_debug_build = + "(disabled) use debug build to see details"; +#endif + +static bool sc_do_mount_ex(const char *source, const char *target, + const char *fs_type, + unsigned long mountflags, const void *data, + bool optional) +{ + char buf[10000] = { 0 }; + const char *mount_cmd = NULL; + + if (sc_is_debug_enabled()) { +#ifdef SNAP_CONFINE_DEBUG_BUILD + mount_cmd = sc_mount_cmd(buf, sizeof(buf), source, + target, fs_type, mountflags, data); +#else + mount_cmd = use_debug_build; +#endif + debug("performing operation: %s", mount_cmd); + } + if (sc_faulty("mount", NULL) + || mount(source, target, fs_type, mountflags, data) < 0) { + int saved_errno = errno; + if (optional && saved_errno == ENOENT) { + // The special-cased value that is allowed to fail. + return false; + } + // Drop privileges so that we can compute our nice error message + // without risking an attack on one of the string functions there. + sc_privs_drop(); + + // Compute the equivalent mount command. + mount_cmd = sc_mount_cmd(buf, sizeof(buf), source, + target, fs_type, mountflags, data); + // Restore errno and die. + errno = saved_errno; + die("cannot perform operation: %s", mount_cmd); + } + return true; +} + +void sc_do_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data) +{ + (void)sc_do_mount_ex(source, target, fs_type, mountflags, data, false); +} + +bool sc_do_optional_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data) +{ + return sc_do_mount_ex(source, target, fs_type, mountflags, data, true); +} + +void sc_do_umount(const char *target, int flags) +{ + char buf[10000] = { 0 }; + const char *umount_cmd = NULL; + + if (sc_is_debug_enabled()) { +#ifdef SNAP_CONFINE_DEBUG_BUILD + umount_cmd = sc_umount_cmd(buf, sizeof(buf), target, flags); +#else + umount_cmd = use_debug_build; +#endif + debug("performing operation: %s", umount_cmd); + } + if (sc_faulty("umount", NULL) || umount2(target, flags) < 0) { + // Save errno as ensure can clobber it. + int saved_errno = errno; + + // Drop privileges so that we can compute our nice error message + // without risking an attack on one of the string functions there. + sc_privs_drop(); + + // Compute the equivalent umount command. + umount_cmd = sc_umount_cmd(buf, sizeof(buf), target, flags); + // Restore errno and die. + errno = saved_errno; + die("cannot perform operation: %s", umount_cmd); + } +} diff --git a/cmd/libsnap-confine-private/mount-opt.h b/cmd/libsnap-confine-private/mount-opt.h new file mode 100644 index 00000000..03c50ffd --- /dev/null +++ b/cmd/libsnap-confine-private/mount-opt.h @@ -0,0 +1,89 @@ +/* + * 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 + +#include +#include + +/** + * Convert flags for mount(2) system call to a string representation. + **/ +const char *sc_mount_opt2str(char *buf, size_t buf_size, unsigned long flags); + +/** + * Compute an equivalent mount(8) command from mount(2) arguments. + * + * This function serves as a human-readable representation of the mount system + * call. The return value is a string that looks like a shell mount command. + * + * Note that the returned command is may not be a valid mount command. No + * sanity checking is performed on the mount flags, source or destination + * arguments. + * + * The returned value is always buf, it is provided as a convenience. + **/ +const char *sc_mount_cmd(char *buf, size_t buf_size, const char *source, const char + *target, const char *fs_type, unsigned long mountflags, + const void *data); + +/** + * Compute an equivalent umount(8) command from umount2(2) arguments. + * + * This function serves as a human-readable representation of the unmount + * system call. The return value is a string that looks like a shell unmount + * command. + * + * Note that some flags are not surfaced at umount command line level. For + * those flags a fake option is synthesized. + * + * Note that the returned command is may not be a valid umount command. No + * sanity checking is performed on the mount flags, source or destination + * arguments. + * + * The returned value is always buf, it is provided as a convenience. + **/ +const char *sc_umount_cmd(char *buf, size_t buf_size, const char *target, + int flags); + +/** + * A thin wrapper around mount(2) with logging and error checks. + **/ +void sc_do_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data); + +/** + * A thin wrapper around mount(2) with logging and error checks. + * + * This variant is allowed to silently fail when mount fails with ENOENT. + * That is, it can be used to perform mount operations and if either the source + * or the destination is not present, carry on as if nothing had happened. + * + * The return value indicates if the operation was successful or not. + **/ +bool sc_do_optional_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data); + +/** + * A thin wrapper around umount(2) with logging and error checks. + **/ +void sc_do_umount(const char *target, int flags); + +#endif // SNAP_CONFINE_MOUNT_OPT_H diff --git a/cmd/libsnap-confine-private/mountinfo-test.c b/cmd/libsnap-confine-private/mountinfo-test.c new file mode 100644 index 00000000..ed0a6101 --- /dev/null +++ b/cmd/libsnap-confine-private/mountinfo-test.c @@ -0,0 +1,200 @@ +/* + * 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(void) +{ + const char *line = + "19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_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(void) +{ + const char *line = + "104 23 0:19 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=99840k,mode=755"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_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(void) +{ + const char *line = + "256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_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(void) +{ + const char *line = "256 104 0:3"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_null(entry); +} + +static void test_parse_mountinfo_entry__no_tags(void) +{ + const char *line = + "1 2 3:4 root mount-dir mount-opts - fs-type mount-source super-opts"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_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(void) +{ + const char *line = + "1 2 3:4 root mount-dir mount-opts tag:1 - fs-type mount-source super-opts"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_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__many_tags(void) +{ + const char *line = + "1 2 3:4 root mount-dir mount-opts tag:1 tag:2 tag:3 tag:4 - fs-type mount-source super-opts"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_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 tag:3 tag:4"); + 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__empty_source(void) +{ + const char *line = + "304 301 0:45 / /snap/test-snapd-content-advanced-plug/x1 rw,relatime - tmpfs rw"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 304); + g_assert_cmpint(entry->parent_id, ==, 301); + g_assert_cmpint(entry->dev_major, ==, 0); + g_assert_cmpint(entry->dev_minor, ==, 45); + g_assert_cmpstr(entry->root, ==, "/"); + g_assert_cmpstr(entry->mount_dir, ==, + "/snap/test-snapd-content-advanced-plug/x1"); + g_assert_cmpstr(entry->mount_opts, ==, "rw,relatime"); + g_assert_cmpstr(entry->optional_fields, ==, ""); + g_assert_cmpstr(entry->fs_type, ==, "tmpfs"); + g_assert_cmpstr(entry->mount_source, ==, ""); + g_assert_cmpstr(entry->super_opts, ==, "rw"); + g_assert_null(entry->next); +} + +static void __attribute__ ((constructor)) init(void) +{ + 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/many_tags", + test_parse_mountinfo_entry__many_tags); + g_test_add_func + ("/mountinfo/parse_mountinfo_entry/empty_source", + test_parse_mountinfo_entry__empty_source); +} diff --git a/cmd/libsnap-confine-private/mountinfo.c b/cmd/libsnap-confine-private/mountinfo.c new file mode 100644 index 00000000..36f12d57 --- /dev/null +++ b/cmd/libsnap-confine-private/mountinfo.c @@ -0,0 +1,274 @@ +/* + * 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 + +#include "cleanup-funcs.h" + +/** + * 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 sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line) + __attribute__ ((nonnull(1))); + +/** + * Free a sc_mountinfo structure and all its entries. + **/ +static void sc_free_mountinfo(struct sc_mountinfo *info) + __attribute__ ((nonnull(1))); + +/** + * Free a sc_mountinfo entry. + **/ +static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info) +{ + return info->first; +} + +struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry + *entry) +{ + return entry->next; +} + +struct sc_mountinfo *sc_parse_mountinfo(const char *fname) +{ + struct sc_mountinfo *info = calloc(1, sizeof *info); + if (info == NULL) { + return NULL; + } + if (fname == NULL) { + fname = "/proc/self/mountinfo"; + } + FILE *f SC_CLEANUP(sc_cleanup_file) = NULL; + f = fopen(fname, "rt"); + if (f == NULL) { + free(info); + return NULL; + } + char *line SC_CLEANUP(sc_cleanup_string) = NULL; + size_t line_size = 0; + struct sc_mountinfo_entry *entry, *last = NULL; + for (;;) { + errno = 0; + if (getline(&line, &line_size, f) == -1) { + if (errno != 0) { + sc_free_mountinfo(info); + return NULL; + } + break; + }; + entry = sc_parse_mountinfo_entry(line); + if (entry == NULL) { + sc_free_mountinfo(info); + return NULL; + } + if (last != NULL) { + last->next = entry; + } else { + info->first = entry; + } + last = entry; + } + return info; +} + +static void show_buffers(const char *line, int offset, + struct sc_mountinfo_entry *entry) +{ +#ifdef MOUNTINFO_DEBUG + fprintf(stderr, "Input buffer (first), with offset arrow\n"); + fprintf(stderr, "Output buffer (second)\n"); + + fputc(' ', stderr); + for (int i = 0; i < offset - 1; ++i) + fputc('-', stderr); + fputc('v', stderr); + fputc('\n', stderr); + + fprintf(stderr, ">%s<\n", line); + + fputc('>', stderr); + for (int i = 0; i < strlen(line); ++i) { + int c = entry->line_buf[i]; + fputc(c == 0 ? '@' : c == 1 ? '#' : c, stderr); + } + fputc('<', stderr); + fputc('\n', stderr); + + fputc('>', stderr); + for (int i = 0; i < strlen(line); ++i) + fputc('=', stderr); + fputc('<', stderr); + fputc('\n', stderr); +#endif // MOUNTINFO_DEBUG +} + +static char *parse_next_string_field(struct sc_mountinfo_entry *entry, + const char *line, int *offset) +{ + int offset_delta = 0; + char *field = &entry->line_buf[0] + *offset; + if (line[*offset] == ' ') { + // Special case for empty fields which cannot be parsed with %s. + *field = '\0'; + *offset += 1; + } else { + int nscanned = + sscanf(line + *offset, "%s%n", field, &offset_delta); + if (nscanned != 1) + return NULL; + *offset += offset_delta; + if (line[*offset] == ' ') { + *offset += 1; + } + } + show_buffers(line, *offset, entry); + return field; +} + +static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line) +{ + // NOTE: the sc_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. + // + // At the end of the parsing process, the input buffer (line) and the + // output buffer (entry->line_buf) are the same except for where spaces + // were converted into NUL bytes (string terminators) and except for the + // leading part of the buffer that contains mount_id, parent_id, dev_major + // and dev_minor integer fields that are parsed separately. + // + // If MOUNTINFO_DEBUG is defined then extra debugging is printed to stderr + // and this allows for visual analysis of what is going on. + struct sc_mountinfo_entry *entry = + calloc(1, sizeof *entry + strlen(line) + 1); + if (entry == NULL) { + return NULL; + } +#ifdef MOUNTINFO_DEBUG + // Poison the buffer with '\1' bytes that are printed as '#' characters + // by show_buffers() below. This is "unaltered" memory. + memset(entry->line_buf, 1, strlen(line)); +#endif // MOUNTINFO_DEBUG + int nscanned; + int offset_delta, offset = 0; + nscanned = sscanf(line, "%d %d %u:%u %n", + &entry->mount_id, &entry->parent_id, + &entry->dev_major, &entry->dev_minor, &offset_delta); + if (nscanned != 4) + goto fail; + offset += offset_delta; + + show_buffers(line, offset, entry); + + if ((entry->root = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + if ((entry->mount_dir = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + if ((entry->mount_opts = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + entry->optional_fields = &entry->line_buf[0] + offset; + // NOTE: This ensures that optional_fields is never NULL. If this changes, + // must adjust all callers of parse_mountinfo_entry() accordingly. + for (int field_num = 0;; ++field_num) { + char *opt_field = parse_next_string_field(entry, line, &offset); + if (opt_field == NULL) + goto fail; + if (strcmp(opt_field, "-") == 0) { + opt_field[0] = 0; + break; + } + if (field_num > 0) { + opt_field[-1] = ' '; + } + } + if ((entry->fs_type = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + if ((entry->mount_source = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + if ((entry->super_opts = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + show_buffers(line, offset, entry); + return entry; + fail: + free(entry); + return NULL; +} + +void sc_cleanup_mountinfo(struct sc_mountinfo **ptr) +{ + if (*ptr != NULL) { + sc_free_mountinfo(*ptr); + *ptr = NULL; + } +} + +static void sc_free_mountinfo(struct sc_mountinfo *info) +{ + struct sc_mountinfo_entry *entry, *next; + for (entry = info->first; entry != NULL; entry = next) { + next = entry->next; + sc_free_mountinfo_entry(entry); + } + free(info); +} + +static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry) +{ + free(entry); +} diff --git a/cmd/libsnap-confine-private/mountinfo.h b/cmd/libsnap-confine-private/mountinfo.h new file mode 100644 index 00000000..6e52c93a --- /dev/null +++ b/cmd/libsnap-confine-private/mountinfo.h @@ -0,0 +1,135 @@ +/* + * 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_MOUNTINFO_H +#define SNAP_CONFINE_MOUNTINFO_H + +/** + * Structure describing entire /proc/self/sc_mountinfo file + **/ +struct sc_mountinfo { + struct sc_mountinfo_entry *first; +}; + +/** + * Structure describing a single entry in /proc/self/sc_mountinfo + **/ +struct sc_mountinfo_entry { + /** + * The mount identifier of a given mount entry. + **/ + int mount_id; + /** + * The parent mount identifier of a given mount entry. + **/ + int parent_id; + unsigned dev_major, dev_minor; + /** + * The root directory of a given mount entry. + **/ + char *root; + /** + * The mount point of a given mount entry. + **/ + char *mount_dir; + /** + * The mount options of a given mount entry. + **/ + char *mount_opts; + /** + * 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. + **/ + char *optional_fields; + /** + * The file system type of a given mount entry. + **/ + char *fs_type; + /** + * The source of a given mount entry. + **/ + char *mount_source; + /** + * The super block options of a given mount entry. + **/ + char *super_opts; + + struct sc_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 file in according to sc_mountinfo syntax. + * + * The argument can be used to parse an arbitrary file. NULL can be used to + * implicitly parse /proc/self/sc_mountinfo, that is the mount information + * associated with the current process. + **/ +struct sc_mountinfo *sc_parse_mountinfo(const char *fname); + +/** + * Free a sc_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 sc_cleanup_mountinfo(struct sc_mountinfo **ptr) + __attribute__ ((nonnull(1))); + +/** + * Get the first sc_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 sc_mountinfo structure + * and should not be freed explicitly. + **/ +struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info) + __attribute__ ((nonnull(1))); + +/** + * Get the next sc_mountinfo entry. + * + * The returned value is a pointer to the next sc_mountinfo entry or NULL if this + * was the last entry. The returned value is bound to the lifecycle of the + * whole sc_mountinfo structure and should not be freed explicitly. + **/ +struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry + *entry) + __attribute__ ((nonnull(1))); + +#endif diff --git a/cmd/libsnap-confine-private/privs-test.c b/cmd/libsnap-confine-private/privs-test.c new file mode 100644 index 00000000..ac32f681 --- /dev/null +++ b/cmd/libsnap-confine-private/privs-test.c @@ -0,0 +1,67 @@ +/* + * Copyright (C) 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 "privs.h" +#include "privs.c" + +#include + +// Test that dropping permissions really works +static void test_sc_privs_drop(void) +{ + if (geteuid() != 0 || getuid() == 0) { + g_test_skip("run this test after chown root.root; chmod u+s"); + return; + } + if (getegid() != 0 || getgid() == 0) { + g_test_skip("run this test after chown root.root; chmod g+s"); + return; + } + if (g_test_subprocess()) { + // We start as a regular user with effective-root identity. + g_assert_cmpint(getuid(), !=, 0); + g_assert_cmpint(getgid(), !=, 0); + + g_assert_cmpint(geteuid(), ==, 0); + g_assert_cmpint(getegid(), ==, 0); + + // We drop the privileges. + sc_privs_drop(); + + // The we are no longer root. + g_assert_cmpint(getuid(), !=, 0); + g_assert_cmpint(geteuid(), !=, 0); + g_assert_cmpint(getgid(), !=, 0); + g_assert_cmpint(getegid(), !=, 0); + + // We don't have any supplementary groups. + gid_t groups[2]; + int num_groups = getgroups(1, groups); + g_assert_cmpint(num_groups, ==, 1); + g_assert_cmpint(groups[0], ==, getgid()); + + // All done. + return; + } + g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_passed(); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/privs/sc_privs_drop", test_sc_privs_drop); +} diff --git a/cmd/libsnap-confine-private/privs.c b/cmd/libsnap-confine-private/privs.c new file mode 100644 index 00000000..18b9587d --- /dev/null +++ b/cmd/libsnap-confine-private/privs.c @@ -0,0 +1,78 @@ +/* + * Copyright (C) 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 "privs.h" + +#define _GNU_SOURCE + +#include + +#include +#include +#include +#include +#include + +#include "utils.h" + +static bool sc_has_capability(const char *cap_name) +{ + // Lookup capability with the given name. + cap_value_t cap; + if (cap_from_name(cap_name, &cap) < 0) { + die("cannot resolve capability name %s", cap_name); + } + // Get the capability state of the current process. + cap_t caps; + if ((caps = cap_get_proc()) == NULL) { + die("cannot obtain capability state (cap_get_proc)"); + } + // Read the effective value of the flag we're dealing with + cap_flag_value_t cap_flags_value; + if (cap_get_flag(caps, cap, CAP_EFFECTIVE, &cap_flags_value) < 0) { + cap_free(caps); // don't bother checking, we die anyway. + die("cannot obtain value of capability flag (cap_get_flag)"); + } + // Free the representation of the capability state of the current process. + if (cap_free(caps) < 0) { + die("cannot free capability flag (cap_free)"); + } + // Check if the effective bit of the capability is set. + return cap_flags_value == CAP_SET; +} + +void sc_privs_drop(void) +{ + gid_t gid = getgid(); + uid_t uid = getuid(); + + // Drop extra group membership if we can. + if (sc_has_capability("cap_setgid")) { + gid_t gid_list[1] = { gid }; + if (setgroups(1, gid_list) < 0) { + die("cannot set supplementary group identifiers"); + } + } + // Switch to real group ID + if (setgid(getgid()) < 0) { + die("cannot set group identifier to %d", gid); + } + // Switch to real user ID + if (setuid(getuid()) < 0) { + die("cannot set user identifier to %d", uid); + } +} diff --git a/cmd/libsnap-confine-private/privs.h b/cmd/libsnap-confine-private/privs.h new file mode 100644 index 00000000..41a4eb5e --- /dev/null +++ b/cmd/libsnap-confine-private/privs.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 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_PRIVS_H +#define SNAP_CONFINE_PRIVS_H + +/** + * Permanently drop elevated permissions. + * + * If the user has elevated permission as a result of running a setuid root + * application then such permission are permanently dropped. + * + * The set of dropped permissions include: + * - user and group identifier + * - supplementary group identifiers + * + * The function ensures that the elevated permission are dropped or dies if + * this cannot be achieved. Note that only the elevated permissions are + * dropped. When the process itself was started by root then this function does + * nothing at all. + **/ +void sc_privs_drop(void); + +#endif diff --git a/cmd/libsnap-confine-private/secure-getenv-test.c b/cmd/libsnap-confine-private/secure-getenv-test.c new file mode 100644 index 00000000..61b42502 --- /dev/null +++ b/cmd/libsnap-confine-private/secure-getenv-test.c @@ -0,0 +1,23 @@ +/* + * Copyright (C) 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 "secure-getenv.h" +#include "secure-getenv.c" + +#include + +// TODO: write some tests diff --git a/cmd/libsnap-confine-private/secure-getenv.c b/cmd/libsnap-confine-private/secure-getenv.c new file mode 100644 index 00000000..cc8b4dc8 --- /dev/null +++ b/cmd/libsnap-confine-private/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/libsnap-confine-private/secure-getenv.h b/cmd/libsnap-confine-private/secure-getenv.h new file mode 100644 index 00000000..1b139a3a --- /dev/null +++ b/cmd/libsnap-confine-private/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/libsnap-confine-private/snap-test.c b/cmd/libsnap-confine-private/snap-test.c new file mode 100644 index 00000000..e5fe9917 --- /dev/null +++ b/cmd/libsnap-confine-private/snap-test.c @@ -0,0 +1,568 @@ +/* + * Copyright (C) 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 "snap.h" +#include "snap.c" + +#include + +static void test_verify_security_tag(void) +{ + // First, test the names we know are good + g_assert_true(verify_security_tag("snap.name.app", "name")); + g_assert_true(verify_security_tag + ("snap.network-manager.NetworkManager", + "network-manager")); + g_assert_true(verify_security_tag("snap.f00.bar-baz1", "f00")); + g_assert_true(verify_security_tag("snap.foo.hook.bar", "foo")); + g_assert_true(verify_security_tag("snap.foo.hook.bar-baz", "foo")); + g_assert_true(verify_security_tag + ("snap.foo_instance.bar-baz", "foo_instance")); + g_assert_true(verify_security_tag + ("snap.foo_instance.hook.bar-baz", "foo_instance")); + g_assert_true(verify_security_tag + ("snap.foo_bar.hook.bar-baz", "foo_bar")); + + // Now, test the names we know are bad + g_assert_false(verify_security_tag + ("pkg-foo.bar.0binary-bar+baz", "bar")); + 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", "name")); + g_assert_false(verify_security_tag("snap.name.", "name")); + g_assert_false(verify_security_tag("snap.name.app.", "name")); + g_assert_false(verify_security_tag("snap.name.hook.", "name")); + g_assert_false(verify_security_tag("snap!name.app", "!name")); + g_assert_false(verify_security_tag("snap.-name.app", "-name")); + g_assert_false(verify_security_tag("snap.name!app", "name!")); + g_assert_false(verify_security_tag("snap.name.-app", "name")); + g_assert_false(verify_security_tag("snap.name.app!hook.foo", "name")); + g_assert_false(verify_security_tag("snap.name.app.hook!foo", "name")); + g_assert_false(verify_security_tag("snap.name.app.hook.-foo", "name")); + g_assert_false(verify_security_tag("snap.name.app.hook.f00", "name")); + g_assert_false(verify_security_tag("sna.pname.app", "pname")); + g_assert_false(verify_security_tag("snap.n@me.app", "n@me")); + g_assert_false(verify_security_tag("SNAP.name.app", "name")); + g_assert_false(verify_security_tag("snap.Name.app", "Name")); + // This used to be false but it's now allowed. + g_assert_true(verify_security_tag("snap.0name.app", "0name")); + g_assert_false(verify_security_tag("snap.-name.app", "-name")); + g_assert_false(verify_security_tag("snap.name.@app", "name")); + g_assert_false(verify_security_tag(".name.app", "name")); + g_assert_false(verify_security_tag("snap..name.app", ".name")); + g_assert_false(verify_security_tag("snap.name..app", "name.")); + g_assert_false(verify_security_tag("snap.name.app..", "name")); + // These contain invalid instance key + g_assert_false(verify_security_tag("snap.foo_.bar-baz", "foo")); + g_assert_false(verify_security_tag + ("snap.foo_toolonginstance.bar-baz", "foo")); + g_assert_false(verify_security_tag + ("snap.foo_inst@nace.bar-baz", "foo")); + g_assert_false(verify_security_tag + ("snap.foo_in-stan-ce.bar-baz", "foo")); + g_assert_false(verify_security_tag("snap.foo_in stan.bar-baz", "foo")); + + // Test names that are both good, but snap name doesn't match security tag + g_assert_false(verify_security_tag("snap.foo.hook.bar", "fo")); + g_assert_false(verify_security_tag("snap.foo.hook.bar", "fooo")); + g_assert_false(verify_security_tag("snap.foo.hook.bar", "snap")); + g_assert_false(verify_security_tag("snap.foo.hook.bar", "bar")); + g_assert_false(verify_security_tag("snap.foo_instance.bar", "foo_bar")); + + // Regression test 12to8 + g_assert_true(verify_security_tag("snap.12to8.128to8", "12to8")); + g_assert_true(verify_security_tag("snap.123test.123test", "123test")); + g_assert_true(verify_security_tag + ("snap.123test.hook.configure", "123test")); +} + +static void test_sc_is_hook_security_tag(void) +{ + // First, test the names we know are good + g_assert_true(sc_is_hook_security_tag("snap.foo.hook.bar")); + g_assert_true(sc_is_hook_security_tag("snap.foo.hook.bar-baz")); + g_assert_true(sc_is_hook_security_tag + ("snap.foo_instance.hook.bar-baz")); + g_assert_true(sc_is_hook_security_tag("snap.foo_bar.hook.bar-baz")); + + // Now, test the names we know are not valid hook security tags + g_assert_false(sc_is_hook_security_tag("snap.foo_instance.bar-baz")); + g_assert_false(sc_is_hook_security_tag("snap.name.app!hook.foo")); + g_assert_false(sc_is_hook_security_tag("snap.name.app.hook!foo")); + g_assert_false(sc_is_hook_security_tag("snap.name.app.hook.-foo")); + g_assert_false(sc_is_hook_security_tag("snap.name.app.hook.f00")); +} + +static void test_sc_snap_or_instance_name_validate(gconstpointer data) +{ + typedef void (*validate_func_t) (const char *, struct sc_error **); + + validate_func_t validate = (validate_func_t) data; + bool is_instance = + (validate == sc_instance_name_validate) ? true : false; + + struct sc_error *err = NULL; + + // Smoke test, a valid snap name + validate("hello-world", &err); + g_assert_null(err); + + // Smoke test: invalid character + validate("hello world", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must use lower case letters, digits or dashes"); + sc_error_free(err); + + // Smoke test: no letters + validate("", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + // Smoke test: leading dash + validate("-foo", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name cannot start with a dash"); + sc_error_free(err); + + // Smoke test: trailing dash + validate("foo-", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name cannot end with a dash"); + sc_error_free(err); + + // Smoke test: double dash + validate("f--oo", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name cannot contain two consecutive dashes"); + sc_error_free(err); + + // Smoke test: NULL name is not valid + validate(NULL, &err); + g_assert_nonnull(err); + // the only case when instance name validation diverges from snap name + // validation + if (!is_instance) { + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name cannot be NULL"); + } else { + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, + SC_SNAP_INVALID_INSTANCE_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap instance name cannot be NULL"); + } + sc_error_free(err); + + const char *valid_names[] = { + "aa", "aaa", "aaaa", + "a-a", "aa-a", "a-aa", "a-b-c", + "a0", "a-0", "a-0a", + "01game", "1-or-2" + }; + for (size_t i = 0; i < sizeof valid_names / sizeof *valid_names; ++i) { + g_test_message("checking valid snap name: %s", valid_names[i]); + validate(valid_names[i], &err); + g_assert_null(err); + } + const char *invalid_names[] = { + // name cannot be empty + "", + // too short + "a", + // names cannot be too long + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", + "1111111111111111111111111111111111111111x", + "x1111111111111111111111111111111111111111", + "x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x", + // dashes alone are not a name + "-", "--", + // double dashes in a name are not allowed + "a--a", + // name should not end with a dash + "a-", + // name cannot have any spaces in it + "a ", " a", "a a", + // a number alone is not a name + "0", "123", "1-2-3", + // identifier must be plain ASCII + "日本語", "한글", "ру́сский язы́к", + }; + for (size_t i = 0; i < sizeof invalid_names / sizeof *invalid_names; + ++i) { + g_test_message("checking invalid snap name: >%s<", + invalid_names[i]); + validate(invalid_names[i], &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + sc_error_free(err); + } + // Regression test: 12to8 and 123test + validate("12to8", &err); + g_assert_null(err); + validate("123test", &err); + g_assert_null(err); + + // In case we switch to a regex, here's a test that could break things. + const char good_bad_name[] = "u-94903713687486543234157734673284536758"; + char varname[sizeof good_bad_name] = { 0 }; + for (size_t i = 3; i <= sizeof varname - 1; i++) { + g_assert_nonnull(memcpy(varname, good_bad_name, i)); + varname[i] = 0; + g_test_message("checking valid snap name: >%s<", varname); + validate(varname, &err); + g_assert_null(err); + sc_error_free(err); + } +} + +static void test_sc_snap_name_validate__respects_error_protocol(void) +{ + if (g_test_subprocess()) { + sc_snap_name_validate("hello world", NULL); + g_test_message("expected sc_snap_name_validate to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("snap name must use lower case letters, digits or dashes\n"); +} + +static void test_sc_instance_name_validate(void) +{ + struct sc_error *err = NULL; + + sc_instance_name_validate("hello-world", &err); + g_assert_null(err); + sc_instance_name_validate("hello-world_foo", &err); + g_assert_null(err); + + // just the separator + sc_instance_name_validate("_", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + // just name, with separator, missing instance key + sc_instance_name_validate("hello-world_", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY)); + g_assert_cmpstr(sc_error_msg(err), ==, + "instance key must contain at least one letter or digit"); + sc_error_free(err); + + // only separator and instance key, missing name + sc_instance_name_validate("_bar", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + sc_instance_name_validate("", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + // third separator + sc_instance_name_validate("foo_bar_baz", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap instance name can contain only one underscore"); + sc_error_free(err); + + const char *valid_names[] = { + "aa", "aaa", "aaaa", + "aa_a", "aa_1", "aa_123", "aa_0123456789", + }; + for (size_t i = 0; i < sizeof valid_names / sizeof *valid_names; ++i) { + g_test_message("checking valid instance name: %s", + valid_names[i]); + sc_instance_name_validate(valid_names[i], &err); + g_assert_null(err); + } + const char *invalid_names[] = { + // too short + "a", + // only letters and digits in the instance key + "a_--23))", "a_ ", "a_091234#", "a_123_456", + // up to 10 characters for the instance key + "a_01234567891", "a_0123456789123", + // snap name must not be more than 40 characters, regardless of instance + // key + "01234567890123456789012345678901234567890_foobar", + "01234567890123456789-01234567890123456789_foobar", + // instance key must be plain ASCII + "foobar_日本語", + // way too many underscores + "foobar_baz_zed_daz", + "foobar______", + }; + for (size_t i = 0; i < sizeof invalid_names / sizeof *invalid_names; + ++i) { + g_test_message("checking invalid instance name: >%s<", + invalid_names[i]); + sc_instance_name_validate(invalid_names[i], &err); + g_assert_nonnull(err); + sc_error_free(err); + } +} + +static void test_sc_snap_drop_instance_key_no_dest(void) +{ + if (g_test_subprocess()) { + sc_snap_drop_instance_key("foo_bar", NULL, 0); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + +} + +static void test_sc_snap_drop_instance_key_short_dest(void) +{ + if (g_test_subprocess()) { + char dest[10] = { 0 }; + sc_snap_drop_instance_key("foo-foo-foo-foo-foo_bar", dest, + sizeof dest); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_snap_drop_instance_key_short_dest2(void) +{ + if (g_test_subprocess()) { + char dest[3] = { 0 }; // "foo" sans the nil byte + sc_snap_drop_instance_key("foo", dest, sizeof dest); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_snap_drop_instance_key_no_name(void) +{ + if (g_test_subprocess()) { + char dest[10] = { 0 }; + sc_snap_drop_instance_key(NULL, dest, sizeof dest); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_snap_drop_instance_key_basic(void) +{ + char name[41] = { 0xff }; + + sc_snap_drop_instance_key("foo_bar", name, sizeof name); + g_assert_cmpstr(name, ==, "foo"); + + memset(name, 0xff, sizeof name); + sc_snap_drop_instance_key("foo-bar_bar", name, sizeof name); + g_assert_cmpstr(name, ==, "foo-bar"); + + memset(name, 0xff, sizeof name); + sc_snap_drop_instance_key("foo-bar", name, sizeof name); + g_assert_cmpstr(name, ==, "foo-bar"); + + memset(name, 0xff, sizeof name); + sc_snap_drop_instance_key("_baz", name, sizeof name); + g_assert_cmpstr(name, ==, ""); + + memset(name, 0xff, sizeof name); + sc_snap_drop_instance_key("foo", name, sizeof name); + g_assert_cmpstr(name, ==, "foo"); +} + +static void test_sc_snap_split_instance_name_trailing_nil(void) +{ + if (g_test_subprocess()) { + char dest[3] = { 0 }; + // pretend there is no place for trailing \0 + sc_snap_split_instance_name("_", NULL, 0, dest, 0); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_snap_split_instance_name_short_instance_dest(void) +{ + if (g_test_subprocess()) { + char dest[10] = { 0 }; + sc_snap_split_instance_name("foo_barbarbarbar", NULL, 0, + dest, sizeof dest); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void test_sc_snap_split_instance_name_basic(void) +{ + char name[41] = { 0xff }; + char instance[20] = { 0xff }; + + sc_snap_split_instance_name("foo_bar", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, "foo"); + g_assert_cmpstr(instance, ==, "bar"); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("foo-bar_bar", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, "foo-bar"); + g_assert_cmpstr(instance, ==, "bar"); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("foo-bar", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, "foo-bar"); + g_assert_cmpstr(instance, ==, ""); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("_baz", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, ""); + g_assert_cmpstr(instance, ==, "baz"); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("foo", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, "foo"); + g_assert_cmpstr(instance, ==, ""); + + memset(name, 0xff, sizeof name); + sc_snap_split_instance_name("foo_bar", name, sizeof name, NULL, 0); + g_assert_cmpstr(name, ==, "foo"); + + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("foo_bar", NULL, 0, instance, + sizeof instance); + g_assert_cmpstr(instance, ==, "bar"); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("hello_world_surprise", name, sizeof name, + instance, sizeof instance); + g_assert_cmpstr(name, ==, "hello"); + g_assert_cmpstr(instance, ==, "world_surprise"); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, ""); + g_assert_cmpstr(instance, ==, ""); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("_", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, ""); + g_assert_cmpstr(instance, ==, ""); + + memset(name, 0xff, sizeof name); + memset(instance, 0xff, sizeof instance); + sc_snap_split_instance_name("foo_", name, sizeof name, instance, + sizeof instance); + g_assert_cmpstr(name, ==, "foo"); + g_assert_cmpstr(instance, ==, ""); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/snap/verify_security_tag", test_verify_security_tag); + g_test_add_func("/snap/sc_is_hook_security_tag", + test_sc_is_hook_security_tag); + + g_test_add_data_func("/snap/sc_snap_name_validate", + sc_snap_name_validate, + test_sc_snap_or_instance_name_validate); + g_test_add_func("/snap/sc_snap_name_validate/respects_error_protocol", + test_sc_snap_name_validate__respects_error_protocol); + + g_test_add_data_func("/snap/sc_instance_name_validate/just_name", + sc_instance_name_validate, + test_sc_snap_or_instance_name_validate); + g_test_add_func("/snap/sc_instance_name_validate/full", + test_sc_instance_name_validate); + + g_test_add_func("/snap/sc_snap_drop_instance_key/basic", + test_sc_snap_drop_instance_key_basic); + g_test_add_func("/snap/sc_snap_drop_instance_key/no_dest", + test_sc_snap_drop_instance_key_no_dest); + g_test_add_func("/snap/sc_snap_drop_instance_key/no_name", + test_sc_snap_drop_instance_key_no_name); + g_test_add_func("/snap/sc_snap_drop_instance_key/short_dest", + test_sc_snap_drop_instance_key_short_dest); + g_test_add_func("/snap/sc_snap_drop_instance_key/short_dest2", + test_sc_snap_drop_instance_key_short_dest2); + + g_test_add_func("/snap/sc_snap_split_instance_name/basic", + test_sc_snap_split_instance_name_basic); + g_test_add_func("/snap/sc_snap_split_instance_name/trailing_nil", + test_sc_snap_split_instance_name_trailing_nil); + g_test_add_func("/snap/sc_snap_split_instance_name/short_instance_dest", + test_sc_snap_split_instance_name_short_instance_dest); +} diff --git a/cmd/libsnap-confine-private/snap.c b/cmd/libsnap-confine-private/snap.c new file mode 100644 index 00000000..d78942b3 --- /dev/null +++ b/cmd/libsnap-confine-private/snap.c @@ -0,0 +1,314 @@ +/* + * 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 +#include +#include + +#include "utils.h" +#include "string-utils.h" +#include "cleanup-funcs.h" + +bool verify_security_tag(const char *security_tag, const char *snap_name) +{ + const char *whitelist_re = + "^snap\\.([a-z0-9](-?[a-z0-9])*(_[a-z0-9]{1,10})?)\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z])*)$"; + regex_t re; + if (regcomp(&re, whitelist_re, REG_EXTENDED) != 0) + die("can not compile regex %s", whitelist_re); + + // first capture is for verifying the full security tag, second capture + // for verifying the snap_name is correct for this security tag + regmatch_t matches[2]; + int status = + regexec(&re, security_tag, sizeof matches / sizeof *matches, + matches, 0); + regfree(&re); + + // Fail if no match or if snap name wasn't captured in the 2nd match group + if (status != 0 || matches[1].rm_so < 0) { + return false; + } + + size_t len = matches[1].rm_eo - matches[1].rm_so; + return len == strlen(snap_name) + && strncmp(security_tag + matches[1].rm_so, snap_name, len) == 0; +} + +bool sc_is_hook_security_tag(const char *security_tag) +{ + const char *whitelist_re = + "^snap\\.[a-z](-?[a-z0-9])*(_[a-z0-9]{1,10})?\\.(hook\\.[a-z](-?[a-z])*)$"; + + regex_t re; + if (regcomp(&re, whitelist_re, REG_EXTENDED | REG_NOSUB) != 0) + die("can not compile regex %s", whitelist_re); + + int status = regexec(&re, security_tag, 0, NULL, 0); + regfree(&re); + + return status == 0; +} + +static int skip_lowercase_letters(const char **p) +{ + int skipped = 0; + for (const char *c = *p; *c >= 'a' && *c <= 'z'; ++c) { + skipped += 1; + } + *p = (*p) + skipped; + return skipped; +} + +static int skip_digits(const char **p) +{ + int skipped = 0; + for (const char *c = *p; *c >= '0' && *c <= '9'; ++c) { + skipped += 1; + } + *p = (*p) + skipped; + return skipped; +} + +static int skip_one_char(const char **p, char c) +{ + if (**p == c) { + *p += 1; + return 1; + } + return 0; +} + +void sc_instance_name_validate(const char *instance_name, + struct sc_error **errorp) +{ + // NOTE: This function should be synchronized with the two other + // implementations: validate_instance_name and snap.ValidateInstanceName. + struct sc_error *err = NULL; + + // Ensure that name is not NULL + if (instance_name == NULL) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_NAME, + "snap instance name cannot be NULL"); + goto out; + } + // 40 char snap_name + '_' + 10 char instance_key + 1 extra overflow + 1 + // NULL + char s[53] = { 0 }; + strncpy(s, instance_name, sizeof(s) - 1); + + char *t = s; + const char *snap_name = strsep(&t, "_"); + const char *instance_key = strsep(&t, "_"); + const char *third_separator = strsep(&t, "_"); + if (third_separator != NULL) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_NAME, + "snap instance name can contain only one underscore"); + goto out; + } + + sc_snap_name_validate(snap_name, &err); + if (err != NULL) { + goto out; + } + // When the instance_name is a normal snap name, instance_key will be + // NULL, so only validate instance_key when we found one. + if (instance_key != NULL) { + sc_instance_key_validate(instance_key, &err); + } + + out: + sc_error_forward(errorp, err); +} + +void sc_instance_key_validate(const char *instance_key, + struct sc_error **errorp) +{ + // NOTE: see snap.ValidateInstanceName for reference of a valid instance key + // format + struct sc_error *err = NULL; + + // Ensure that name is not NULL + if (instance_key == NULL) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "instance key cannot be NULL"); + goto out; + } + // This is a regexp-free routine hand-coding the following pattern: + // + // "^[a-z]{1,10}$" + // + // The only motivation for not using regular expressions is so that we don't + // run untrusted input against a potentially complex regular expression + // engine. + int i = 0; + for (i = 0; instance_key[i] != '\0'; i++) { + if (islower(instance_key[i]) || isdigit(instance_key[i])) { + continue; + } + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY, + "instance key must use lower case letters or digits"); + goto out; + } + + if (i == 0) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY, + "instance key must contain at least one letter or digit"); + } else if (i > 10) { + err = + sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY, + "instance key must be shorter than 10 characters"); + } + out: + sc_error_forward(errorp, err); +} + +void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp) +{ + // NOTE: This function should be synchronized with the two other + // implementations: validate_snap_name and snap.ValidateName. + struct sc_error *err = NULL; + + // Ensure that name is not NULL + if (snap_name == NULL) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name cannot be NULL"); + goto out; + } + // This is a regexp-free routine hand-codes the following pattern: + // + // "^([a-z0-9]+-?)*[a-z](-?[a-z0-9])*$" + // + // The only motivation for not using regular expressions is so that we + // don't run untrusted input against a potentially complex regular + // expression engine. + const char *p = snap_name; + if (skip_one_char(&p, '-')) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name cannot start with a dash"); + goto out; + } + bool got_letter = false; + int n = 0, m; + for (; *p != '\0';) { + if ((m = skip_lowercase_letters(&p)) > 0) { + n += m; + got_letter = true; + continue; + } + if ((m = skip_digits(&p)) > 0) { + n += m; + continue; + } + if (skip_one_char(&p, '-') > 0) { + n++; + if (*p == '\0') { + err = + sc_error_init(SC_SNAP_DOMAIN, + SC_SNAP_INVALID_NAME, + "snap name cannot end with a dash"); + goto out; + } + if (skip_one_char(&p, '-') > 0) { + err = + sc_error_init(SC_SNAP_DOMAIN, + SC_SNAP_INVALID_NAME, + "snap name cannot contain two consecutive dashes"); + goto out; + } + continue; + } + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name must use lower case letters, digits or dashes"); + goto out; + } + if (!got_letter) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name must contain at least one letter"); + goto out; + } + if (n < 2) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name must be longer than 1 character"); + goto out; + } + if (n > 40) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name must be shorter than 40 characters"); + goto out; + } + + out: + sc_error_forward(errorp, err); +} + +void sc_snap_drop_instance_key(const char *instance_name, char *snap_name, + size_t snap_name_size) +{ + sc_snap_split_instance_name(instance_name, snap_name, snap_name_size, + NULL, 0); +} + +void sc_snap_split_instance_name(const char *instance_name, char *snap_name, + size_t snap_name_size, char *instance_key, + size_t instance_key_size) +{ + if (instance_name == NULL) { + die("internal error: cannot split instance name when it is unset"); + } + if (snap_name == NULL && instance_key == NULL) { + die("internal error: cannot split instance name when both snap name and instance key are unset"); + } + + const char *pos = strchr(instance_name, '_'); + const char *instance_key_start = ""; + size_t snap_name_len = 0; + size_t instance_key_len = 0; + if (pos == NULL) { + snap_name_len = strlen(instance_name); + } else { + snap_name_len = pos - instance_name; + instance_key_start = pos + 1; + instance_key_len = strlen(instance_key_start); + } + + if (snap_name != NULL) { + if (snap_name_len >= snap_name_size) { + die("snap name buffer too small"); + } + + memcpy(snap_name, instance_name, snap_name_len); + snap_name[snap_name_len] = '\0'; + } + + if (instance_key != NULL) { + if (instance_key_len >= instance_key_size) { + die("instance key buffer too small"); + } + memcpy(instance_key, instance_key_start, instance_key_len); + instance_key[instance_key_len] = '\0'; + } +} diff --git a/cmd/libsnap-confine-private/snap.h b/cmd/libsnap-confine-private/snap.h new file mode 100644 index 00000000..49851289 --- /dev/null +++ b/cmd/libsnap-confine-private/snap.h @@ -0,0 +1,119 @@ +/* + * 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_SNAP_H +#define SNAP_CONFINE_SNAP_H + +#include +#include + +#include "error.h" + +/** + * Error domain for errors related to the snap module. + **/ +#define SC_SNAP_DOMAIN "snap" + +enum { + /** The name of the snap is not valid. */ + SC_SNAP_INVALID_NAME = 1, + /** The instance key of the snap is not valid. */ + SC_SNAP_INVALID_INSTANCE_KEY = 2, + /** The instance of the snap is not valid. */ + SC_SNAP_INVALID_INSTANCE_NAME = 3, +}; + +/** + * Validate the given snap name. + * + * Valid name cannot be NULL and must match a regular expression describing the + * strict naming requirements. Please refer to snapd source code for details. + * + * The error protocol is observed so if the caller doesn't provide an outgoing + * error pointer the function will die on any error. + **/ +void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp); + +/** + * Validate the given instance key. + * + * Valid instance key cannot be NULL and must match a regular expression + * describing the strict naming requirements. Please refer to snapd source code + * for details. + * + * The error protocol is observed so if the caller doesn't provide an outgoing + * error pointer the function will die on any error. + **/ +void sc_instance_key_validate(const char *instance_key, + struct sc_error **errorp); + +/** + * Validate the given snap instance name. + * + * Valid instance name must be composed of a valid snap name and a valid + * instance key. + * + * The error protocol is observed so if the caller doesn't provide an outgoing + * error pointer the function will die on any error. + **/ +void sc_instance_name_validate(const char *instance_name, + struct sc_error **errorp); + +/** + * Validate security tag against strict naming requirements and snap name. + * + * The executable name is of form: + * snap..(|hook.) + * - must start with lowercase letter, then may contain + * lowercase alphanumerics and '-'; it must match snap_name + * - may contain alphanumerics and '-' + * - snap, just-snap => just-snap + **/ +void sc_snap_drop_instance_key(const char *instance_name, char *snap_name, + size_t snap_name_size); + +/** + * Extract snap name and instance key out of an instance name. + * + * A snap may be installed multiple times in parallel under distinct instance + * names. This function extracts the snap name and instance key out of the + * instance name. One of snap_name, instance_key must be non-NULL. + * + * For example: + * name_instance => "name" & "instance" + * just-name => "just-name" & "" + * + **/ +void sc_snap_split_instance_name(const char *instance_name, char *snap_name, + size_t snap_name_size, char *instance_key, + size_t instance_key_size); + +#endif diff --git a/cmd/libsnap-confine-private/string-utils-test.c b/cmd/libsnap-confine-private/string-utils-test.c new file mode 100644 index 00000000..5892d881 --- /dev/null +++ b/cmd/libsnap-confine-private/string-utils-test.c @@ -0,0 +1,848 @@ +/* + * Copyright (C) 2016-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 "string-utils.h" +#include "string-utils.c" + +#include + +static void test_sc_streq(void) +{ + g_assert_false(sc_streq(NULL, NULL)); + g_assert_false(sc_streq(NULL, "text")); + g_assert_false(sc_streq("text", NULL)); + g_assert_false(sc_streq("foo", "bar")); + g_assert_false(sc_streq("foo", "barbar")); + g_assert_false(sc_streq("foofoo", "bar")); + g_assert_true(sc_streq("text", "text")); + g_assert_true(sc_streq("", "")); +} + +static void test_sc_endswith(void) +{ + // NULL doesn't end with anything, nothing ends with NULL + g_assert_false(sc_endswith("", NULL)); + g_assert_false(sc_endswith(NULL, "")); + g_assert_false(sc_endswith(NULL, NULL)); + // Empty string ends with an empty string + g_assert_true(sc_endswith("", "")); + // Ends-with (matches) + g_assert_true(sc_endswith("foobar", "bar")); + g_assert_true(sc_endswith("foobar", "ar")); + g_assert_true(sc_endswith("foobar", "r")); + g_assert_true(sc_endswith("foobar", "")); + g_assert_true(sc_endswith("bar", "bar")); + // Ends-with (non-matches) + g_assert_false(sc_endswith("foobar", "quux")); + g_assert_false(sc_endswith("", "bar")); + g_assert_false(sc_endswith("b", "bar")); + g_assert_false(sc_endswith("ba", "bar")); +} + +static void test_sc_must_snprintf(void) +{ + char buf[5] = { 0 }; + sc_must_snprintf(buf, sizeof buf, "1234"); + g_assert_cmpstr(buf, ==, "1234"); +} + +static void test_sc_must_snprintf__fail(void) +{ + if (g_test_subprocess()) { + char buf[5]; + sc_must_snprintf(buf, sizeof buf, "12345"); + g_test_message("expected sc_must_snprintf not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot format string: 1234\n"); +} + +// Check that appending to a buffer works OK. +static void test_sc_string_append(void) +{ + union { + char bigbuf[6]; + struct { + signed char canary1; + char buf[4]; + signed char canary2; + }; + } data = { + .buf = { + 'f', '\0', 0xFF, 0xFF},.canary1 = ~0,.canary2 = ~0,}; + + // Sanity check, ensure that the layout of structures is as spelled above. + // (first canary1, then buf and finally canary2. + g_assert_cmpint(((char *)&data.buf[0]) - ((char *)&data.canary1), ==, + 1); + g_assert_cmpint(((char *)&data.buf[4]) - ((char *)&data.canary2), ==, + 0); + + sc_string_append(data.buf, sizeof data.buf, "oo"); + + // Check that we didn't corrupt either canary. + g_assert_cmpint(data.canary1, ==, ~0); + g_assert_cmpint(data.canary2, ==, ~0); + + // Check that we got the result that was expected. + g_assert_cmpstr(data.buf, ==, "foo"); +} + +// Check that appending an empty string to a full buffer is valid. +static void test_sc_string_append__empty_to_full(void) +{ + union { + char bigbuf[6]; + struct { + signed char canary1; + char buf[4]; + signed char canary2; + }; + } data = { + .buf = { + 'f', 'o', 'o', '\0'},.canary1 = ~0,.canary2 = ~0,}; + + // Sanity check, ensure that the layout of structures is as spelled above. + // (first canary1, then buf and finally canary2. + g_assert_cmpint(((char *)&data.buf[0]) - ((char *)&data.canary1), ==, + 1); + g_assert_cmpint(((char *)&data.buf[4]) - ((char *)&data.canary2), ==, + 0); + + sc_string_append(data.buf, sizeof data.buf, ""); + + // Check that we didn't corrupt either canary. + g_assert_cmpint(data.canary1, ==, ~0); + g_assert_cmpint(data.canary2, ==, ~0); + + // Check that we got the result that was expected. + g_assert_cmpstr(data.buf, ==, "foo"); +} + +// Check that the overflow detection works. +static void test_sc_string_append__overflow(void) +{ + if (g_test_subprocess()) { + char buf[4] = { 0 }; + + // Try to append a string that's one character too long. + sc_string_append(buf, sizeof buf, "1234"); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append string: str is too long or unterminated\n"); +} + +// Check that the uninitialized buffer detection works. +static void test_sc_string_append__uninitialized_buf(void) +{ + if (g_test_subprocess()) { + char buf[4] = { 0xFF, 0xFF, 0xFF, 0xFF }; + + // Try to append a string to a buffer which is not a valic C-string. + sc_string_append(buf, sizeof buf, ""); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append string: dst is unterminated\n"); +} + +// Check that `buf' cannot be NULL. +static void test_sc_string_append__NULL_buf(void) +{ + if (g_test_subprocess()) { + char buf[4]; + + sc_string_append(NULL, sizeof buf, "foo"); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot append string: buffer is NULL\n"); +} + +// Check that `src' cannot be NULL. +static void test_sc_string_append__NULL_str(void) +{ + if (g_test_subprocess()) { + char buf[4]; + + sc_string_append(buf, sizeof buf, NULL); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot append string: string is NULL\n"); +} + +static void test_sc_string_init__normal(void) +{ + char buf[1] = { 0xFF }; + + sc_string_init(buf, sizeof buf); + g_assert_cmpint(buf[0], ==, 0); +} + +static void test_sc_string_init__empty_buf(void) +{ + if (g_test_subprocess()) { + char buf[1] = { 0xFF }; + + sc_string_init(buf, 0); + + g_test_message("expected sc_string_init not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot initialize string, buffer is too small\n"); +} + +static void test_sc_string_init__NULL_buf(void) +{ + if (g_test_subprocess()) { + sc_string_init(NULL, 1); + + g_test_message("expected sc_string_init not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot initialize string, buffer is NULL\n"); +} + +static void test_sc_string_append_char__uninitialized_buf(void) +{ + if (g_test_subprocess()) { + char buf[2] = { 0xFF, 0xFF }; + sc_string_append_char(buf, sizeof buf, 'a'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character: dst is unterminated\n"); +} + +static void test_sc_string_append_char__NULL_buf(void) +{ + if (g_test_subprocess()) { + sc_string_append_char(NULL, 2, 'a'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot append character: buffer is NULL\n"); +} + +static void test_sc_string_append_char__overflow(void) +{ + if (g_test_subprocess()) { + char buf[1] = { 0 }; + sc_string_append_char(buf, sizeof buf, 'a'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character: not enough space\n"); +} + +static void test_sc_string_append_char__invalid_zero(void) +{ + if (g_test_subprocess()) { + char buf[2] = { 0 }; + sc_string_append_char(buf, sizeof buf, '\0'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character: cannot append string terminator\n"); +} + +static void test_sc_string_append_char__normal(void) +{ + char buf[16]; + size_t len; + sc_string_init(buf, sizeof buf); + + len = sc_string_append_char(buf, sizeof buf, 'h'); + g_assert_cmpstr(buf, ==, "h"); + g_assert_cmpint(len, ==, 1); + len = sc_string_append_char(buf, sizeof buf, 'e'); + g_assert_cmpstr(buf, ==, "he"); + g_assert_cmpint(len, ==, 2); + len = sc_string_append_char(buf, sizeof buf, 'l'); + g_assert_cmpstr(buf, ==, "hel"); + g_assert_cmpint(len, ==, 3); + len = sc_string_append_char(buf, sizeof buf, 'l'); + g_assert_cmpstr(buf, ==, "hell"); + g_assert_cmpint(len, ==, 4); + len = sc_string_append_char(buf, sizeof buf, 'o'); + g_assert_cmpstr(buf, ==, "hello"); + g_assert_cmpint(len, ==, 5); +} + +static void test_sc_string_append_char_pair__uninitialized_buf(void) +{ + if (g_test_subprocess()) { + char buf[3] = { 0xFF, 0xFF, 0xFF }; + sc_string_append_char_pair(buf, sizeof buf, 'a', 'b'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: dst is unterminated\n"); +} + +static void test_sc_string_append_char_pair__NULL_buf(void) +{ + if (g_test_subprocess()) { + sc_string_append_char_pair(NULL, 3, 'a', 'b'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: buffer is NULL\n"); +} + +static void test_sc_string_append_char_pair__overflow(void) +{ + if (g_test_subprocess()) { + char buf[2] = { 0 }; + sc_string_append_char_pair(buf, sizeof buf, 'a', 'b'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: not enough space\n"); +} + +static void test_sc_string_append_char_pair__invalid_zero_c1(void) +{ + if (g_test_subprocess()) { + char buf[3] = { 0 }; + sc_string_append_char_pair(buf, sizeof buf, '\0', 'a'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: cannot append string terminator\n"); +} + +static void test_sc_string_append_char_pair__invalid_zero_c2(void) +{ + if (g_test_subprocess()) { + char buf[3] = { 0 }; + sc_string_append_char_pair(buf, sizeof buf, 'a', '\0'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: cannot append string terminator\n"); +} + +static void test_sc_string_append_char_pair__normal(void) +{ + char buf[16]; + size_t len; + sc_string_init(buf, sizeof buf); + + len = sc_string_append_char_pair(buf, sizeof buf, 'h', 'e'); + g_assert_cmpstr(buf, ==, "he"); + g_assert_cmpint(len, ==, 2); + len = sc_string_append_char_pair(buf, sizeof buf, 'l', 'l'); + g_assert_cmpstr(buf, ==, "hell"); + g_assert_cmpint(len, ==, 4); + len = sc_string_append_char_pair(buf, sizeof buf, 'o', '!'); + g_assert_cmpstr(buf, ==, "hello!"); + g_assert_cmpint(len, ==, 6); +} + +static void test_sc_string_quote_NULL_str(void) +{ + if (g_test_subprocess()) { + char buf[16] = { 0 }; + sc_string_quote(buf, sizeof buf, NULL); + + g_test_message("expected sc_string_quote not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot quote string: string is NULL\n"); +} + +static void test_quoting_of(bool tested[], int c, const char *expected) +{ + char buf[16]; + + g_assert_cmpint(c, >=, 0); + g_assert_cmpint(c, <=, 255); + + // Create an input string with one character. + char input[2] = { (unsigned char)c, 0 }; + sc_string_quote(buf, sizeof buf, input); + + // Ensure it was quoted as we expected. + g_assert_cmpstr(buf, ==, expected); + + tested[c] = true; +} + +static void test_sc_string_quote(void) +{ +#define DQ "\"" + char buf[16]; + bool is_tested[256] = { false }; + + // Exhaustive test for quoting of every 8bit input. This is very verbose + // but the goal is to have a very obvious and correct test that ensures no + // edge case is lost. + // + // block 1: 0x00 - 0x0f + test_quoting_of(is_tested, 0x00, DQ "" DQ); + test_quoting_of(is_tested, 0x01, DQ "\\x01" DQ); + test_quoting_of(is_tested, 0x02, DQ "\\x02" DQ); + test_quoting_of(is_tested, 0x03, DQ "\\x03" DQ); + test_quoting_of(is_tested, 0x04, DQ "\\x04" DQ); + test_quoting_of(is_tested, 0x05, DQ "\\x05" DQ); + test_quoting_of(is_tested, 0x06, DQ "\\x06" DQ); + test_quoting_of(is_tested, 0x07, DQ "\\x07" DQ); + test_quoting_of(is_tested, 0x08, DQ "\\x08" DQ); + test_quoting_of(is_tested, 0x09, DQ "\\t" DQ); + test_quoting_of(is_tested, 0x0a, DQ "\\n" DQ); + test_quoting_of(is_tested, 0x0b, DQ "\\v" DQ); + test_quoting_of(is_tested, 0x0c, DQ "\\x0c" DQ); + test_quoting_of(is_tested, 0x0d, DQ "\\r" DQ); + test_quoting_of(is_tested, 0x0e, DQ "\\x0e" DQ); + test_quoting_of(is_tested, 0x0f, DQ "\\x0f" DQ); + // block 2: 0x10 - 0x1f + test_quoting_of(is_tested, 0x10, DQ "\\x10" DQ); + test_quoting_of(is_tested, 0x11, DQ "\\x11" DQ); + test_quoting_of(is_tested, 0x12, DQ "\\x12" DQ); + test_quoting_of(is_tested, 0x13, DQ "\\x13" DQ); + test_quoting_of(is_tested, 0x14, DQ "\\x14" DQ); + test_quoting_of(is_tested, 0x15, DQ "\\x15" DQ); + test_quoting_of(is_tested, 0x16, DQ "\\x16" DQ); + test_quoting_of(is_tested, 0x17, DQ "\\x17" DQ); + test_quoting_of(is_tested, 0x18, DQ "\\x18" DQ); + test_quoting_of(is_tested, 0x19, DQ "\\x19" DQ); + test_quoting_of(is_tested, 0x1a, DQ "\\x1a" DQ); + test_quoting_of(is_tested, 0x1b, DQ "\\x1b" DQ); + test_quoting_of(is_tested, 0x1c, DQ "\\x1c" DQ); + test_quoting_of(is_tested, 0x1d, DQ "\\x1d" DQ); + test_quoting_of(is_tested, 0x1e, DQ "\\x1e" DQ); + test_quoting_of(is_tested, 0x1f, DQ "\\x1f" DQ); + // block 3: 0x20 - 0x2f + test_quoting_of(is_tested, 0x20, DQ " " DQ); + test_quoting_of(is_tested, 0x21, DQ "!" DQ); + test_quoting_of(is_tested, 0x22, DQ "\\\"" DQ); + test_quoting_of(is_tested, 0x23, DQ "#" DQ); + test_quoting_of(is_tested, 0x24, DQ "$" DQ); + test_quoting_of(is_tested, 0x25, DQ "%" DQ); + test_quoting_of(is_tested, 0x26, DQ "&" DQ); + test_quoting_of(is_tested, 0x27, DQ "'" DQ); + test_quoting_of(is_tested, 0x28, DQ "(" DQ); + test_quoting_of(is_tested, 0x29, DQ ")" DQ); + test_quoting_of(is_tested, 0x2a, DQ "*" DQ); + test_quoting_of(is_tested, 0x2b, DQ "+" DQ); + test_quoting_of(is_tested, 0x2c, DQ "," DQ); + test_quoting_of(is_tested, 0x2d, DQ "-" DQ); + test_quoting_of(is_tested, 0x2e, DQ "." DQ); + test_quoting_of(is_tested, 0x2f, DQ "/" DQ); + // block 4: 0x30 - 0x3f + test_quoting_of(is_tested, 0x30, DQ "0" DQ); + test_quoting_of(is_tested, 0x31, DQ "1" DQ); + test_quoting_of(is_tested, 0x32, DQ "2" DQ); + test_quoting_of(is_tested, 0x33, DQ "3" DQ); + test_quoting_of(is_tested, 0x34, DQ "4" DQ); + test_quoting_of(is_tested, 0x35, DQ "5" DQ); + test_quoting_of(is_tested, 0x36, DQ "6" DQ); + test_quoting_of(is_tested, 0x37, DQ "7" DQ); + test_quoting_of(is_tested, 0x38, DQ "8" DQ); + test_quoting_of(is_tested, 0x39, DQ "9" DQ); + test_quoting_of(is_tested, 0x3a, DQ ":" DQ); + test_quoting_of(is_tested, 0x3b, DQ ";" DQ); + test_quoting_of(is_tested, 0x3c, DQ "<" DQ); + test_quoting_of(is_tested, 0x3d, DQ "=" DQ); + test_quoting_of(is_tested, 0x3e, DQ ">" DQ); + test_quoting_of(is_tested, 0x3f, DQ "?" DQ); + // block 5: 0x40 - 0x4f + test_quoting_of(is_tested, 0x40, DQ "@" DQ); + test_quoting_of(is_tested, 0x41, DQ "A" DQ); + test_quoting_of(is_tested, 0x42, DQ "B" DQ); + test_quoting_of(is_tested, 0x43, DQ "C" DQ); + test_quoting_of(is_tested, 0x44, DQ "D" DQ); + test_quoting_of(is_tested, 0x45, DQ "E" DQ); + test_quoting_of(is_tested, 0x46, DQ "F" DQ); + test_quoting_of(is_tested, 0x47, DQ "G" DQ); + test_quoting_of(is_tested, 0x48, DQ "H" DQ); + test_quoting_of(is_tested, 0x49, DQ "I" DQ); + test_quoting_of(is_tested, 0x4a, DQ "J" DQ); + test_quoting_of(is_tested, 0x4b, DQ "K" DQ); + test_quoting_of(is_tested, 0x4c, DQ "L" DQ); + test_quoting_of(is_tested, 0x4d, DQ "M" DQ); + test_quoting_of(is_tested, 0x4e, DQ "N" DQ); + test_quoting_of(is_tested, 0x4f, DQ "O" DQ); + // block 6: 0x50 - 0x5f + test_quoting_of(is_tested, 0x50, DQ "P" DQ); + test_quoting_of(is_tested, 0x51, DQ "Q" DQ); + test_quoting_of(is_tested, 0x52, DQ "R" DQ); + test_quoting_of(is_tested, 0x53, DQ "S" DQ); + test_quoting_of(is_tested, 0x54, DQ "T" DQ); + test_quoting_of(is_tested, 0x55, DQ "U" DQ); + test_quoting_of(is_tested, 0x56, DQ "V" DQ); + test_quoting_of(is_tested, 0x57, DQ "W" DQ); + test_quoting_of(is_tested, 0x58, DQ "X" DQ); + test_quoting_of(is_tested, 0x59, DQ "Y" DQ); + test_quoting_of(is_tested, 0x5a, DQ "Z" DQ); + test_quoting_of(is_tested, 0x5b, DQ "[" DQ); + test_quoting_of(is_tested, 0x5c, DQ "\\\\" DQ); + test_quoting_of(is_tested, 0x5d, DQ "]" DQ); + test_quoting_of(is_tested, 0x5e, DQ "^" DQ); + test_quoting_of(is_tested, 0x5f, DQ "_" DQ); + // block 7: 0x60 - 0x6f + test_quoting_of(is_tested, 0x60, DQ "`" DQ); + test_quoting_of(is_tested, 0x61, DQ "a" DQ); + test_quoting_of(is_tested, 0x62, DQ "b" DQ); + test_quoting_of(is_tested, 0x63, DQ "c" DQ); + test_quoting_of(is_tested, 0x64, DQ "d" DQ); + test_quoting_of(is_tested, 0x65, DQ "e" DQ); + test_quoting_of(is_tested, 0x66, DQ "f" DQ); + test_quoting_of(is_tested, 0x67, DQ "g" DQ); + test_quoting_of(is_tested, 0x68, DQ "h" DQ); + test_quoting_of(is_tested, 0x69, DQ "i" DQ); + test_quoting_of(is_tested, 0x6a, DQ "j" DQ); + test_quoting_of(is_tested, 0x6b, DQ "k" DQ); + test_quoting_of(is_tested, 0x6c, DQ "l" DQ); + test_quoting_of(is_tested, 0x6d, DQ "m" DQ); + test_quoting_of(is_tested, 0x6e, DQ "n" DQ); + test_quoting_of(is_tested, 0x6f, DQ "o" DQ); + // block 8: 0x70 - 0x7f + test_quoting_of(is_tested, 0x70, DQ "p" DQ); + test_quoting_of(is_tested, 0x71, DQ "q" DQ); + test_quoting_of(is_tested, 0x72, DQ "r" DQ); + test_quoting_of(is_tested, 0x73, DQ "s" DQ); + test_quoting_of(is_tested, 0x74, DQ "t" DQ); + test_quoting_of(is_tested, 0x75, DQ "u" DQ); + test_quoting_of(is_tested, 0x76, DQ "v" DQ); + test_quoting_of(is_tested, 0x77, DQ "w" DQ); + test_quoting_of(is_tested, 0x78, DQ "x" DQ); + test_quoting_of(is_tested, 0x79, DQ "y" DQ); + test_quoting_of(is_tested, 0x7a, DQ "z" DQ); + test_quoting_of(is_tested, 0x7b, DQ "{" DQ); + test_quoting_of(is_tested, 0x7c, DQ "|" DQ); + test_quoting_of(is_tested, 0x7d, DQ "}" DQ); + test_quoting_of(is_tested, 0x7e, DQ "~" DQ); + test_quoting_of(is_tested, 0x7f, DQ "\\x7f" DQ); + // block 9 (8-bit): 0x80 - 0x8f + test_quoting_of(is_tested, 0x80, DQ "\\x80" DQ); + test_quoting_of(is_tested, 0x81, DQ "\\x81" DQ); + test_quoting_of(is_tested, 0x82, DQ "\\x82" DQ); + test_quoting_of(is_tested, 0x83, DQ "\\x83" DQ); + test_quoting_of(is_tested, 0x84, DQ "\\x84" DQ); + test_quoting_of(is_tested, 0x85, DQ "\\x85" DQ); + test_quoting_of(is_tested, 0x86, DQ "\\x86" DQ); + test_quoting_of(is_tested, 0x87, DQ "\\x87" DQ); + test_quoting_of(is_tested, 0x88, DQ "\\x88" DQ); + test_quoting_of(is_tested, 0x89, DQ "\\x89" DQ); + test_quoting_of(is_tested, 0x8a, DQ "\\x8a" DQ); + test_quoting_of(is_tested, 0x8b, DQ "\\x8b" DQ); + test_quoting_of(is_tested, 0x8c, DQ "\\x8c" DQ); + test_quoting_of(is_tested, 0x8d, DQ "\\x8d" DQ); + test_quoting_of(is_tested, 0x8e, DQ "\\x8e" DQ); + test_quoting_of(is_tested, 0x8f, DQ "\\x8f" DQ); + // block 10 (8-bit): 0x90 - 0x9f + test_quoting_of(is_tested, 0x90, DQ "\\x90" DQ); + test_quoting_of(is_tested, 0x91, DQ "\\x91" DQ); + test_quoting_of(is_tested, 0x92, DQ "\\x92" DQ); + test_quoting_of(is_tested, 0x93, DQ "\\x93" DQ); + test_quoting_of(is_tested, 0x94, DQ "\\x94" DQ); + test_quoting_of(is_tested, 0x95, DQ "\\x95" DQ); + test_quoting_of(is_tested, 0x96, DQ "\\x96" DQ); + test_quoting_of(is_tested, 0x97, DQ "\\x97" DQ); + test_quoting_of(is_tested, 0x98, DQ "\\x98" DQ); + test_quoting_of(is_tested, 0x99, DQ "\\x99" DQ); + test_quoting_of(is_tested, 0x9a, DQ "\\x9a" DQ); + test_quoting_of(is_tested, 0x9b, DQ "\\x9b" DQ); + test_quoting_of(is_tested, 0x9c, DQ "\\x9c" DQ); + test_quoting_of(is_tested, 0x9d, DQ "\\x9d" DQ); + test_quoting_of(is_tested, 0x9e, DQ "\\x9e" DQ); + test_quoting_of(is_tested, 0x9f, DQ "\\x9f" DQ); + // block 11 (8-bit): 0xa0 - 0xaf + test_quoting_of(is_tested, 0xa0, DQ "\\xa0" DQ); + test_quoting_of(is_tested, 0xa1, DQ "\\xa1" DQ); + test_quoting_of(is_tested, 0xa2, DQ "\\xa2" DQ); + test_quoting_of(is_tested, 0xa3, DQ "\\xa3" DQ); + test_quoting_of(is_tested, 0xa4, DQ "\\xa4" DQ); + test_quoting_of(is_tested, 0xa5, DQ "\\xa5" DQ); + test_quoting_of(is_tested, 0xa6, DQ "\\xa6" DQ); + test_quoting_of(is_tested, 0xa7, DQ "\\xa7" DQ); + test_quoting_of(is_tested, 0xa8, DQ "\\xa8" DQ); + test_quoting_of(is_tested, 0xa9, DQ "\\xa9" DQ); + test_quoting_of(is_tested, 0xaa, DQ "\\xaa" DQ); + test_quoting_of(is_tested, 0xab, DQ "\\xab" DQ); + test_quoting_of(is_tested, 0xac, DQ "\\xac" DQ); + test_quoting_of(is_tested, 0xad, DQ "\\xad" DQ); + test_quoting_of(is_tested, 0xae, DQ "\\xae" DQ); + test_quoting_of(is_tested, 0xaf, DQ "\\xaf" DQ); + // block 12 (8-bit): 0xb0 - 0xbf + test_quoting_of(is_tested, 0xb0, DQ "\\xb0" DQ); + test_quoting_of(is_tested, 0xb1, DQ "\\xb1" DQ); + test_quoting_of(is_tested, 0xb2, DQ "\\xb2" DQ); + test_quoting_of(is_tested, 0xb3, DQ "\\xb3" DQ); + test_quoting_of(is_tested, 0xb4, DQ "\\xb4" DQ); + test_quoting_of(is_tested, 0xb5, DQ "\\xb5" DQ); + test_quoting_of(is_tested, 0xb6, DQ "\\xb6" DQ); + test_quoting_of(is_tested, 0xb7, DQ "\\xb7" DQ); + test_quoting_of(is_tested, 0xb8, DQ "\\xb8" DQ); + test_quoting_of(is_tested, 0xb9, DQ "\\xb9" DQ); + test_quoting_of(is_tested, 0xba, DQ "\\xba" DQ); + test_quoting_of(is_tested, 0xbb, DQ "\\xbb" DQ); + test_quoting_of(is_tested, 0xbc, DQ "\\xbc" DQ); + test_quoting_of(is_tested, 0xbd, DQ "\\xbd" DQ); + test_quoting_of(is_tested, 0xbe, DQ "\\xbe" DQ); + test_quoting_of(is_tested, 0xbf, DQ "\\xbf" DQ); + // block 13 (8-bit): 0xc0 - 0xcf + test_quoting_of(is_tested, 0xc0, DQ "\\xc0" DQ); + test_quoting_of(is_tested, 0xc1, DQ "\\xc1" DQ); + test_quoting_of(is_tested, 0xc2, DQ "\\xc2" DQ); + test_quoting_of(is_tested, 0xc3, DQ "\\xc3" DQ); + test_quoting_of(is_tested, 0xc4, DQ "\\xc4" DQ); + test_quoting_of(is_tested, 0xc5, DQ "\\xc5" DQ); + test_quoting_of(is_tested, 0xc6, DQ "\\xc6" DQ); + test_quoting_of(is_tested, 0xc7, DQ "\\xc7" DQ); + test_quoting_of(is_tested, 0xc8, DQ "\\xc8" DQ); + test_quoting_of(is_tested, 0xc9, DQ "\\xc9" DQ); + test_quoting_of(is_tested, 0xca, DQ "\\xca" DQ); + test_quoting_of(is_tested, 0xcb, DQ "\\xcb" DQ); + test_quoting_of(is_tested, 0xcc, DQ "\\xcc" DQ); + test_quoting_of(is_tested, 0xcd, DQ "\\xcd" DQ); + test_quoting_of(is_tested, 0xce, DQ "\\xce" DQ); + test_quoting_of(is_tested, 0xcf, DQ "\\xcf" DQ); + // block 14 (8-bit): 0xd0 - 0xdf + test_quoting_of(is_tested, 0xd0, DQ "\\xd0" DQ); + test_quoting_of(is_tested, 0xd1, DQ "\\xd1" DQ); + test_quoting_of(is_tested, 0xd2, DQ "\\xd2" DQ); + test_quoting_of(is_tested, 0xd3, DQ "\\xd3" DQ); + test_quoting_of(is_tested, 0xd4, DQ "\\xd4" DQ); + test_quoting_of(is_tested, 0xd5, DQ "\\xd5" DQ); + test_quoting_of(is_tested, 0xd6, DQ "\\xd6" DQ); + test_quoting_of(is_tested, 0xd7, DQ "\\xd7" DQ); + test_quoting_of(is_tested, 0xd8, DQ "\\xd8" DQ); + test_quoting_of(is_tested, 0xd9, DQ "\\xd9" DQ); + test_quoting_of(is_tested, 0xda, DQ "\\xda" DQ); + test_quoting_of(is_tested, 0xdb, DQ "\\xdb" DQ); + test_quoting_of(is_tested, 0xdc, DQ "\\xdc" DQ); + test_quoting_of(is_tested, 0xdd, DQ "\\xdd" DQ); + test_quoting_of(is_tested, 0xde, DQ "\\xde" DQ); + test_quoting_of(is_tested, 0xdf, DQ "\\xdf" DQ); + // block 15 (8-bit): 0xe0 - 0xef + test_quoting_of(is_tested, 0xe0, DQ "\\xe0" DQ); + test_quoting_of(is_tested, 0xe1, DQ "\\xe1" DQ); + test_quoting_of(is_tested, 0xe2, DQ "\\xe2" DQ); + test_quoting_of(is_tested, 0xe3, DQ "\\xe3" DQ); + test_quoting_of(is_tested, 0xe4, DQ "\\xe4" DQ); + test_quoting_of(is_tested, 0xe5, DQ "\\xe5" DQ); + test_quoting_of(is_tested, 0xe6, DQ "\\xe6" DQ); + test_quoting_of(is_tested, 0xe7, DQ "\\xe7" DQ); + test_quoting_of(is_tested, 0xe8, DQ "\\xe8" DQ); + test_quoting_of(is_tested, 0xe9, DQ "\\xe9" DQ); + test_quoting_of(is_tested, 0xea, DQ "\\xea" DQ); + test_quoting_of(is_tested, 0xeb, DQ "\\xeb" DQ); + test_quoting_of(is_tested, 0xec, DQ "\\xec" DQ); + test_quoting_of(is_tested, 0xed, DQ "\\xed" DQ); + test_quoting_of(is_tested, 0xee, DQ "\\xee" DQ); + test_quoting_of(is_tested, 0xef, DQ "\\xef" DQ); + // block 16 (8-bit): 0xf0 - 0xff + test_quoting_of(is_tested, 0xf0, DQ "\\xf0" DQ); + test_quoting_of(is_tested, 0xf1, DQ "\\xf1" DQ); + test_quoting_of(is_tested, 0xf2, DQ "\\xf2" DQ); + test_quoting_of(is_tested, 0xf3, DQ "\\xf3" DQ); + test_quoting_of(is_tested, 0xf4, DQ "\\xf4" DQ); + test_quoting_of(is_tested, 0xf5, DQ "\\xf5" DQ); + test_quoting_of(is_tested, 0xf6, DQ "\\xf6" DQ); + test_quoting_of(is_tested, 0xf7, DQ "\\xf7" DQ); + test_quoting_of(is_tested, 0xf8, DQ "\\xf8" DQ); + test_quoting_of(is_tested, 0xf9, DQ "\\xf9" DQ); + test_quoting_of(is_tested, 0xfa, DQ "\\xfa" DQ); + test_quoting_of(is_tested, 0xfb, DQ "\\xfb" DQ); + test_quoting_of(is_tested, 0xfc, DQ "\\xfc" DQ); + test_quoting_of(is_tested, 0xfd, DQ "\\xfd" DQ); + test_quoting_of(is_tested, 0xfe, DQ "\\xfe" DQ); + test_quoting_of(is_tested, 0xff, DQ "\\xff" DQ); + + // Ensure the search was exhaustive. + for (int i = 0; i <= 0xff; ++i) { + g_assert_true(is_tested[i]); + } + + // Few extra tests (repeated) for specific things. + + // Smoke test + sc_string_quote(buf, sizeof buf, "hello 123"); + g_assert_cmpstr(buf, ==, DQ "hello 123" DQ); + + // Whitespace + sc_string_quote(buf, sizeof buf, "\n"); + g_assert_cmpstr(buf, ==, DQ "\\n" DQ); + sc_string_quote(buf, sizeof buf, "\r"); + g_assert_cmpstr(buf, ==, DQ "\\r" DQ); + sc_string_quote(buf, sizeof buf, "\t"); + g_assert_cmpstr(buf, ==, DQ "\\t" DQ); + sc_string_quote(buf, sizeof buf, "\v"); + g_assert_cmpstr(buf, ==, DQ "\\v" DQ); + + // Escape character itself + sc_string_quote(buf, sizeof buf, "\\"); + g_assert_cmpstr(buf, ==, DQ "\\\\" DQ); + + // Double quote character + sc_string_quote(buf, sizeof buf, "\""); + g_assert_cmpstr(buf, ==, DQ "\\\"" DQ); + +#undef DQ +} + +static void test_sc_strdup(void) +{ + char *s = sc_strdup("snap install everything"); + g_assert_nonnull(s); + g_assert_cmpstr(s, ==, "snap install everything"); + free(s); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/string-utils/sc_streq", test_sc_streq); + g_test_add_func("/string-utils/sc_endswith", test_sc_endswith); + g_test_add_func("/string-utils/sc_must_snprintf", + test_sc_must_snprintf); + g_test_add_func("/string-utils/sc_must_snprintf/fail", + test_sc_must_snprintf__fail); + g_test_add_func("/string-utils/sc_string_append/normal", + test_sc_string_append); + g_test_add_func("/string-utils/sc_string_append/empty_to_full", + test_sc_string_append__empty_to_full); + g_test_add_func("/string-utils/sc_string_append/overflow", + test_sc_string_append__overflow); + g_test_add_func("/string-utils/sc_string_append/uninitialized_buf", + test_sc_string_append__uninitialized_buf); + g_test_add_func("/string-utils/sc_string_append/NULL_buf", + test_sc_string_append__NULL_buf); + g_test_add_func("/string-utils/sc_string_append/NULL_str", + test_sc_string_append__NULL_str); + g_test_add_func("/string-utils/sc_string_init/normal", + test_sc_string_init__normal); + g_test_add_func("/string-utils/sc_string_init/empty_buf", + test_sc_string_init__empty_buf); + g_test_add_func("/string-utils/sc_string_init/NULL_buf", + test_sc_string_init__NULL_buf); + g_test_add_func + ("/string-utils/sc_string_append_char__uninitialized_buf", + test_sc_string_append_char__uninitialized_buf); + g_test_add_func("/string-utils/sc_string_append_char__NULL_buf", + test_sc_string_append_char__NULL_buf); + g_test_add_func("/string-utils/sc_string_append_char__overflow", + test_sc_string_append_char__overflow); + g_test_add_func("/string-utils/sc_string_append_char__invalid_zero", + test_sc_string_append_char__invalid_zero); + g_test_add_func("/string-utils/sc_string_append_char__normal", + test_sc_string_append_char__normal); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__NULL_buf", + test_sc_string_append_char_pair__NULL_buf); + g_test_add_func("/string-utils/sc_string_append_char_pair__overflow", + test_sc_string_append_char_pair__overflow); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__invalid_zero_c1", + test_sc_string_append_char_pair__invalid_zero_c1); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__invalid_zero_c2", + test_sc_string_append_char_pair__invalid_zero_c2); + g_test_add_func("/string-utils/sc_string_append_char_pair__normal", + test_sc_string_append_char_pair__normal); + g_test_add_func("/string-utils/sc_string_quote__NULL_buf", + test_sc_string_quote_NULL_str); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__uninitialized_buf", + test_sc_string_append_char_pair__uninitialized_buf); + g_test_add_func("/string-utils/sc_string_quote", test_sc_string_quote); + g_test_add_func("/string-utils/sc_strdup", test_sc_strdup); +} diff --git a/cmd/libsnap-confine-private/string-utils.c b/cmd/libsnap-confine-private/string-utils.c new file mode 100644 index 00000000..43a321ff --- /dev/null +++ b/cmd/libsnap-confine-private/string-utils.c @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2016-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 "string-utils.h" + +#include +#include +#include +#include + +#include "utils.h" + +bool sc_streq(const char *a, const char *b) +{ + if (!a || !b) { + return false; + } + + size_t alen = strlen(a); + size_t blen = strlen(b); + + if (alen != blen) { + return false; + } + + return strncmp(a, b, alen) == 0; +} + +bool sc_endswith(const char *str, const char *suffix) +{ + if (!str || !suffix) { + return false; + } + + size_t xlen = strlen(suffix); + size_t slen = strlen(str); + + if (slen < xlen) { + return false; + } + + return strncmp(str - xlen + slen, suffix, xlen) == 0; +} + +char *sc_strdup(const char *str) +{ + size_t len; + char *copy; + if (str == NULL) { + die("cannot duplicate NULL string"); + } + len = strlen(str); + copy = malloc(len + 1); + if (copy == NULL) { + die("cannot allocate string copy (len: %zd)", len); + } + memcpy(copy, str, len + 1); + return copy; +} + +int sc_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 || (size_t) n >= size) + die("cannot format string: %s", str); + + return n; +} + +size_t sc_string_append(char *dst, size_t dst_size, const char *str) +{ + // Set errno in case we die. + errno = 0; + if (dst == NULL) { + die("cannot append string: buffer is NULL"); + } + if (str == NULL) { + die("cannot append string: string is NULL"); + } + size_t dst_len = strnlen(dst, dst_size); + if (dst_len == dst_size) { + die("cannot append string: dst is unterminated"); + } + + size_t max_str_len = dst_size - dst_len; + size_t str_len = strnlen(str, max_str_len); + if (str_len == max_str_len) { + die("cannot append string: str is too long or unterminated"); + } + // Append the string + memcpy(dst + dst_len, str, str_len); + // Ensure we are terminated + dst[dst_len + str_len] = '\0'; + // return the new size + return strlen(dst); +} + +size_t sc_string_append_char(char *dst, size_t dst_size, char c) +{ + // Set errno in case we die. + errno = 0; + if (dst == NULL) { + die("cannot append character: buffer is NULL"); + } + size_t dst_len = strnlen(dst, dst_size); + if (dst_len == dst_size) { + die("cannot append character: dst is unterminated"); + } + size_t max_str_len = dst_size - dst_len; + if (max_str_len < 2) { + die("cannot append character: not enough space"); + } + if (c == 0) { + die("cannot append character: cannot append string terminator"); + } + // Append the character and terminate the string. + dst[dst_len + 0] = c; + dst[dst_len + 1] = '\0'; + // Return the new size + return dst_len + 1; +} + +size_t sc_string_append_char_pair(char *dst, size_t dst_size, char c1, char c2) +{ + // Set errno in case we die. + errno = 0; + if (dst == NULL) { + die("cannot append character pair: buffer is NULL"); + } + size_t dst_len = strnlen(dst, dst_size); + if (dst_len == dst_size) { + die("cannot append character pair: dst is unterminated"); + } + size_t max_str_len = dst_size - dst_len; + if (max_str_len < 3) { + die("cannot append character pair: not enough space"); + } + if (c1 == 0 || c2 == 0) { + die("cannot append character pair: cannot append string terminator"); + } + // Append the two characters and terminate the string. + dst[dst_len + 0] = c1; + dst[dst_len + 1] = c2; + dst[dst_len + 2] = '\0'; + // Return the new size + return dst_len + 2; +} + +void sc_string_init(char *buf, size_t buf_size) +{ + errno = 0; + if (buf == NULL) { + die("cannot initialize string, buffer is NULL"); + } + if (buf_size == 0) { + die("cannot initialize string, buffer is too small"); + } + buf[0] = '\0'; +} + +void sc_string_quote(char *buf, size_t buf_size, const char *str) +{ + if (str == NULL) { + die("cannot quote string: string is NULL"); + } + const char *hex = "0123456789abcdef"; + // NOTE: this also checks buf/buf_size sanity so that we don't have to. + sc_string_init(buf, buf_size); + sc_string_append_char(buf, buf_size, '"'); + for (unsigned char c; (c = *str) != 0; ++str) { + switch (c) { + // Pass ASCII letters and digits unmodified. + case '0' ... '9': + case 'A' ... 'Z': + case 'a' ... 'z': + // Pass most of the punctuation unmodified. + case ' ': + case '!': + case '#': + case '$': + case '%': + case '&': + case '(': + case ')': + case '*': + case '+': + case ',': + case '-': + case '.': + case '/': + case ':': + case ';': + case '<': + case '=': + case '>': + case '?': + case '@': + case '[': + case '\'': + case ']': + case '^': + case '_': + case '`': + case '{': + case '|': + case '}': + case '~': + sc_string_append_char(buf, buf_size, c); + break; + // Escape special whitespace characters. + case '\n': + sc_string_append_char_pair(buf, buf_size, '\\', 'n'); + break; + case '\r': + sc_string_append_char_pair(buf, buf_size, '\\', 'r'); + break; + case '\t': + sc_string_append_char_pair(buf, buf_size, '\\', 't'); + break; + case '\v': + sc_string_append_char_pair(buf, buf_size, '\\', 'v'); + break; + // Escape the escape character. + case '\\': + sc_string_append_char_pair(buf, buf_size, '\\', '\\'); + break; + // Escape double quote character. + case '"': + sc_string_append_char_pair(buf, buf_size, '\\', '"'); + break; + // Escape everything else as a generic hexadecimal escape string. + default: + sc_string_append_char_pair(buf, buf_size, '\\', 'x'); + sc_string_append_char_pair(buf, buf_size, hex[c >> 4], + hex[c & 15]); + break; + } + } + sc_string_append_char(buf, buf_size, '"'); +} diff --git a/cmd/libsnap-confine-private/string-utils.h b/cmd/libsnap-confine-private/string-utils.h new file mode 100644 index 00000000..208ed71e --- /dev/null +++ b/cmd/libsnap-confine-private/string-utils.h @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2016-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_STRING_UTILS_H +#define SNAP_CONFINE_STRING_UTILS_H + +#include +#include + +/** + * Check if two strings are equal. + **/ +bool sc_streq(const char *a, const char *b); + +/** + * Check if a string has a given suffix. + **/ +bool sc_endswith(const char *str, const char *suffix); + +/** + * Allocate and return a copy of a string. +**/ +char *sc_strdup(const char *str); + +/** + * Safer version of snprintf. + * + * This version dies on any error condition. + **/ +__attribute__ ((format(printf, 3, 4))) +int sc_must_snprintf(char *str, size_t size, const char *format, ...); + +/** + * Append a string to a buffer containing a string. + * + * This version is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL or a buffer overflow is detected + * then the function dies. + * + * The buffers cannot overlap. + **/ +size_t sc_string_append(char *dst, size_t dst_size, const char *str); + +/** + * Append a single character to a buffer containing a string. + * + * This version is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL or a buffer overflow is detected + * then the function dies. + * + * The character cannot be the string terminator. + * + * The return value is the new length of the string. + **/ +size_t sc_string_append_char(char *dst, size_t dst_size, char c); + +/** + * Append a pair of characters to a buffer containing a string. + * + * This version is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL or a buffer overflow is detected + * then the function dies. + * + * Neither character can be the string terminator. + * + * The return value is the new length of the string. + **/ +size_t sc_string_append_char_pair(char *dst, size_t dst_size, char c1, char c2); + +/** + * Initialize a string (make it empty). + * + * Initialize a string as empty, ensuring buf is non-NULL buf_size is > 0. + **/ +void sc_string_init(char *buf, size_t buf_size); + +/** + * Quote a string so it is safe for printing. + * + * This function is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL or a buffer overflow is detected + * then the function dies. + * + * The function "quotes" the content of the given string into the given buffer. + * The buffer must be of sufficient size. Apart from letters and digits and + * some punctuation all characters are escaped using their hexadecimal escape + * codes. + * + * As a practical consideration the buffer should be of the following capacity: + * strlen(str) * 4 + 2 + 1; This corresponds to the most pessimistic escape + * process (each character is escaped to a hexadecimal value like \x05, two + * double-quote characters (one front, one rear) and the final string + * terminator character. + **/ +void sc_string_quote(char *buf, size_t buf_size, const char *str); + +#endif diff --git a/cmd/libsnap-confine-private/test-utils-test.c b/cmd/libsnap-confine-private/test-utils-test.c new file mode 100644 index 00000000..04894c45 --- /dev/null +++ b/cmd/libsnap-confine-private/test-utils-test.c @@ -0,0 +1,45 @@ +/* + * Copyright (C) 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 "test-utils.h" + +#include +#include + +#include + +// Check that rm_rf_tmp doesn't remove things outside of /tmp +static void test_rm_rf_tmp(void) +{ + 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(); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/test-utils/rm_rf_tmp", test_rm_rf_tmp); +} diff --git a/cmd/libsnap-confine-private/test-utils.c b/cmd/libsnap-confine-private/test-utils.c new file mode 100644 index 00000000..ab13ce4b --- /dev/null +++ b/cmd/libsnap-confine-private/test-utils.c @@ -0,0 +1,73 @@ +/* + * 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 "test-utils.h" + +#include "error.h" +#include "utils.h" + +#include + +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); +} diff --git a/cmd/libsnap-confine-private/test-utils.h b/cmd/libsnap-confine-private/test-utils.h new file mode 100644 index 00000000..e13ffcbf --- /dev/null +++ b/cmd/libsnap-confine-private/test-utils.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 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_TEST_UTILS_H +#define SNAP_CONFINE_TEST_UTILS_H + +/** + * Shell-out to "rm -rf -- $dir" as long as $dir is in /tmp. + */ +void rm_rf_tmp(const char *dir); + +#endif diff --git a/cmd/libsnap-confine-private/tool.c b/cmd/libsnap-confine-private/tool.c new file mode 100644 index 00000000..fc67ebb2 --- /dev/null +++ b/cmd/libsnap-confine-private/tool.c @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2018 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 "tool.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/apparmor-support.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +/** + * sc_open_snapd_tool returns a file descriptor of the given internal executable. + * + * The executable is located based on the location of the currently executing process. + * The returning file descriptor can be used with fexecve function, like in sc_call_snapd_tool. +**/ +static int sc_open_snapd_tool(const char *tool_name); + +/** + * sc_call_snapd_tool calls a snapd tool by file descriptor. + * + * The idea with calling with an open file descriptor is to allow calling executables + * across mount namespaces, where the executable may not be visible in the new filesystem + * anymore. The caller establishes an open file descriptor in one namespace and later on + * performs the call in another mount namespace. + * + * The environment vector has special support for expanding the string "SNAPD_DEBUG=x". + * If such string is present, the "x" is replaced with either "0" or "1" depending on + * the result of is_sc_debug_enabled(). + **/ +static void sc_call_snapd_tool(int tool_fd, const char *tool_name, char **argv, + char **envp); + +/** + * sc_call_snapd_tool_with_apparmor calls a snapd tool by file descriptor, + * possibly confining the program with a specific apparmor profile. +**/ +static void sc_call_snapd_tool_with_apparmor(int tool_fd, const char *tool_name, + struct sc_apparmor *apparmor, + const char *aa_profile, + char **argv, char **envp); + +int sc_open_snap_update_ns(void) +{ + return sc_open_snapd_tool("snap-update-ns"); +} + +void sc_call_snap_update_ns(int snap_update_ns_fd, const char *snap_name, + struct sc_apparmor *apparmor) +{ + char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; + snap_name_copy = sc_strdup(snap_name); + + char aa_profile[PATH_MAX] = { 0 }; + sc_must_snprintf(aa_profile, sizeof aa_profile, "snap-update-ns.%s", + snap_name); + + char *argv[] = { + "snap-update-ns", + /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ + "--from-snap-confine", + snap_name_copy, NULL + }; + char *envp[] = { "SNAPD_DEBUG=x", NULL }; + sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, + "snap-update-ns", apparmor, + aa_profile, argv, envp); +} + +void sc_call_snap_update_ns_as_user(int snap_update_ns_fd, + const char *snap_name, + struct sc_apparmor *apparmor) +{ + char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; + snap_name_copy = sc_strdup(snap_name); + + char aa_profile[PATH_MAX] = { 0 }; + sc_must_snprintf(aa_profile, sizeof aa_profile, "snap-update-ns.%s", + snap_name); + + const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); + char xdg_runtime_dir_env[PATH_MAX+strlen("XDG_RUNTIME_DIR=")]; + if (xdg_runtime_dir != NULL) { + sc_must_snprintf(xdg_runtime_dir_env, + sizeof(xdg_runtime_dir_env), + "XDG_RUNTIME_DIR=%s", xdg_runtime_dir); + } + + char *argv[] = { + "snap-update-ns", + /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ + /* TODO: enable this in sync with snap-update-ns changes, "--from-snap-confine", */ + /* This tells snap-update-ns that we want to process the per-user profile */ + "--user-mounts", snap_name_copy, NULL + }; + char *envp[] = { + /* SNAPD_DEBUG=x is replaced by sc_call_snapd_tool_with_apparmor + * with either SNAPD_DEBUG=0 or SNAPD_DEBUG=1, see that function + * for details. */ + "SNAPD_DEBUG=x", + xdg_runtime_dir_env, NULL + }; + sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, + "snap-update-ns", apparmor, + aa_profile, argv, envp); +} + +int sc_open_snap_discard_ns(void) +{ + return sc_open_snapd_tool("snap-discard-ns"); +} + +void sc_call_snap_discard_ns(int snap_discard_ns_fd, const char *snap_name) +{ + char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; + snap_name_copy = sc_strdup(snap_name); + char *argv[] = + { "snap-discard-ns", "--from-snap-confine", snap_name_copy, NULL }; + /* SNAPD_DEBUG=x is replaced by sc_call_snapd_tool_with_apparmor with + * either SNAPD_DEBUG=0 or SNAPD_DEBUG=1, see that function for details. */ + char *envp[] = { "SNAPD_DEBUG=x", NULL }; + sc_call_snapd_tool(snap_discard_ns_fd, "snap-discard-ns", argv, envp); +} + +static int sc_open_snapd_tool(const char *tool_name) +{ + // +1 is for the case where the link is exactly PATH_MAX long but we also + // want to store the terminating '\0'. The readlink system call doesn't add + // terminating null, but our initialization of buf handles this for us. + char buf[PATH_MAX + 1] = { 0 }; + if (readlink("/proc/self/exe", buf, sizeof buf) < 0) { + die("cannot readlink /proc/self/exe"); + } + if (buf[0] != '/') { // this shouldn't happen, but make sure have absolute path + die("readlink /proc/self/exe returned relative path"); + } + char *dir_name = dirname(buf); + int dir_fd SC_CLEANUP(sc_cleanup_close) = 1; + dir_fd = open(dir_name, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (dir_fd < 0) { + die("cannot open path %s", dir_name); + } + int tool_fd = -1; + tool_fd = openat(dir_fd, tool_name, O_PATH | O_NOFOLLOW | O_CLOEXEC); + if (tool_fd < 0) { + die("cannot open path %s/%s", dir_name, tool_name); + } + debug("opened %s executable as file descriptor %d", tool_name, tool_fd); + return tool_fd; +} + +static void sc_call_snapd_tool(int tool_fd, const char *tool_name, char **argv, + char **envp) +{ + sc_call_snapd_tool_with_apparmor(tool_fd, tool_name, NULL, NULL, argv, + envp); +} + +static void sc_call_snapd_tool_with_apparmor(int tool_fd, const char *tool_name, + struct sc_apparmor *apparmor, + const char *aa_profile, + char **argv, char **envp) +{ + debug("calling snapd tool %s", tool_name); + pid_t child = fork(); + if (child < 0) { + die("cannot fork to run snapd tool %s", tool_name); + } + if (child == 0) { + /* If the caller provided template environment entry for SNAPD_DEBUG + * then expand it to the actual value. */ + for (char **env = envp; + /* Mama mia, that's a spicy meatball. */ + env != NULL && *env != NULL && **env != '\0'; env++) { + if (sc_streq(*env, "SNAPD_DEBUG=x")) { + /* NOTE: this is not released, on purpose. */ + char *entry = sc_strdup(*env); + entry[strlen("SNAPD_DEBUG=x") - 1] = + sc_is_debug_enabled()? '1' : '0'; + *env = entry; + } + } + /* Switch apparmor profile for the process after exec. */ + if (apparmor != NULL && aa_profile != NULL) { + sc_maybe_aa_change_onexec(apparmor, aa_profile); + } + fexecve(tool_fd, argv, envp); + die("cannot execute snapd tool %s", tool_name); + } else { + int status = 0; + debug("waiting for snapd tool %s to terminate", tool_name); + if (waitpid(child, &status, 0) < 0) { + die("cannot get snapd tool %s termination status via waitpid", tool_name); + } + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { + die("%s failed with code %i", tool_name, + WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + die("%s killed by signal %i", tool_name, + WTERMSIG(status)); + } + debug("%s finished successfully", tool_name); + } +} diff --git a/cmd/libsnap-confine-private/tool.h b/cmd/libsnap-confine-private/tool.h new file mode 100644 index 00000000..784b0a6e --- /dev/null +++ b/cmd/libsnap-confine-private/tool.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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_TOOL_H +#define SNAP_CONFINE_TOOL_H + +/* Forward declaration, for real see apparmor-support.h */ +struct sc_apparmor; + +/** + * sc_open_snap_update_ns returns a file descriptor for the snap-update-ns tool. +**/ +int sc_open_snap_update_ns(void); + +/** + * sc_call_snap_update_ns calls snap-update-ns from snap-confine + **/ +void sc_call_snap_update_ns(int snap_update_ns_fd, const char *snap_name, + struct sc_apparmor *apparmor); + +/** + * sc_call_snap_update_ns calls snap-update-ns --user-mounts from snap-confine + **/ +void sc_call_snap_update_ns_as_user(int snap_update_ns_fd, + const char *snap_name, + struct sc_apparmor *apparmor); + +/** + * sc_open_snap_update_ns returns a file descriptor for the snap-discard-ns tool. +**/ +int sc_open_snap_discard_ns(void); + +/** + * sc_call_snap_discard_ns calls the snap-discard-ns from snap confine. +**/ +void sc_call_snap_discard_ns(int snap_discard_ns_fd, const char *snap_name); + +#endif diff --git a/cmd/libsnap-confine-private/unit-tests-main.c b/cmd/libsnap-confine-private/unit-tests-main.c new file mode 100644 index 00000000..f2c0993d --- /dev/null +++ b/cmd/libsnap-confine-private/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/libsnap-confine-private/unit-tests.c b/cmd/libsnap-confine-private/unit-tests.c new file mode 100644 index 00000000..c3a822e2 --- /dev/null +++ b/cmd/libsnap-confine-private/unit-tests.c @@ -0,0 +1,29 @@ +/* + * 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 + +int sc_run_unit_tests(int *argc, char ***argv) +{ + g_test_init(argc, argv, NULL); + g_test_set_nonfatal_assertions(); + return g_test_run(); +} diff --git a/cmd/libsnap-confine-private/unit-tests.h b/cmd/libsnap-confine-private/unit-tests.h new file mode 100644 index 00000000..31414ad5 --- /dev/null +++ b/cmd/libsnap-confine-private/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/libsnap-confine-private/utils-test.c b/cmd/libsnap-confine-private/utils-test.c new file mode 100644 index 00000000..83364801 --- /dev/null +++ b/cmd/libsnap-confine-private/utils-test.c @@ -0,0 +1,203 @@ +/* + * 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_parse_bool(void) +{ + int err; + bool value; + + value = false; + err = parse_bool("yes", &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_true(value); + + value = false; + err = parse_bool("1", &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_true(value); + + value = true; + err = parse_bool("no", &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + value = true; + err = parse_bool("0", &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + value = true; + err = parse_bool("", &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + value = true; + err = parse_bool(NULL, &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + value = false; + err = parse_bool(NULL, &value, true); + g_assert_cmpint(err, ==, 0); + g_assert_true(value); + + value = true; + err = parse_bool("flower", &value, false); + g_assert_cmpint(err, ==, -1); + g_assert_cmpint(errno, ==, EINVAL); + g_assert_true(value); + + err = parse_bool("yes", NULL, false); + g_assert_cmpint(err, ==, -1); + g_assert_cmpint(errno, ==, EFAULT); +} + +static void test_die(void) +{ + 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(void) +{ + 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"); +} + +// A variant of rmdir that is compatible with GDestroyNotify +static void my_rmdir(const char *path) +{ + if (rmdir(path) != 0) { + die("cannot rmdir %s", path); + } +} + +// A variant of chdir that is compatible with GDestroyNotify +static void my_chdir(const char *path) +{ + if (chdir(path) != 0) { + die("cannot change dir to %s", path); + } +} + +/** + * 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(void) +{ + 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_free(temp_dir); + g_test_queue_destroy((GDestroyNotify) my_rmdir, temp_dir); + g_test_queue_free(orig_dir); + g_test_queue_destroy((GDestroyNotify) my_chdir, 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) my_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) my_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(void) +{ + 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(void) +{ + 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(void) +{ + g_test_add_func("/utils/parse_bool", test_parse_bool); + 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/libsnap-confine-private/utils.c b/cmd/libsnap-confine-private/utils.c new file mode 100644 index 00000000..c0170eae --- /dev/null +++ b/cmd/libsnap-confine-private/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, with a default. + * + * 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. + * + * If the text cannot be recognized, the default value is used. + **/ +static int parse_bool(const char *text, bool * value, bool default_value) +{ + if (value == NULL) { + errno = EFAULT; + return -1; + } + if (text == NULL) { + *value = default_value; + return 0; + } + for (size_t 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 parse_bool(), namely "yes", "no" as well as "1" + * and "0". All other values are treated as false and a diagnostic message is + * printed to stderr. If the environment variable is unset, set value to the + * default_value as if the environment variable was set to default_value. + **/ +static bool getenv_bool(const char *name, bool default_value) +{ + const char *str_value = getenv(name); + bool value = default_value; + if (parse_bool(str_value, &value, default_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; +} + +bool sc_is_debug_enabled(void) +{ + return getenv_bool("SNAP_CONFINE_DEBUG", false) + || getenv_bool("SNAPD_DEBUG", false); +} + +bool sc_is_reexec_enabled(void) +{ + return getenv_bool("SNAP_REEXEC", true); +} + +void debug(const char *msg, ...) +{ + if (sc_is_debug_enabled()) { + 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 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 SC_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 SC_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/libsnap-confine-private/utils.h b/cmd/libsnap-confine-private/utils.h new file mode 100644 index 00000000..b544f6cf --- /dev/null +++ b/cmd/libsnap-confine-private/utils.h @@ -0,0 +1,63 @@ +/* + * 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 CORE_LAUNCHER_UTILS_H +#define CORE_LAUNCHER_UTILS_H + +#include +#include + +__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, ...); + +/** + * Return true if debugging is enabled. + * + * This can used to avoid costly computation that is only useful for debugging. + **/ +bool sc_is_debug_enabled(void); + +/** + * Return true if re-execution is enabled. + **/ +bool sc_is_reexec_enabled(void); + +void write_string_to_file(const char *filepath, const char *buf); + +/** + * 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/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..fecf87cb --- /dev/null +++ b/cmd/snap-confine/README.mount_namespace @@ -0,0 +1,92 @@ += 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) + +# 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/cookie-support-test.c b/cmd/snap-confine/cookie-support-test.c new file mode 100644 index 00000000..b5f382ca --- /dev/null +++ b/cmd/snap-confine/cookie-support-test.c @@ -0,0 +1,102 @@ +/* + * Copyright (C) 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 "cookie-support.h" +#include "cookie-support.c" + +#include "../libsnap-confine-private/test-utils.h" + +#include +#include +#include +#include + +// Set alternate cookie directory +static void set_cookie_dir(const char *dir) +{ + sc_cookie_dir = dir; +} + +static void set_fake_cookie_dir(void) +{ + char *ctx_dir = NULL; + ctx_dir = g_dir_make_tmp(NULL, NULL); + g_assert_nonnull(ctx_dir); + g_test_queue_free(ctx_dir); + + g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, ctx_dir); + g_test_queue_destroy((GDestroyNotify) set_cookie_dir, SC_COOKIE_DIR); + + set_cookie_dir(ctx_dir); +} + +static void create_dumy_cookie_file(const char *snap_name, + const char *dummy_cookie) +{ + char path[PATH_MAX] = { 0 }; + FILE *f; + int n; + + snprintf(path, sizeof(path), "%s/snap.%s", sc_cookie_dir, snap_name); + + f = fopen(path, "w"); + g_assert_nonnull(f); + + n = fwrite(dummy_cookie, 1, strlen(dummy_cookie), f); + g_assert_cmpint(n, ==, strlen(dummy_cookie)); + + fclose(f); +} + +static void test_cookie_get_from_snapd__successful(void) +{ + struct sc_error *err = NULL; + char *cookie; + + char *dummy = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijmnopqrst"; + + set_fake_cookie_dir(); + create_dumy_cookie_file("test-snap", dummy); + + cookie = sc_cookie_get_from_snapd("test-snap", &err); + g_assert_null(err); + g_assert_nonnull(cookie); + g_assert_cmpint(strlen(cookie), ==, 44); + g_assert_cmpstr(cookie, ==, dummy); +} + +static void test_cookie_get_from_snapd__nofile(void) +{ + struct sc_error *err = NULL; + char *cookie; + + set_fake_cookie_dir(); + + cookie = sc_cookie_get_from_snapd("test-snap2", &err); + g_assert_nonnull(err); + g_assert_cmpstr(sc_error_domain(err), ==, SC_ERRNO_DOMAIN); + g_assert_nonnull(strstr(sc_error_msg(err), "cannot open cookie file")); + g_assert_null(cookie); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/snap-cookie/cookie_get_from_snapd/successful", + test_cookie_get_from_snapd__successful); + g_test_add_func("/snap-cookie/cookie_get_from_snapd/no_cookie_file", + test_cookie_get_from_snapd__nofile); +} diff --git a/cmd/snap-confine/cookie-support.c b/cmd/snap-confine/cookie-support.c new file mode 100644 index 00000000..1d267fb7 --- /dev/null +++ b/cmd/snap-confine/cookie-support.c @@ -0,0 +1,75 @@ +/* + * Copyright (C) 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 "cookie-support.h" + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#define SC_COOKIE_DIR "/var/lib/snapd/cookie" + +/** + * Effective value of SC_COOKIE_DIR + **/ +static const char *sc_cookie_dir = SC_COOKIE_DIR; + +char *sc_cookie_get_from_snapd(const char *snap_name, struct sc_error **errorp) +{ + char context_path[PATH_MAX] = { 0 }; + struct sc_error *err = NULL; + char *context = NULL; + + sc_must_snprintf(context_path, sizeof(context_path), "%s/snap.%s", + sc_cookie_dir, snap_name); + int fd SC_CLEANUP(sc_cleanup_close) = -1; + fd = open(context_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) { + err = + sc_error_init_from_errno(errno, + "warning: cannot open cookie file %s", + context_path); + goto out; + } + // large enough buffer for opaque cookie string + char context_val[255] = { 0 }; + ssize_t n = read(fd, context_val, sizeof(context_val) - 1); + if (n < 0) { + err = + sc_error_init_from_errno(errno, + "cannot read cookie file %s", + context_path); + goto out; + } + context = strndup(context_val, n); + if (context == NULL) { + die("cannot duplicate snap cookie value"); + } + + out: + sc_error_forward(errorp, err); + return context; +} diff --git a/cmd/snap-confine/cookie-support.h b/cmd/snap-confine/cookie-support.h new file mode 100644 index 00000000..49774b0c --- /dev/null +++ b/cmd/snap-confine/cookie-support.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 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_CONTEXT_SUPPORT_H +#define SNAP_CONFINE_CONTEXT_SUPPORT_H + +#include "../libsnap-confine-private/error.h" + +/** + * Return snap cookie string for given snap. + * + * The context value is read from /var/lib/snapd/cookie/snap. + * file. The caller of the function takes the ownership of the returned cookie + * string. + * If the file cannot be read then an error is returned in errorp and + * the function returns NULL. + **/ +char *sc_cookie_get_from_snapd(const char *snap_name, struct sc_error **errorp); + +#endif 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-support-nvidia.c b/cmd/snap-confine/mount-support-nvidia.c new file mode 100644 index 00000000..b47123a6 --- /dev/null +++ b/cmd/snap-confine/mount-support-nvidia.c @@ -0,0 +1,547 @@ +/* + * 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 +/* POSIX version of basename() and dirname() */ +#include + +#include "../libsnap-confine-private/classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +#define SC_NVIDIA_DRIVER_VERSION_FILE "/sys/module/nvidia/version" + +// note: if the parent dir changes to something other than +// the current /var/lib/snapd/lib then sc_mkdir_and_mount_and_bind +// and sc_mkdir_and_mount_and_bind need updating. +#define SC_LIB "/var/lib/snapd/lib" +#define SC_LIBGL_DIR SC_LIB "/gl" +#define SC_LIBGL32_DIR SC_LIB "/gl32" +#define SC_VULKAN_DIR SC_LIB "/vulkan" +#define SC_GLVND_DIR SC_LIB "/glvnd" + +#define SC_VULKAN_SOURCE_DIR "/usr/share/vulkan" +#define SC_EGL_VENDOR_SOURCE_DIR "/usr/share/glvnd" + +// Location for NVIDIA vulkan files (including _wayland) +static const char *vulkan_globs[] = { + "icd.d/*nvidia*.json", +}; + +static const size_t vulkan_globs_len = + sizeof vulkan_globs / sizeof *vulkan_globs; + +// Location of EGL vendor files +static const char *egl_vendor_globs[] = { + "egl_vendor.d/*nvidia*.json", +}; + +static const size_t egl_vendor_globs_len = + sizeof egl_vendor_globs / sizeof *egl_vendor_globs; + +#if defined(NVIDIA_BIARCH) || defined(NVIDIA_MULTIARCH) + +// 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[] = { + "libEGL.so*", + "libEGL_nvidia.so*", + "libGL.so*", + "libOpenGL.so*", + "libGLESv1_CM.so*", + "libGLESv1_CM_nvidia.so*", + "libGLESv2.so*", + "libGLESv2_nvidia.so*", + "libGLX_indirect.so*", + "libGLX_nvidia.so*", + "libGLX.so*", + "libGLdispatch.so*", + "libGLU.so*", + "libXvMCNVIDIA.so*", + "libXvMCNVIDIA_dynamic.so*", + "libcuda.so*", + "libcudart.so*", + "libnvcuvid.so*", + "libnvidia-cfg.so*", + "libnvidia-compiler.so*", + "libnvidia-eglcore.so*", + "libnvidia-egl-wayland*", + "libnvidia-encode.so*", + "libnvidia-fatbinaryloader.so*", + "libnvidia-fbc.so*", + "libnvidia-glcore.so*", + "libnvidia-glsi.so*", + "libnvidia-glvkspirv.so*", + "libnvidia-ifr.so*", + "libnvidia-ml.so*", + "libnvidia-opencl.so*", + "libnvidia-ptxjitcompiler.so*", + "libnvidia-tls.so*", + "tls/libnvidia-tls.so*", + "vdpau/libvdpau_nvidia.so*", +}; + +static const size_t nvidia_globs_len = + sizeof nvidia_globs / sizeof *nvidia_globs; + +#define LIBNVIDIA_GLCORE_SO_PATTERN "libnvidia-glcore.so.%d.%d" + +#endif // defined(NVIDIA_BIARCH) || defined(NVIDIA_MULTIARCH) + +// 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. +// +// The glob list passed to us is produced with paths relative to source dir, +// to simplify the various tie-in points with this function. +static void sc_populate_libgl_with_hostfs_symlinks(const char *libgl_dir, + const char *source_dir, + const char *glob_list[], + size_t glob_list_len) +{ + size_t source_dir_len = strlen(source_dir); + glob_t glob_res SC_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]; + char glob_pattern_full[512] = { 0 }; + sc_must_snprintf(glob_pattern_full, sizeof glob_pattern_full, + "%s/%s", source_dir, glob_pattern); + + int err = glob(glob_pattern_full, 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_full, err); + } + } + // Symlink each file found + for (size_t i = 0; i < glob_res.gl_pathc; ++i) { + char symlink_name[512] = { 0 }; + char symlink_target[512] = { 0 }; + char prefix_dir[512] = { 0 }; + const char *pathname = glob_res.gl_pathv[i]; + char *pathname_copy1 + SC_CLEANUP(sc_cleanup_string) = sc_strdup(pathname); + char *pathname_copy2 + SC_CLEANUP(sc_cleanup_string) = sc_strdup(pathname); + // POSIX dirname() and basename() may modify their input arguments + char *filename = basename(pathname_copy1); + char *directory_name = dirname(pathname_copy2); + sc_must_snprintf(prefix_dir, sizeof prefix_dir, "%s", + libgl_dir); + + if (strlen(directory_name) > source_dir_len) { + // Additional path elements between source_dir and dirname, meaning the + // actual file is not placed directly under source_dir but under one or + // more directories below source_dir. Make sure to recreate the whole + // prefix + sc_must_snprintf(prefix_dir, sizeof prefix_dir, + "%s%s", libgl_dir, + &directory_name[source_dir_len]); + if (sc_nonfatal_mkpath(prefix_dir, 0755) != 0) { + die("failed to create prefix path: %s", + prefix_dir); + } + } + + 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] == '/') { + sc_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 + sc_must_snprintf(symlink_target, + sizeof symlink_target, "%s", + hostfs_symlink_target); + } + break; + case S_IFREG: + sc_must_snprintf(symlink_target, + sizeof symlink_target, + "/var/lib/snapd/hostfs%s", pathname); + break; + default: + debug("ignoring unsupported entry: %s", pathname); + continue; + } + sc_must_snprintf(symlink_name, sizeof symlink_name, + "%s/%s", prefix_dir, filename); + debug("creating symbolic link %s -> %s", symlink_name, + symlink_target); + + // Make sure we don't have some link already (merged GLVND systems) + if (lstat(symlink_name, &stat_buf) == 0) { + if (unlink(symlink_name) != 0) { + die("cannot remove symbolic link target %s", + symlink_name); + } + } + + if (symlink(symlink_target, symlink_name) != 0) { + die("cannot create symbolic link %s -> %s", + symlink_name, symlink_target); + } + } +} + +static void sc_mkdir_and_mount_and_glob_files(const char *rootfs_dir, + const char *source_dir[], + size_t source_dir_len, + const char *tgt_dir, + const char *glob_list[], + size_t glob_list_len) +{ + // Bind mount a tmpfs on $rootfs_dir/$tgt_dir (i.e. /var/lib/snapd/lib/gl) + char buf[512] = { 0 }; + sc_must_snprintf(buf, sizeof(buf), "%s%s", rootfs_dir, tgt_dir); + const char *libgl_dir = buf; + + int res = mkdir(libgl_dir, 0755); + if (res != 0 && errno != EEXIST) { + die("cannot create tmpfs target %s", libgl_dir); + } + if (res == 0 && (chown(libgl_dir, 0, 0) < 0)) { + // Adjust the ownership only if we created the directory. + die("cannot change ownership of %s", libgl_dir); + } + + 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); + }; + + for (size_t i = 0; i < source_dir_len; i++) { + // Populate libgl_dir with symlinks to libraries from hostfs + sc_populate_libgl_with_hostfs_symlinks(libgl_dir, source_dir[i], + glob_list, + glob_list_len); + } + // Remount $tgt_dir (i.e. .../lib/gl) read only + debug("remounting tmpfs as read-only %s", libgl_dir); + if (mount(NULL, buf, NULL, MS_REMOUNT | MS_BIND | MS_RDONLY, NULL) != 0) { + die("cannot remount %s as read-only", buf); + } +} + +#ifdef NVIDIA_BIARCH + +// Expose host NVIDIA drivers to the snap on biarch systems. +// +// Order is absolutely imperative here. We'll attempt to find the +// primary files for the architecture in the main directory, and end +// up copying any files across. However it is possible we're using a +// GLVND enabled host, in which case we copied libGL* to the farm. +// The next step in the list is to look within the private nvidia +// directory, exposed using ld.so.conf tricks within the host OS. +// In some distros (i.e. Solus) only the private libGL/libEGL files +// may be found here, and they'll clobber the existing GLVND files from +// the previous run. +// In other distros (like Fedora) all NVIDIA libraries are contained +// within the private directory, so we clobber the GLVND files and we +// also grab all the private NVIDIA libraries. +// +// In non GLVND cases we just copy across the exposed libGLs and NVIDIA +// libraries from wherever we find, and clobbering is also harmless. +static void sc_mount_nvidia_driver_biarch(const char *rootfs_dir) +{ + + const char *native_sources[] = { + NATIVE_LIBDIR, + NATIVE_LIBDIR "/nvidia*", + }; + const size_t native_sources_len = + sizeof native_sources / sizeof *native_sources; + +#if UINTPTR_MAX == 0xffffffffffffffff + // Alternative 32-bit support + const char *lib32_sources[] = { + LIB32_DIR, + LIB32_DIR "/nvidia*", + }; + const size_t lib32_sources_len = + sizeof lib32_sources / sizeof *lib32_sources; +#endif + + // Primary arch + sc_mkdir_and_mount_and_glob_files(rootfs_dir, + native_sources, native_sources_len, + SC_LIBGL_DIR, nvidia_globs, + nvidia_globs_len); + +#if UINTPTR_MAX == 0xffffffffffffffff + // Alternative 32-bit support + sc_mkdir_and_mount_and_glob_files(rootfs_dir, lib32_sources, + lib32_sources_len, SC_LIBGL32_DIR, + nvidia_globs, nvidia_globs_len); +#endif +} + +#endif // ifdef NVIDIA_BIARCH + +#ifdef NVIDIA_MULTIARCH + +struct sc_nvidia_driver { + int major_version; + int minor_version; +}; + +static void sc_probe_nvidia_driver(struct sc_nvidia_driver *driver) +{ + FILE *file SC_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_mkdir_and_mount_and_bind(const char *rootfs_dir, + const char *src_dir, + const char *tgt_dir) +{ + struct sc_nvidia_driver driver; + + // Probe sysfs to get the version of the driver that is currently inserted. + sc_probe_nvidia_driver(&driver); + + // If there's driver in the kernel then don't mount userspace. + if (driver.major_version == 0) { + return; + } + // Construct the paths for the driver userspace libraries + // and for the gl directory. + char src[PATH_MAX] = { 0 }; + char dst[PATH_MAX] = { 0 }; + sc_must_snprintf(src, sizeof src, "%s-%d", src_dir, + driver.major_version); + sc_must_snprintf(dst, sizeof dst, "%s%s", rootfs_dir, tgt_dir); + + // If there is no userspace driver available then don't try to mount it. + // This can happen for any number of reasons but one interesting one is + // that that snapd runs in a lxd container on a host that uses nvidia. In + // that case the container may not have the userspace library installed but + // the kernel will still have the module around. + if (access(src, F_OK) != 0) { + return; + } + int res = mkdir(dst, 0755); + if (res != 0 && errno != EEXIST) { + die("cannot create directory %s", dst); + } + if (res == 0 && (chown(dst, 0, 0) < 0)) { + // Adjust the ownership only if we created the directory. + die("cannot change ownership of %s", dst); + } + // Bind mount the binary nvidia driver into $tgt_dir (i.e. /var/lib/snapd/lib/gl). + 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); + } +} + +static int sc_mount_nvidia_is_driver_in_dir(const char *dir) +{ + char driver_path[512] = { 0 }; + + struct sc_nvidia_driver driver; + + // Probe sysfs to get the version of the driver that is currently inserted. + sc_probe_nvidia_driver(&driver); + + // If there's no driver then we should not bother ourselves with finding the + // matching library + if (driver.major_version == 0) { + return 0; + } + // Probe if a well known library is found in directory dir + sc_must_snprintf(driver_path, sizeof driver_path, + "%s/" LIBNVIDIA_GLCORE_SO_PATTERN, dir, + driver.major_version, driver.minor_version); + + if (access(driver_path, F_OK) == 0) { + debug("nvidia library detected at path %s", driver_path); + return 1; + } + return 0; +} + +static void sc_mount_nvidia_driver_multiarch(const char *rootfs_dir) +{ + const char *native_libdir = NATIVE_LIBDIR "/" HOST_ARCH_TRIPLET; + const char *lib32_libdir = NATIVE_LIBDIR "/" HOST_ARCH32_TRIPLET; + + if ((strlen(HOST_ARCH_TRIPLET) > 0) && + (sc_mount_nvidia_is_driver_in_dir(native_libdir) == 1)) { + + // sc_mkdir_and_mount_and_glob_files() takes an array of strings, so + // initialize native_sources accordingly, but calculate the array length + // dynamically to make adjustments to native_sources easier. + const char *native_sources[] = { native_libdir }; + const size_t native_sources_len = + sizeof native_sources / sizeof *native_sources; + // Primary arch + sc_mkdir_and_mount_and_glob_files(rootfs_dir, + native_sources, + native_sources_len, + SC_LIBGL_DIR, nvidia_globs, + nvidia_globs_len); + + // Alternative 32-bit support + if ((strlen(HOST_ARCH32_TRIPLET) > 0) && + (sc_mount_nvidia_is_driver_in_dir(lib32_libdir) == 1)) { + + // sc_mkdir_and_mount_and_glob_files() takes an array of strings, so + // initialize lib32_sources accordingly, but calculate the array length + // dynamically to make adjustments to lib32_sources easier. + const char *lib32_sources[] = { lib32_libdir }; + const size_t lib32_sources_len = + sizeof lib32_sources / sizeof *lib32_sources; + sc_mkdir_and_mount_and_glob_files(rootfs_dir, + lib32_sources, + lib32_sources_len, + SC_LIBGL32_DIR, + nvidia_globs, + nvidia_globs_len); + } + } else { + // Attempt mount of both the native and 32-bit variants of the driver if they exist + sc_mkdir_and_mount_and_bind(rootfs_dir, "/usr/lib/nvidia", + SC_LIBGL_DIR); + // Alternative 32-bit support + sc_mkdir_and_mount_and_bind(rootfs_dir, "/usr/lib32/nvidia", + SC_LIBGL32_DIR); + } +} + +#endif // ifdef NVIDIA_MULTIARCH + +static void sc_mount_vulkan(const char *rootfs_dir) +{ + const char *vulkan_sources[] = { + SC_VULKAN_SOURCE_DIR, + }; + const size_t vulkan_sources_len = + sizeof vulkan_sources / sizeof *vulkan_sources; + + sc_mkdir_and_mount_and_glob_files(rootfs_dir, vulkan_sources, + vulkan_sources_len, SC_VULKAN_DIR, + vulkan_globs, vulkan_globs_len); +} + +static void sc_mount_egl(const char *rootfs_dir) +{ + const char *egl_vendor_sources[] = { SC_EGL_VENDOR_SOURCE_DIR }; + const size_t egl_vendor_sources_len = + sizeof egl_vendor_sources / sizeof *egl_vendor_sources; + + sc_mkdir_and_mount_and_glob_files(rootfs_dir, egl_vendor_sources, + egl_vendor_sources_len, SC_GLVND_DIR, + egl_vendor_globs, + egl_vendor_globs_len); +} + +void sc_mount_nvidia_driver(const char *rootfs_dir) +{ + /* If NVIDIA module isn't loaded, don't attempt to mount the drivers */ + if (access(SC_NVIDIA_DRIVER_VERSION_FILE, F_OK) != 0) { + return; + } + + int res = mkdir(SC_LIB, 0755); + if (res != 0 && errno != EEXIST) { + die("cannot create " SC_LIB); + } + if (res == 0 && (chown(SC_LIB, 0, 0) < 0)) { + // Adjust the ownership only if we created the directory. + die("cannot change ownership of " SC_LIB); + } +#ifdef NVIDIA_MULTIARCH + sc_mount_nvidia_driver_multiarch(rootfs_dir); +#endif // ifdef NVIDIA_MULTIARCH +#ifdef NVIDIA_BIARCH + sc_mount_nvidia_driver_biarch(rootfs_dir); +#endif // ifdef NVIDIA_BIARCH + + // Common for both driver mechanisms + sc_mount_vulkan(rootfs_dir); + sc_mount_egl(rootfs_dir); +} 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..a57016a5 --- /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(void) +{ + 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(void) +{ + 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(void) +{ + // 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(void) +{ + 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..233d7ee3 --- /dev/null +++ b/cmd/snap-confine/mount-support.c @@ -0,0 +1,704 @@ +/* + * 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 "mount-support.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/apparmor-support.h" +#include "../libsnap-confine-private/classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/mount-opt.h" +#include "../libsnap-confine-private/mountinfo.h" +#include "../libsnap-confine-private/snap.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/tool.h" +#include "../libsnap-confine-private/utils.h" +#include "mount-support-nvidia.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" + +// TODO: simplify this, after all it is just a tmpfs +// TODO: fold this into bootstrap +static void setup_private_mount(const char *snap_name) +{ + 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 + sc_must_snprintf(tmpdir, sizeof(tmpdir), "/tmp/snap.%d_%s_XXXXXX", uid, + snap_name); + 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 = sc_strdup(tmpdir); + sc_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 + sc_do_mount(tmpdir, "/tmp", NULL, MS_BIND, NULL); + // MS_PRIVATE needs linux > 2.6.11 + sc_do_mount("none", "/tmp", NULL, MS_PRIVATE, NULL); + // 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); +} + +// TODO: fold this into bootstrap +static void setup_private_pts(void) +{ + // 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 + sc_do_mount("devpts", "/dev/pts", "devpts", MS_MGC_VAL, + "newinstance,ptmxmode=0666,mode=0620,gid=5"); + sc_do_mount("/dev/pts/ptmx", "/dev/ptmx", "none", MS_BIND, 0); +} + +struct sc_mount { + const char *path; + bool is_bidirectional; + // Alternate path defines the rbind mount "alternative" of path. + // It exists so that we can make /media on systems that use /run/media. + const char *altpath; + // Optional mount points are not processed unless the source and + // destination both exist. + bool is_optional; +}; + +struct sc_mount_config { + const char *rootfs_dir; + // The struct is terminated with an entry with NULL path. + const struct sc_mount *mounts; + sc_distro distro; + bool normal_mode; + const char *base_snap_name; +}; + +/** + * 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] = { 0 }; + char dst[PATH_MAX] = { 0 }; + 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. + sc_do_mount("none", "/", NULL, MS_REC | MS_SHARED, NULL); + // 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. + sc_do_mount(scratch_dir, scratch_dir, NULL, MS_BIND, NULL); + // 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. + sc_do_mount("none", scratch_dir, NULL, MS_UNBINDABLE, NULL); + // 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. + sc_do_mount(config->rootfs_dir, scratch_dir, NULL, MS_REC | MS_BIND, + NULL); + // 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. + sc_do_mount("none", scratch_dir, NULL, MS_REC | MS_SLAVE, NULL); + // 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); + } + sc_must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, + mnt->path); + if (mnt->is_optional) { + bool ok = sc_do_optional_mount(mnt->path, dst, NULL, + MS_REC | MS_BIND, NULL); + if (!ok) { + // If we cannot mount it, just continue. + continue; + } + } else { + sc_do_mount(mnt->path, dst, NULL, MS_REC | MS_BIND, + NULL); + } + 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. + sc_do_mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL); + } + if (mnt->altpath == NULL) { + continue; + } + // An alternate path of mnt->path is provided at another location. + // It should behave exactly the same as the original. + sc_must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, + mnt->altpath); + struct stat stat_buf; + if (lstat(dst, &stat_buf) < 0) { + die("cannot lstat %s", dst); + } + if ((stat_buf.st_mode & S_IFMT) == S_IFLNK) { + die("cannot bind mount alternate path over a symlink: %s", dst); + } + sc_do_mount(mnt->path, dst, NULL, MS_REC | MS_BIND, NULL); + if (!mnt->is_bidirectional) { + sc_do_mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL); + } + } + if (config->normal_mode) { + // Since we mounted /etc from the host filesystem to the scratch directory, + // we may need to put certain directories 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. + // + // Fixes the following bugs: + // - https://bugs.launchpad.net/snap-confine/+bug/1580018 + // - https://bugzilla.opensuse.org/show_bug.cgi?id=1028568 + const char *dirs_from_core[] = + { "/etc/alternatives", "/etc/ssl", "/etc/nsswitch.conf", + NULL + }; + for (const char **dirs = dirs_from_core; *dirs != NULL; dirs++) { + const char *dir = *dirs; + struct stat buf; + if (access(dir, F_OK) == 0) { + sc_must_snprintf(src, sizeof src, "%s%s", + config->rootfs_dir, dir); + sc_must_snprintf(dst, sizeof dst, "%s%s", + scratch_dir, dir); + if (lstat(src, &buf) == 0 + && lstat(dst, &buf) == 0) { + sc_do_mount(src, dst, NULL, MS_BIND, + NULL); + sc_do_mount("none", dst, NULL, MS_SLAVE, + NULL); + } + } + } + } + // The "core" base snap is special as it contains snapd and friends. + // Other base snaps do not, so whenever a base snap other than core is + // in use we need extra provisions for setting up internal tooling to + // be available. + // + // However on a core18 (and similar) system the core snap is not + // a special base anymore and we should map our own tooling in. + if (config->distro == SC_DISTRO_CORE_OTHER + || !sc_streq(config->base_snap_name, "core")) { + // when bases are used we need to bind-mount the libexecdir + // (that contains snap-exec) into /usr/lib/snapd of the + // base snap so that snap-exec is available for the snaps + // (base snaps do not ship snapd) + + // dst is always /usr/lib/snapd as this is where snapd + // assumes to find snap-exec + sc_must_snprintf(dst, sizeof dst, "%s/usr/lib/snapd", + scratch_dir); + + // bind mount the current $ROOT/usr/lib/snapd path, + // where $ROOT is either "/" or the "/snap/{core,snapd}/current" + // that we are re-execing from + char *src = NULL; + char self[PATH_MAX + 1] = { 0 }; + if (readlink("/proc/self/exe", self, sizeof(self) - 1) < 0) { + die("cannot read /proc/self/exe"); + } + // this cannot happen except when the kernel is buggy + if (strstr(self, "/snap-confine") == NULL) { + die("cannot use result from readlink: %s", self); + } + src = dirname(self); + // dirname(path) might return '.' depending on path. + // /proc/self/exe should always point + // to an absolute path, but let's guarantee that. + if (src[0] != '/') { + die("cannot use the result of dirname(): %s", src); + } + + sc_do_mount(src, dst, NULL, MS_BIND | MS_RDONLY, NULL); + sc_do_mount("none", dst, NULL, MS_SLAVE, NULL); + } + // 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. + sc_must_snprintf(dst, sizeof dst, "%s/snap", scratch_dir); + sc_do_mount(SNAP_MOUNT_DIR, dst, NULL, MS_BIND | MS_REC | MS_SLAVE, + NULL); + sc_do_mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL); + // 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); + } + } + // Ensure that hostfs isgroup owned by root. We may have (now or earlier) + // created the directory as the user who first ran a snap on a given + // system and the group identity of that user is visilbe on disk. + // This was LP:#1665004 + struct stat sb; + if (stat(SC_HOSTFS_DIR, &sb) < 0) { + die("cannot stat %s", SC_HOSTFS_DIR); + } + if (sb.st_uid != 0 || sb.st_gid != 0) { + if (chown(SC_HOSTFS_DIR, 0, 0) < 0) { + die("cannot change user/group owner of %s to root", + 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. + sc_must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, SC_HOSTFS_DIR); + sc_do_mount(dst, dst, NULL, MS_BIND, NULL); + sc_do_mount("none", dst, NULL, MS_PRIVATE, NULL); + // 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->distro == SC_DISTRO_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. + sc_must_snprintf(dst, sizeof dst, "%s/%s", SC_HOSTFS_DIR, scratch_dir); + sc_do_umount(dst, 0); + // 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. + sc_do_mount("none", SC_HOSTFS_DIR, NULL, MS_REC | MS_SLAVE, NULL); + // 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). + sc_must_snprintf(src, sizeof src, "%s/sys", SC_HOSTFS_DIR); + sc_do_umount(src, UMOUNT_NOFOLLOW | MNT_DETACH); + // Detach the redundant hostfs version of /dev since it shows up in the + // mount table and software inspecting the mount table may become confused. + sc_must_snprintf(src, sizeof src, "%s/dev", SC_HOSTFS_DIR); + sc_do_umount(src, UMOUNT_NOFOLLOW | MNT_DETACH); + // Detach the redundant hostfs version of /proc since it shows up in the + // mount table and software inspecting the mount table may become confused. + sc_must_snprintf(src, sizeof src, "%s/proc", SC_HOSTFS_DIR); + sc_do_umount(src, UMOUNT_NOFOLLOW | MNT_DETACH); +} + +/** + * @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) +{ + size_t 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(struct sc_apparmor *apparmor, int snap_update_ns_fd, + const char *base_snap_name, const char *snap_name) +{ + // 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 SC_CLEANUP(sc_cleanup_string) = NULL; + vanilla_cwd = get_current_dir_name(); + if (vanilla_cwd == NULL) { + die("cannot get the current working directory"); + } + // Classify the current distribution, as claimed by /etc/os-release. + sc_distro distro = sc_classify_distro(); + // Check which mode we should run in, normal or legacy. + if (sc_should_use_normal_mode(distro, base_snap_name)) { + // In normal mode we use the base snap as / and set up several bind mounts. + 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",.is_optional = true}, // 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, "/media"}, // 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 + // The /mnt directory is optional in base snaps to ensure backwards + // compatibility with the first version of base snaps that was + // released. + {"/mnt",.is_optional = true}, // to support the removable-media interface + {"/var/lib/extrausers",.is_optional = true}, // access to UID/GID of extrausers (if available) + {}, + }; + char rootfs_dir[PATH_MAX] = { 0 }; + sc_must_snprintf(rootfs_dir, sizeof rootfs_dir, + "%s/%s/current/", SNAP_MOUNT_DIR, + base_snap_name); + if (access(rootfs_dir, F_OK) != 0) { + if (sc_streq(base_snap_name, "core")) { + // As a special fallback, allow the + // base snap to degrade from "core" to + // "ubuntu-core". This is needed for + // the migration tests. + base_snap_name = "ubuntu-core"; + sc_must_snprintf(rootfs_dir, sizeof rootfs_dir, + "%s/%s/current/", + SNAP_MOUNT_DIR, + base_snap_name); + if (access(rootfs_dir, F_OK) != 0) { + die("cannot locate the core or legacy core snap (current symlink missing?)"); + } + } + if (access(rootfs_dir, F_OK) != 0) + die("cannot locate the base snap: %s", base_snap_name); + } + struct sc_mount_config normal_config = { + .rootfs_dir = rootfs_dir, + .mounts = mounts, + .distro = distro, + .normal_mode = true, + .base_snap_name = base_snap_name, + }; + sc_bootstrap_mount_namespace(&normal_config); + } else { + // In legacy mode we don't pivot and instead just arrange bi- + // directional mount propagation for two directories. + const struct sc_mount mounts[] = { + {"/media", true}, + {"/run/netns", true}, + {}, + }; + struct sc_mount_config legacy_config = { + .rootfs_dir = "/", + .mounts = mounts, + .distro = distro, + .normal_mode = false, + .base_snap_name = base_snap_name, + }; + sc_bootstrap_mount_namespace(&legacy_config); + } + + // set up private mounts + // TODO: rename this and fold it into bootstrap + setup_private_mount(snap_name); + + // set up private /dev/pts + // TODO: fold this into bootstrap + setup_private_pts(); + + // setup the security backend bind mounts + sc_call_snap_update_ns(snap_update_ns_fd, snap_name, apparmor); + + // 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); + } +} + +static bool is_mounted_with_shared_option(const char *dir) + __attribute__ ((nonnull(1))); + +static bool is_mounted_with_shared_option(const char *dir) +{ + struct sc_mountinfo *sm SC_CLEANUP(sc_cleanup_mountinfo) = NULL; + sm = sc_parse_mountinfo(NULL); + if (sm == NULL) { + die("cannot parse /proc/self/mountinfo"); + } + struct sc_mountinfo_entry *entry = sc_first_mountinfo_entry(sm); + while (entry != NULL) { + const char *mount_dir = entry->mount_dir; + if (sc_streq(mount_dir, dir)) { + const char *optional_fields = entry->optional_fields; + if (strstr(optional_fields, "shared:") != NULL) { + return true; + } + } + entry = sc_next_mountinfo_entry(entry); + } + return false; +} + +void sc_ensure_shared_snap_mount(void) +{ + if (!is_mounted_with_shared_option("/") + && !is_mounted_with_shared_option(SNAP_MOUNT_DIR)) { + // TODO: We could be more aggressive and refuse to function but since + // we have no data on actual environments that happen to limp along in + // this configuration let's not do that yet. This code should be + // removed once we have a measurement and feedback mechanism that lets + // us decide based on measurable data. + sc_do_mount(SNAP_MOUNT_DIR, SNAP_MOUNT_DIR, "none", + MS_BIND | MS_REC, 0); + sc_do_mount("none", SNAP_MOUNT_DIR, NULL, MS_SHARED | MS_REC, + NULL); + } +} + +static void sc_make_slave_mount_ns(void) +{ + if (unshare(CLONE_NEWNS) < 0) { + die("can not unshare mount namespace"); + } + // In our new mount namespace, recursively change all mounts + // to slave mode, so we see changes from the parent namespace + // but don't propagate our own changes. + sc_do_mount("none", "/", NULL, MS_REC | MS_SLAVE, NULL); +} + +void sc_setup_user_mounts(struct sc_apparmor *apparmor, int snap_update_ns_fd, + const char *snap_name) +{ + debug("%s: %s", __FUNCTION__, snap_name); + + char profile_path[PATH_MAX]; + struct stat st; + + sc_must_snprintf(profile_path, sizeof(profile_path), + "/var/lib/snapd/mount/snap.%s.user-fstab", snap_name); + if (stat(profile_path, &st) != 0) { + // It is ok for the user fstab to not exist. + return; + } + + sc_make_slave_mount_ns(); + sc_call_snap_update_ns_as_user(snap_update_ns_fd, snap_name, apparmor); +} diff --git a/cmd/snap-confine/mount-support.h b/cmd/snap-confine/mount-support.h new file mode 100644 index 00000000..082c304f --- /dev/null +++ b/cmd/snap-confine/mount-support.h @@ -0,0 +1,77 @@ +/* + * 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 + +#include "../libsnap-confine-private/apparmor-support.h" + +/** + * Return a file descriptor referencing the snap-update-ns utility + * + * By calling this prior to changing the mount namespace, it is + * possible to execute the utility even if a different version is now + * mounted at the expected location. + **/ +int sc_open_snap_update_ns(void); + +/** + * Return a file descriptor referencing the snap-discard-ns utility + * + * By calling this prior to changing the mount namespace, it is + * possible to execute the utility even if a different version is now + * mounted at the expected location. + **/ +int sc_open_snap_discard_ns(void); + +/** + * 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 + * - 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(struct sc_apparmor *apparmor, int snap_update_ns_fd, + const char *base_snap_name, const char *snap_name); + +/** + * Ensure that / or /snap is mounted with the SHARED option. + * + * If the system is found to be not having a shared mount for "/" + * snap-confine will create a shared bind mount for "/snap" to + * ensure that "/snap" is mounted shared. See LP:#1668659 + */ +void sc_ensure_shared_snap_mount(void); + +/** + * Set up user mounts, private to this process. + * + * If any user mounts have been configured for this process, this does + * the following: + * - create a new mount namespace + * - reconfigure all existing mounts to slave mode + * - perform all user mounts + */ +void sc_setup_user_mounts(struct sc_apparmor *apparmor, int snap_update_ns_fd, + const char *snap_name); + +#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..1eb952a4 --- /dev/null +++ b/cmd/snap-confine/ns-support-test.c @@ -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 . + * + */ + +#include "ns-support.h" +#include "ns-support.c" + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/test-utils.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; +} + +// A variant of unsetenv that is compatible with GDestroyNotify +static void my_unsetenv(const char *k) +{ + unsetenv(k); +} + +// 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(void) +{ + 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) my_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_mount_ns(void) +{ + struct sc_mount_ns *group = NULL; + group = sc_alloc_mount_ns(); + g_test_queue_free(group); + g_assert_nonnull(group); + g_assert_cmpint(group->dir_fd, ==, -1); + g_assert_cmpint(group->pipe_master[0], ==, -1); + g_assert_cmpint(group->pipe_master[1], ==, -1); + g_assert_cmpint(group->pipe_helper[0], ==, -1); + g_assert_cmpint(group->pipe_helper[1], ==, -1); + g_assert_cmpint(group->child, ==, 0); + g_assert_null(group->name); +} + +// Initialize a namespace group. +// +// The group is automatically destroyed at the end of the test. +static struct sc_mount_ns *sc_test_open_mount_ns(const char *group_name) +{ + // Initialize a namespace group + struct sc_mount_ns *group = NULL; + if (group_name == NULL) { + group_name = "test-group"; + } + group = sc_open_mount_ns(group_name); + g_test_queue_destroy((GDestroyNotify) sc_close_mount_ns, group); + // Check if the returned group data looks okay + g_assert_nonnull(group); + g_assert_cmpint(group->dir_fd, !=, -1); + g_assert_cmpint(group->pipe_master[0], ==, -1); + g_assert_cmpint(group->pipe_master[1], ==, -1); + g_assert_cmpint(group->pipe_helper[0], ==, -1); + g_assert_cmpint(group->pipe_helper[1], ==, -1); + g_assert_cmpint(group->child, ==, 0); + g_assert_cmpstr(group->name, ==, group_name); + return group; +} + +// Check that initializing a namespace group creates the appropriate +// filesystem structure. +static void test_sc_open_mount_ns(void) +{ + const char *ns_dir = sc_test_use_fake_ns_dir(); + sc_test_open_mount_ns(NULL); + // Check that the group directory exists + g_assert_true(g_file_test + (ns_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)); +} + +// Sanity check, ensure that the namespace filesystem identifier is what we +// expect, aka NSFS_MAGIC. +static void test_nsfs_fs_id(void) +{ + 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 __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/ns/sc_alloc_mount_ns", test_sc_alloc_mount_ns); + g_test_add_func("/ns/sc_open_mount_ns", test_sc_open_mount_ns); + g_test_add_func("/ns/nsfs_fs_id", test_nsfs_fs_id); +} diff --git a/cmd/snap-confine/ns-support.c b/cmd/snap-confine/ns-support.c new file mode 100644 index 00000000..d677c150 --- /dev/null +++ b/cmd/snap-confine/ns-support.c @@ -0,0 +1,802 @@ +/* + * 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 + +#include "../libsnap-confine-private/cgroup-freezer-support.h" +#include "../libsnap-confine-private/classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/locking.h" +#include "../libsnap-confine-private/mountinfo.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/tool.h" +#include "../libsnap-confine-private/utils.h" +#include "user-support.h" + +/*! + * 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; + +enum { + HELPER_CMD_EXIT, + HELPER_CMD_CAPTURE_MOUNT_NS, + HELPER_CMD_CAPTURE_PER_USER_MOUNT_NS, +}; + +void sc_reassociate_with_pid1_mount_ns(void) +{ + int init_mnt_fd SC_CLEANUP(sc_cleanup_close) = -1; + int self_mnt_fd SC_CLEANUP(sc_cleanup_close) = -1; + const char *path_pid_1 = "/proc/1/ns/mnt"; + const char *path_pid_self = "/proc/self/ns/mnt"; + + init_mnt_fd = open(path_pid_1, + O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_PATH); + if (init_mnt_fd < 0) { + die("cannot open path %s", path_pid_1); + } + self_mnt_fd = open(path_pid_self, + O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_PATH); + if (self_mnt_fd < 0) { + die("cannot open path %s", path_pid_1); + } + char init_buf[128] = { 0 }; + char self_buf[128] = { 0 }; + memset(init_buf, 0, sizeof init_buf); + if (readlinkat(init_mnt_fd, "", init_buf, sizeof init_buf) < 0) { + if (errno == ENOENT) { + // According to namespaces(7) on a pre 3.8 kernel the namespace + // files are hardlinks, not sylinks. If that happens readlinkat + // fails with ENOENT. As a quick workaround for this special-case + // functionality, just bail out and do nothing without raising an + // error. + return; + } + die("cannot read mount namespace identifier of pid 1"); + } + memset(self_buf, 0, sizeof self_buf); + if (readlinkat(self_mnt_fd, "", self_buf, sizeof self_buf) < 0) { + die("cannot read mount namespace identifier of the current process"); + } + if (memcmp(init_buf, self_buf, sizeof init_buf) != 0) { + debug("moving to mount namespace of pid 1"); + // We cannot use O_NOFOLLOW here because that file will always be a + // symbolic link. We actually want to open it this way. + int init_mnt_fd_real SC_CLEANUP(sc_cleanup_close) = -1; + init_mnt_fd_real = open(path_pid_1, O_RDONLY | O_CLOEXEC); + if (init_mnt_fd_real < 0) { + die("cannot open %s", path_pid_1); + } + if (setns(init_mnt_fd_real, CLONE_NEWNS) < 0) { + die("cannot join mount namespace of pid 1"); + } + } +} + +void sc_initialize_mount_ns(void) +{ + /* Ensure that /run/snapd/ns is a directory. */ + if (sc_nonfatal_mkpath(sc_ns_dir, 0755) < 0) { + die("cannot create directory %s", sc_ns_dir); + } + + /* Read and analyze the mount table. We need to see whether /run/snapd/ns + * is a mount point with private event propagation. */ + struct sc_mountinfo *info SC_CLEANUP(sc_cleanup_mountinfo) = NULL; + info = sc_parse_mountinfo(NULL); + if (info == NULL) { + die("cannot parse /proc/self/mountinfo"); + } + + bool is_mnt = false; + bool is_private = false; + for (struct sc_mountinfo_entry * entry = sc_first_mountinfo_entry(info); + entry != NULL; entry = sc_next_mountinfo_entry(entry)) { + /* Find /run/snapd/ns */ + if (!sc_streq(entry->mount_dir, sc_ns_dir)) { + continue; + } + is_mnt = true; + if (strstr(entry->optional_fields, "shared:") == NULL) { + /* Mount event propagation is not set to shared, good. */ + is_private = true; + } + break; + } + + if (!is_mnt) { + if (mount(sc_ns_dir, sc_ns_dir, NULL, MS_BIND | MS_REC, NULL) < + 0) { + die("cannot self-bind mount %s", sc_ns_dir); + } + } + + if (!is_private) { + if (mount(NULL, sc_ns_dir, NULL, MS_PRIVATE, NULL) < 0) { + die("cannot change propagation type to MS_PRIVATE in %s", sc_ns_dir); + } + } +} + +struct sc_mount_ns { + // 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; + // Pair of descriptors for a pair for a pipe file descriptors (read end, + // write end) that snap-confine uses to send messages to the helper + // process and back. + int pipe_helper[2]; + int pipe_master[2]; + // Identifier of the child process that is used during the one-time (per + // group) initialization and capture process. + pid_t child; +}; + +static struct sc_mount_ns *sc_alloc_mount_ns(void) +{ + struct sc_mount_ns *group = calloc(1, sizeof *group); + if (group == NULL) { + die("cannot allocate memory for sc_mount_ns"); + } + group->dir_fd = -1; + group->pipe_helper[0] = -1; + group->pipe_helper[1] = -1; + group->pipe_master[0] = -1; + group->pipe_master[1] = -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_mount_ns *sc_open_mount_ns(const char *group_name) +{ + struct sc_mount_ns *group = sc_alloc_mount_ns(); + group->dir_fd = open(sc_ns_dir, + O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW); + if (group->dir_fd < 0) { + die("cannot open directory %s", sc_ns_dir); + } + group->name = sc_strdup(group_name); + return group; +} + +void sc_close_mount_ns(struct sc_mount_ns *group) +{ + if (group->child != 0) { + sc_wait_for_helper(group); + } + sc_cleanup_close(&group->dir_fd); + sc_cleanup_close(&group->pipe_master[0]); + sc_cleanup_close(&group->pipe_master[1]); + sc_cleanup_close(&group->pipe_helper[0]); + sc_cleanup_close(&group->pipe_helper[1]); + free(group->name); + free(group); +} + +static dev_t find_base_snap_device(const char *base_snap_name, + const char *base_snap_rev) +{ + // Find the backing device of the base snap. + // TODO: add support for "try mode" base snaps that also need + // consideration of the mie->root component. + dev_t base_snap_dev = 0; + char base_squashfs_path[PATH_MAX]; + sc_must_snprintf(base_squashfs_path, + sizeof base_squashfs_path, "%s/%s/%s", + SNAP_MOUNT_DIR, base_snap_name, base_snap_rev); + struct sc_mountinfo *mi SC_CLEANUP(sc_cleanup_mountinfo) = NULL; + mi = sc_parse_mountinfo(NULL); + if (mi == NULL) { + die("cannot parse mountinfo of the current process"); + } + bool found = false; + for (struct sc_mountinfo_entry * mie = + sc_first_mountinfo_entry(mi); mie != NULL; + mie = sc_next_mountinfo_entry(mie)) { + if (sc_streq(mie->mount_dir, base_squashfs_path)) { + base_snap_dev = makedev(mie->dev_major, mie->dev_minor); + debug("block device of snap %s, revision %s is %d:%d", + base_snap_name, base_snap_rev, mie->dev_major, + mie->dev_minor); + // Don't break when found, we are interested in the last + // entry as this is the "effective" one. + found = true; + } + } + if (!found) { + die("cannot find mount entry for snap %s revision %s", + base_snap_name, base_snap_rev); + } + return base_snap_dev; +} + +static bool should_discard_current_ns(dev_t base_snap_dev) +{ + // Inspect the namespace and check if we should discard it. + // + // The namespace may become "stale" when the rootfs is not the same + // device we found above. This will happen whenever the base snap is + // refreshed since the namespace was first created. + struct sc_mountinfo_entry *mie; + struct sc_mountinfo *mi SC_CLEANUP(sc_cleanup_mountinfo) = NULL; + + mi = sc_parse_mountinfo(NULL); + if (mi == NULL) { + die("cannot parse mountinfo of the current process"); + } + for (mie = sc_first_mountinfo_entry(mi); mie != NULL; + mie = sc_next_mountinfo_entry(mie)) { + if (!sc_streq(mie->mount_dir, "/")) { + continue; + } + // NOTE: we want the initial rootfs just in case overmount + // was used to do something weird. The initial rootfs was + // set up by snap-confine and that is the one we want to + // measure. + debug("block device of the root filesystem is %d:%d", + mie->dev_major, mie->dev_minor); + return base_snap_dev != makedev(mie->dev_major, mie->dev_minor); + } + die("cannot find mount entry of the root filesystem"); +} + +enum sc_discard_vote { + SC_DISCARD_NO = 1, + SC_DISCARD_YES = 2, +}; + +// The namespace may be stale. To check this we must actually switch into it +// but then we use up our setns call (the kernel misbehaves if we setns twice). +// To work around this we'll fork a child and use it to probe. The child will +// inspect the namespace and send information back via eventfd and then exit +// unconditionally. +static int sc_inspect_and_maybe_discard_stale_ns(int mnt_fd, + const char *snap_name, + const char *base_snap_name, + int snap_discard_ns_fd) +{ + char base_snap_rev[PATH_MAX] = { 0 }; + char fname[PATH_MAX] = { 0 }; + dev_t base_snap_dev; + int event_fd SC_CLEANUP(sc_cleanup_close) = -1; + + // Read the revision of the base snap by looking at the current symlink. + sc_must_snprintf(fname, sizeof fname, "%s/%s/current", + SNAP_MOUNT_DIR, base_snap_name); + if (readlink(fname, base_snap_rev, sizeof base_snap_rev) < 0) { + die("cannot read current revision of snap %s", snap_name); + } + if (base_snap_rev[sizeof base_snap_rev - 1] != '\0') { + die("cannot read current revision of snap %s: value too long", + snap_name); + } + // Find the device that is backing the current revision of the base snap. + base_snap_dev = find_base_snap_device(base_snap_name, base_snap_rev); + + // Check if we are running in normal mode with pivot root. Do this here + // because once on the inside of the transformed mount namespace we can no + // longer tell. + bool is_normal_mode = + sc_should_use_normal_mode(sc_classify_distro(), base_snap_name); + + // Store the PID of this process. This is done instead of calls to + // getppid() below because then we can reliably track the PID of the + // parent even if the child process is re-parented. + pid_t parent = getpid(); + + // Create an eventfd for the communication with the child. + event_fd = eventfd(0, EFD_CLOEXEC); + if (event_fd < 0) { + die("cannot create eventfd"); + } + // Fork a child, it will do the inspection for us. + pid_t child = fork(); + if (child < 0) { + die("cannot fork support process"); + } + + if (child == 0) { + // This is the child process which will inspect the mount namespace. + // + // 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. + if (kill(parent, 0) < 0) { + switch (errno) { + case ESRCH: + debug("parent process has terminated"); + abort(); + default: + die("cannot confirm that parent process is alive"); + break; + } + } + + debug("joining preserved mount namespace for inspection"); + // Move to the mount namespace of the snap we're trying to inspect. + if (setns(mnt_fd, CLONE_NEWNS) < 0) { + die("cannot join preserved mount namespace"); + } + // Check if the namespace needs to be discarded. + // + // TODO: enable this for core distributions. This is complex because on + // core the rootfs is mounted in initrd and is _not_ changed (no + // pivot_root) and the base snap is again mounted (2nd time) by + // systemd. This makes us end up in a situation where the outer base + // snap will never match the rootfs inside the mount namespace. + bool should_discard = + is_normal_mode ? should_discard_current_ns(base_snap_dev) : + false; + + // Send this back to the parent: 2 - discard, 1 - keep. + // Note that we cannot just use 0 and 1 because of the semantics of eventfd(2). + if (eventfd_write(event_fd, should_discard ? + SC_DISCARD_YES : SC_DISCARD_NO) < 0) { + die("cannot send information to %s preserved mount namespace", should_discard ? "discard" : "keep"); + } + // Exit, we're done. + exit(0); + } + // This is back in the parent process. + // + // Enable a sanity timeout in case the read blocks for unbound amount of + // time. This will ensure we will not hang around while holding the lock. + // Next, read the value written by the child process. + sc_enable_sanity_timeout(); + eventfd_t value = 0; + if (eventfd_read(event_fd, &value) < 0) { + die("cannot read from eventfd"); + } + sc_disable_sanity_timeout(); + + // Wait for the child process to exit and collect its exit status. + errno = 0; + int status = 0; + if (waitpid(child, &status, 0) < 0) { + die("cannot wait for the support process for mount namespace inspection"); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + die("support process for mount namespace inspection exited abnormally"); + } + // If the namespace is up-to-date then we are done. + if (value == SC_DISCARD_NO) { + debug("preserved mount namespace can be reused"); + return 0; + } + // The namespace is stale, let's check if we can discard it. + if (sc_cgroup_freezer_occupied(snap_name)) { + // Some processes are still using the namespace so we cannot discard it + // as that would fracture the view that the set of processes inside + // have on what is mounted. + debug("preserved mount namespace is stale but occupied"); + return 0; + } + // The namespace is both stale and empty. We can discard it now. + sc_call_snap_discard_ns(snap_discard_ns_fd, snap_name); + return EAGAIN; +} + +static void helper_fork(struct sc_mount_ns *group, + struct sc_apparmor *apparmor); +static void helper_main(struct sc_mount_ns *group, struct sc_apparmor *apparmor, + pid_t parent); +static void helper_capture_ns(struct sc_mount_ns *group, pid_t parent); +static void helper_capture_per_user_ns(struct sc_mount_ns *group, pid_t parent); + +int sc_join_preserved_ns(struct sc_mount_ns *group, struct sc_apparmor + *apparmor, const char *base_snap_name, + const char *snap_name, int snap_discard_ns_fd) +{ + // Open the mount namespace file. + char mnt_fname[PATH_MAX] = { 0 }; + sc_must_snprintf(mnt_fname, sizeof mnt_fname, "%s.mnt", group->name); + int mnt_fd SC_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. + mnt_fd = openat(group->dir_fd, mnt_fname, + O_CREAT | O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600); + if (mnt_fd < 0) { + die("cannot open preserved mount namespace %s", group->name); + } + // Check if we got an nsfs-based or procfs file or a regular file. This can + // be reliably tested because nsfs has an unique filesystem type + // NSFS_MAGIC. On older kernels that don't support nsfs yet we can look + // for PROC_SUPER_MAGIC instead. + // We can just ensure that this is the case thanks to fstatfs. + struct statfs ns_statfs_buf; + if (fstatfs(mnt_fd, &ns_statfs_buf) < 0) { + die("cannot inspect filesystem of preserved mount namespace file"); + } + // Stat the mount namespace as well, this is later used to check if the + // namespace is used by other processes if we are considering discarding a + // stale namespace. + struct stat ns_stat_buf; + if (fstat(mnt_fd, &ns_stat_buf) < 0) { + die("cannot inspect preserved mount namespace file"); + } +#ifndef NSFS_MAGIC +// Account for kernel headers old enough to not know about NSFS_MAGIC. +#define NSFS_MAGIC 0x6e736673 +#endif + if (ns_statfs_buf.f_type == NSFS_MAGIC + || ns_statfs_buf.f_type == PROC_SUPER_MAGIC) { + + // Inspect and perhaps discard the preserved mount namespace. + if (sc_inspect_and_maybe_discard_stale_ns + (mnt_fd, snap_name, base_snap_name, + snap_discard_ns_fd) == EAGAIN) { + return ESRCH; + } + // Remember the vanilla working directory so that we may attempt to restore it later. + char *vanilla_cwd SC_CLEANUP(sc_cleanup_string) = NULL; + vanilla_cwd = get_current_dir_name(); + if (vanilla_cwd == NULL) { + die("cannot get the current working directory"); + } + // Move to the mount namespace of the snap we're trying to start. + if (setns(mnt_fd, CLONE_NEWNS) < 0) { + die("cannot join preserved mount namespace %s", + group->name); + } + debug("joined preserved mount namespace %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 enter %s, moving to void", vanilla_cwd); + if (chdir(SC_VOID_DIR) != 0) { + die("cannot change directory to %s", + SC_VOID_DIR); + } + } + return 0; + } + return ESRCH; +} + +int sc_join_preserved_per_user_ns(struct sc_mount_ns *group, + const char *snap_name) +{ + uid_t uid = getuid(); + char mnt_fname[PATH_MAX] = { 0 }; + sc_must_snprintf(mnt_fname, sizeof mnt_fname, "%s.%d.mnt", group->name, + (int)uid); + + int mnt_fd SC_CLEANUP(sc_cleanup_close) = -1; + mnt_fd = openat(group->dir_fd, mnt_fname, + O_CREAT | O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600); + if (mnt_fd < 0) { + die("cannot open preserved mount namespace %s", group->name); + } + struct statfs ns_statfs_buf; + if (fstatfs(mnt_fd, &ns_statfs_buf) < 0) { + die("cannot inspect filesystem of preserved mount namespace file"); + } + struct stat ns_stat_buf; + if (fstat(mnt_fd, &ns_stat_buf) < 0) { + die("cannot inspect preserved mount namespace file"); + } +#ifndef NSFS_MAGIC + /* Define NSFS_MAGIC for Ubuntu 14.04 and other older systems. */ +#define NSFS_MAGIC 0x6e736673 +#endif + if (ns_statfs_buf.f_type == NSFS_MAGIC + || ns_statfs_buf.f_type == PROC_SUPER_MAGIC) { + // TODO: refactor the cwd workflow across all of snap-confine. + char *vanilla_cwd SC_CLEANUP(sc_cleanup_string) = NULL; + vanilla_cwd = get_current_dir_name(); + if (vanilla_cwd == NULL) { + die("cannot get the current working directory"); + } + if (setns(mnt_fd, CLONE_NEWNS) < 0) { + die("cannot join preserved per-user mount namespace %s", + group->name); + } + debug("joined preserved mount namespace %s", group->name); + if (chdir(vanilla_cwd) != 0) { + debug("cannot enter %s, moving to void", vanilla_cwd); + if (chdir(SC_VOID_DIR) != 0) { + die("cannot change directory to %s", + SC_VOID_DIR); + } + } + return 0; + } + return ESRCH; +} + +static void setup_signals_for_helper(void) +{ + /* Ignore the SIGPIPE signal so that we get EPIPE on the read / write + * operations attempting to work with a closed pipe. This ensures that we + * are not killed by the default disposition (terminate) and can return a + * non-signal-death return code to the program invoking snap-confine. */ + if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) { + die("cannot install ignore handler for SIGPIPE"); + } +} + +static void teardown_signals_for_helper(void) +{ + /* Undo operations done by setup_signals_for_helper. */ + if (signal(SIGPIPE, SIG_DFL) == SIG_ERR) { + die("cannot restore default handler for SIGPIPE"); + } +} + +static void helper_fork(struct sc_mount_ns *group, struct sc_apparmor *apparmor) +{ + // Create a pipe for sending commands to the helper process. + if (pipe2(group->pipe_master, O_CLOEXEC | O_DIRECT) < 0) { + die("cannot create pipes for commanding the helper process"); + } + if (pipe2(group->pipe_helper, O_CLOEXEC | O_DIRECT) < 0) { + die("cannot create pipes for responding to master process"); + } + // 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(); + + // For rationale of forking see this: + // https://lists.linuxfoundation.org/pipermail/containers/2013-August/033386.html + pid_t pid = fork(); + if (pid < 0) { + die("cannot fork helper process for mount namespace capture"); + } + if (pid == 0) { + /* helper */ + sc_cleanup_close(&group->pipe_master[1]); + sc_cleanup_close(&group->pipe_helper[0]); + helper_main(group, apparmor, parent); + } else { + setup_signals_for_helper(); + + /* master */ + sc_cleanup_close(&group->pipe_master[0]); + sc_cleanup_close(&group->pipe_helper[1]); + + // 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. + debug("forked support process %d", (int)pid); + group->child = pid; + } +} + +static void helper_main(struct sc_mount_ns *group, struct sc_apparmor *apparmor, + pid_t parent) +{ + // This is the child process which will capture the mount namespace. + // + // It will do so by bind-mounting the .mnt 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. + 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 read(2) 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 the pipe below. + if (kill(parent, 0) < 0) { + switch (errno) { + case ESRCH: + debug("parent process has terminated"); + abort(); + default: + die("cannot confirm that parent process is alive"); + break; + } + } + if (fchdir(group->dir_fd) < 0) { + die("cannot move to directory with preserved namespaces"); + } + int command = -1; + int run = 1; + while (run) { + debug("helper process waiting for command"); + sc_enable_sanity_timeout(); + if (read(group->pipe_master[0], &command, sizeof command) < 0) { + int saved_errno = errno; + // This will ensure we get the correct error message + // if there is a read error because the timeout + // expired. + sc_disable_sanity_timeout(); + errno = saved_errno; + die("cannot read command from the pipe"); + } + sc_disable_sanity_timeout(); + debug("helper process received command %d", command); + switch (command) { + case HELPER_CMD_EXIT: + run = 0; + break; + case HELPER_CMD_CAPTURE_MOUNT_NS: + helper_capture_ns(group, parent); + break; + case HELPER_CMD_CAPTURE_PER_USER_MOUNT_NS: + helper_capture_per_user_ns(group, parent); + break; + } + if (write(group->pipe_helper[1], &command, sizeof command) < 0) { + die("cannot write ack"); + } + } + debug("helper process exiting"); + exit(0); +} + +static void helper_capture_ns(struct sc_mount_ns *group, pid_t parent) +{ + char src[PATH_MAX] = { 0 }; + char dst[PATH_MAX] = { 0 }; + + debug("capturing per-snap mount namespace"); + sc_must_snprintf(src, sizeof src, "/proc/%d/ns/mnt", (int)parent); + sc_must_snprintf(dst, sizeof dst, "%s.mnt", group->name); + + /* Ensure the bind mount destination exists. */ + int fd = open(dst, O_CREAT | O_CLOEXEC | O_NOFOLLOW | O_RDONLY, 0600); + if (fd < 0) { + die("cannot create file %s", dst); + } + close(fd); + if (mount(src, dst, NULL, MS_BIND, NULL) < 0) { + die("cannot preserve mount namespace of process %d as %s", + (int)parent, dst); + } + debug("mount namespace of process %d preserved as %s", + (int)parent, dst); +} + +static void helper_capture_per_user_ns(struct sc_mount_ns *group, pid_t parent) +{ + char src[PATH_MAX] = { 0 }; + char dst[PATH_MAX] = { 0 }; + uid_t uid = getuid(); + + debug("capturing per-snap, per-user mount namespace"); + sc_must_snprintf(src, sizeof src, "/proc/%d/ns/mnt", (int)parent); + sc_must_snprintf(dst, sizeof dst, "%s.%d.mnt", group->name, (int)uid); + if (mount(src, dst, NULL, MS_BIND, NULL) < 0) { + die("cannot preserve per-user mount namespace of process %d as %s", (int)parent, dst); + } + debug("per-user mount namespace of process %d preserved as %s", + (int)parent, dst); +} + +static void sc_message_capture_helper(struct sc_mount_ns *group, int command_id) +{ + int ack; + if (group->child == 0) { + die("precondition failed: we don't have a helper process"); + } + if (group->pipe_master[1] < 0) { + die("precondition failed: we don't have a pipe"); + } + if (group->pipe_helper[0] < 0) { + die("precondition failed: we don't have a pipe"); + } + debug("sending command %d to helper process (pid: %d)", + command_id, group->child); + if (write(group->pipe_master[1], &command_id, sizeof command_id) < 0) { + die("cannot send command %d to helper process", command_id); + } + debug("waiting for response from helper"); + int read_n = read(group->pipe_helper[0], &ack, sizeof ack); + if (read_n < 0) { + die("cannot receive ack from helper process"); + } + if (read_n == 0) { + die("unexpected eof from helper process"); + } +} + +static void sc_wait_for_capture_helper(struct sc_mount_ns *group) +{ + if (group->child == 0) { + die("precondition failed: we don't have a helper process"); + } + debug("waiting for the helper process to exit"); + int status = 0; + errno = 0; + if (waitpid(group->child, &status, 0) < 0) { + die("cannot wait for the helper process"); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + die("helper process exited abnormally"); + } + debug("helper process exited normally"); + group->child = 0; + teardown_signals_for_helper(); +} + +void sc_fork_helper(struct sc_mount_ns *group, struct sc_apparmor *apparmor) +{ + helper_fork(group, apparmor); +} + +void sc_preserve_populated_mount_ns(struct sc_mount_ns *group) +{ + sc_message_capture_helper(group, HELPER_CMD_CAPTURE_MOUNT_NS); +} + +void sc_preserve_populated_per_user_mount_ns(struct sc_mount_ns *group) +{ + sc_message_capture_helper(group, HELPER_CMD_CAPTURE_PER_USER_MOUNT_NS); +} + +void sc_wait_for_helper(struct sc_mount_ns *group) +{ + sc_message_capture_helper(group, HELPER_CMD_EXIT); + sc_wait_for_capture_helper(group); +} diff --git a/cmd/snap-confine/ns-support.h b/cmd/snap-confine/ns-support.h new file mode 100644 index 00000000..288ad10a --- /dev/null +++ b/cmd/snap-confine/ns-support.h @@ -0,0 +1,148 @@ +/* + * 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 "../libsnap-confine-private/apparmor-support.h" + +/** + * Re-associate the current process with the mount namespace of pid 1. + * + * This function inspects the mount namespace of the current process and that + * of pid 1. In case they differ the current process is re-associated with the + * mount namespace of pid 1. + * + * This function should be called before sc_initialize_mount_ns(). + **/ +void sc_reassociate_with_pid1_mount_ns(void); + +/** + * 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 should be called with a global lock (see sc_lock_global) held + * to ensure that no other instance of snap-confine attempts to do this + * concurrently. + * + * 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_mount_ns(void); + +/** + * Data required to manage namespaces amongst a group of processes. + */ +struct sc_mount_ns; + +/** + * Open a namespace group. + * + * This will open and keep file descriptors for /run/snapd/ns/. + * + * The following methods should be called only while holding a lock protecting + * that specific snap namespace: + * - sc_create_or_join_mount_ns() + * - sc_preserve_populated_mount_ns() + */ +struct sc_mount_ns *sc_open_mount_ns(const char *group_name); + +/** + * Close namespace group. + * + * This will close all of the open file descriptors and release allocated memory. + */ +void sc_close_mount_ns(struct sc_mount_ns *group); + +/** + * Join a preserved mount namespace 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 preserved mount namespace does not exist or exists but is stale and + * was discarded and returns ESRCH. If the mount namespace was joined the + * function returns zero. + **/ +int sc_join_preserved_ns(struct sc_mount_ns *group, struct sc_apparmor + *apparmor, const char *base_snap_name, + const char *snap_name, int snap_discard_ns_fd); + +/** + * Join a preserved, per-user, mount namespace if one exists. + * + * Technically the function opens /run/snapd/ns/snap.$SNAP_NAME.$UID.mnt and + * tries to use setns() with the obtained file descriptor. + * + * The return is ESRCH if a preserved per-user mount namespace does not exist + * and cannot be joined or zero otherwise. +**/ +int sc_join_preserved_per_user_ns(struct sc_mount_ns *group, + const char *snap_name); + +/** + * Fork off a helper process for mount namespace capture. + * + * This function forks the helper process. It needs to be paired with + * sc_wait_for_helper which instructs the helper to shut down and waits for + * that to happen. + * + * For rationale for forking and using a helper process please see + * https://lists.linuxfoundation.org/pipermail/containers/2013-August/033386.html + **/ +void sc_fork_helper(struct sc_mount_ns *group, struct sc_apparmor *apparmor); + +/** + * Preserve prepared namespace group. + * + * This function signals the child support process for namespace capture to + * perform the capture. + * + * Technically this function writes to pipe that causes the child process to + * wake up and bind mount /proc/$ppid/ns/mnt to + * /run/snapd/ns/${group_name}.mnt. + * + * The helper process will wait for subsequent commands. Please call + * sc_wait_for_helper() to terminate it. + **/ +void sc_preserve_populated_mount_ns(struct sc_mount_ns *group); + +void sc_preserve_populated_per_user_mount_ns(struct sc_mount_ns *group); + +/** + * Ask the helper process to terminate and wait for it to finish. + * + * This function asks the helper process to exit by writing an appropriate + * command to the pipe used for the inter process communication between the + * main snap-confine process and the helper and then waits for the process to + * terminate cleanly. + **/ +void sc_wait_for_helper(struct sc_mount_ns *group); + +#endif diff --git a/cmd/snap-confine/seccomp-support-ext.c b/cmd/snap-confine/seccomp-support-ext.c new file mode 100644 index 00000000..c3892648 --- /dev/null +++ b/cmd/snap-confine/seccomp-support-ext.c @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2019 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 "seccomp-support-ext.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/utils.h" + +#ifndef SECCOMP_FILTER_FLAG_LOG +#define SECCOMP_FILTER_FLAG_LOG 2 +#endif + +#ifndef seccomp +// prototype because we build with -Wstrict-prototypes +int seccomp(unsigned int operation, unsigned int flags, void *args); + +int seccomp(unsigned int operation, unsigned int flags, void *args) { + errno = 0; + return syscall(__NR_seccomp, operation, flags, args); +} +#endif + +size_t sc_read_seccomp_filter(const char *filename, char *buf, size_t buf_size) { + FILE *file = fopen(filename, "rb"); + if (file == NULL) { + die("cannot open seccomp filter %s", filename); + } + size_t num_read = fread(buf, 1, buf_size, file); + if (ferror(file) != 0) { + die("cannot read seccomp profile %s", filename); + } + if (feof(file) == 0) { + die("cannot fit seccomp profile %s to memory buffer", filename); + } + fclose(file); + debug("read %zu bytes from %s", num_read, filename); + return num_read; +} + +void sc_apply_seccomp_filter(struct sock_fprog *prog) { + uid_t real_uid, effective_uid, saved_uid; + int err; + + if (getresuid(&real_uid, &effective_uid, &saved_uid) < 0) { + die("cannot call getresuid"); + } + + // If we can, raise privileges so that we can load the BPF into the kernel + // via 'prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...)'. + 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 filter into the kernel. + // + // Importantly we are intentionally *not* setting NO_NEW_PRIVS because it + // interferes with exec transitions in AppArmor with certain snappy + // interfaces. Not setting NO_NEW_PRIVS does mean that applications can + // adjust their sandbox if they have CAP_SYS_ADMIN or, if running on < 4.8 + // kernels, break out of the seccomp via ptrace. Both CAP_SYS_ADMIN and + // 'ptrace (trace)' are blocked by AppArmor with typical snappy interfaces. + err = seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_LOG, prog); + if (err != 0) { + /* The profile may fail to load using the "modern" interface. + * In such case use the older prctl-based interface instead. */ + switch (errno) { + case ENOSYS: + debug("kernel doesn't support the seccomp(2) syscall"); + break; + case EINVAL: + debug("kernel may not support the SECCOMP_FILTER_FLAG_LOG flag"); + break; + } + debug("falling back to prctl(2) syscall to load seccomp filter"); + err = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, prog); + if (err != 0) { + die("cannot apply seccomp profile"); + } + } + + /* 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-ext.h b/cmd/snap-confine/seccomp-support-ext.h new file mode 100644 index 00000000..a3a028f9 --- /dev/null +++ b/cmd/snap-confine/seccomp-support-ext.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2019 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_EXT_H +#define SNAP_CONFINE_SECCOMP_SUPPORT_EXT_H + +#include +#include + +size_t sc_read_seccomp_filter(const char *filename, char *buf, size_t buf_size); + +/** + * Apply a given bpf program as a seccomp system call filter. + **/ +void sc_apply_seccomp_filter(struct sock_fprog *prog); + +#endif diff --git a/cmd/snap-confine/seccomp-support.c b/cmd/snap-confine/seccomp-support.c new file mode 100644 index 00000000..82a8de5d --- /dev/null +++ b/cmd/snap-confine/seccomp-support.c @@ -0,0 +1,185 @@ +/* + * 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 +#include + +#include +#include + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/secure-getenv.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +#include "seccomp-support-ext.h" + +static const char *filter_profile_dir = "/var/lib/snapd/seccomp/bpf/"; + +// MAX_BPF_SIZE is an arbitrary limit. +#define MAX_BPF_SIZE (32 * 1024) + +typedef struct sock_filter bpf_instr; + +static void validate_path_has_strict_perms(const char *path) +{ + struct stat stat_buf; + if (stat(path, &stat_buf) < 0) { + die("cannot stat %s", path); + } + + errno = 0; + if (stat_buf.st_uid != 0 || stat_buf.st_gid != 0) { + die("%s not root-owned %i:%i", path, stat_buf.st_uid, + stat_buf.st_gid); + } + + if (stat_buf.st_mode & S_IWOTH) { + die("%s has 'other' write %o", path, stat_buf.st_mode); + } +} + +static void validate_bpfpath_is_safe(const char *path) +{ + if (path == NULL || strlen(path) == 0 || path[0] != '/') { + die("valid_bpfpath_is_safe needs an absolute path as input"); + } + // strtok_r() modifies its first argument, so work on a copy + char *tokenized SC_CLEANUP(sc_cleanup_string) = NULL; + tokenized = sc_strdup(path); + // allocate a string large enough to hold path, and initialize it to + // '/' + size_t checked_path_size = strlen(path) + 1; + char *checked_path SC_CLEANUP(sc_cleanup_string) = NULL; + checked_path = calloc(checked_path_size, 1); + if (checked_path == NULL) { + die("cannot allocate memory for checked_path"); + } + + checked_path[0] = '/'; + checked_path[1] = '\0'; + + // validate '/' + validate_path_has_strict_perms(checked_path); + + // strtok_r needs a pointer to keep track of where it is in the + // string. + char *buf_saveptr = NULL; + + // reconstruct the path from '/' down to profile_name + char *buf_token = strtok_r(tokenized, "/", &buf_saveptr); + while (buf_token != NULL) { + char *prev SC_CLEANUP(sc_cleanup_string) = NULL; + prev = sc_strdup(checked_path); // needed by vsnprintf in sc_must_snprintf + // append '' if checked_path is '/', otherwise '/' + if (strlen(checked_path) == 1) { + sc_must_snprintf(checked_path, checked_path_size, + "%s%s", prev, buf_token); + } else { + sc_must_snprintf(checked_path, checked_path_size, + "%s/%s", prev, buf_token); + } + validate_path_has_strict_perms(checked_path); + + buf_token = strtok_r(NULL, "/", &buf_saveptr); + } +} + +bool sc_apply_seccomp_profile_for_security_tag(const char *security_tag) +{ + debug("loading bpf program for security tag %s", security_tag); + + char profile_path[PATH_MAX] = { 0 }; + sc_must_snprintf(profile_path, sizeof(profile_path), "%s/%s.bin", + filter_profile_dir, security_tag); + + // Wait some time for the security profile to show up. When + // the system boots snapd will created security profiles, but + // a service snap (e.g. network-manager) starts in parallel with + // snapd so for such snaps, the profiles may not be generated + // yet + long max_wait = 120; + const char *MAX_PROFILE_WAIT = getenv("SNAP_CONFINE_MAX_PROFILE_WAIT"); + if (MAX_PROFILE_WAIT != NULL) { + char *endptr = NULL; + errno = 0; + long env_max_wait = strtol(MAX_PROFILE_WAIT, &endptr, 10); + if (errno != 0 || MAX_PROFILE_WAIT == endptr || *endptr != '\0' + || env_max_wait <= 0) { + die("SNAP_CONFINE_MAX_PROFILE_WAIT invalid"); + } + max_wait = env_max_wait > 0 ? env_max_wait : max_wait; + } + if (max_wait > 3600) { + max_wait = 3600; + } + for (long i = 0; i < max_wait; ++i) { + if (access(profile_path, F_OK) == 0) { + break; + } + sleep(1); + } + + // TODO: move over to open/openat as an additional hardening measure. + + // validate '/' down to profile_path are root-owned and not + // 'other' writable to avoid possibility of privilege + // escalation via bpf program load when paths are incorrectly + // set on the system. + validate_bpfpath_is_safe(profile_path); + + char bpf[MAX_BPF_SIZE + 1] = { 0 }; // account for EOF + size_t num_read = sc_read_seccomp_filter(profile_path, bpf, sizeof bpf); + if (sc_streq(bpf, "@unrestricted\n")) { + return false; + } + struct sock_fprog prog = { + .len = num_read / sizeof(struct sock_filter), + .filter = (struct sock_filter *)bpf, + }; + sc_apply_seccomp_filter(&prog); + return true; +} + +void sc_apply_global_seccomp_profile(void) +{ + const char *profile_path = "/var/lib/snapd/seccomp/bpf/global.bin"; + /* The profile may be absent. */ + if (access(profile_path, F_OK) != 0) { + return; + } + // TODO: move over to open/openat as an additional hardening measure. + validate_bpfpath_is_safe(profile_path); + + char bpf[MAX_BPF_SIZE + 1] = { 0 }; + size_t num_read = sc_read_seccomp_filter(profile_path, bpf, sizeof bpf); + struct sock_fprog prog = { + .len = num_read / sizeof(struct sock_filter), + .filter = (struct sock_filter *)bpf, + }; + sc_apply_seccomp_filter(&prog); +} diff --git a/cmd/snap-confine/seccomp-support.h b/cmd/snap-confine/seccomp-support.h new file mode 100644 index 00000000..7fd79420 --- /dev/null +++ b/cmd/snap-confine/seccomp-support.h @@ -0,0 +1,51 @@ +/* + * 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 +#include + +/** + * sc_apply_seccomp_profile_for_security_tag applies a seccomp profile to the + * current process. The filter is loaded from a pre-compiled bpf bytecode + * stored in "/var/lib/snap/seccomp/bpf" using the security tag and the + * extension ".bin". All components along that path must be owned by root and + * cannot be writable by UNIX _other_. + * + * The security tag is shared with other parts of snapd. + * For applications it is the string "snap.${SNAP_INSTANCE_NAME}.${app}". + * For hooks it is "snap.${SNAP_INSTANCE_NAME}.hook.{hook_name}". + * + * Profiles must be present in the file-system. If a profile is not present + * then several attempts are made, each coupled with a sleep period. Up 3600 + * seconds may elapse before the function gives up. Unless + * $SNAP_CONFINE_MAX_PROFILE_WAIT environment variable dictates otherwise, the + * default wait time is 120 seconds. + * + * A profile may contain valid BPF program or the string "@unrestricted\n". In + * the former case the profile is applied to the current process using + * sc_apply_seccomp_filter. In the latter case no action takes place. + * + * The return value indicates if the process uses confinement or runs under the + * special non-confining "@unrestricted" profile. + **/ +bool sc_apply_seccomp_profile_for_security_tag(const char *security_tag); + +void sc_apply_global_seccomp_profile(void); + +#endif diff --git a/cmd/snap-confine/snap-confine-args-test.c b/cmd/snap-confine/snap-confine-args-test.c new file mode 100644 index 00000000..95bfe1a7 --- /dev/null +++ b/cmd/snap-confine/snap-confine-args-test.c @@ -0,0 +1,482 @@ +/* + * 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-confine-args.h" +#include "snap-confine-args.c" +#include "../libsnap-confine-private/cleanup-funcs.h" + +#include + +#include + +/** + * Create an argc + argv pair out of a NULL terminated argument list. + **/ +static void + __attribute__ ((sentinel)) test_argc_argv(int *argcp, char ***argvp, ...) +{ + int argc = 0; + char **argv = NULL; + g_test_queue_free(argv); + + va_list ap; + va_start(ap, argvp); + const char *arg; + do { + arg = va_arg(ap, const char *); + // XXX: yeah, wrong way but the worse that can happen is for test to fail + argv = realloc(argv, sizeof(const char **) * (argc + 1)); + g_assert_nonnull(argv); + if (arg != NULL) { + char *arg_copy = sc_strdup(arg); + g_test_queue_free(arg_copy); + argv[argc] = arg_copy; + argc += 1; + } else { + argv[argc] = NULL; + } + } while (arg != NULL); + va_end(ap); + + *argcp = argc; + *argvp = argv; +} + +static void test_test_argc_argv(void) +{ + // Check that test_argc_argv() correctly stores data + int argc; + char **argv; + + test_argc_argv(&argc, &argv, NULL); + g_assert_cmpint(argc, ==, 0); + g_assert_null(argv[0]); + + test_argc_argv(&argc, &argv, "zero", "one", "two", NULL); + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "zero"); + g_assert_cmpstr(argv[1], ==, "one"); + g_assert_cmpstr(argv[2], ==, "two"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__typical(void) +{ + // Test that typical invocation of snap-confine is parsed correctly. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "snap.SNAP_NAME.APP_NAME", + "/usr/lib/snapd/snap-exec", "--option", "arg", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); + g_assert_null(sc_args_base_snap(args)); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/lib/snapd/snap-confine"); + g_assert_cmpstr(argv[1], ==, "--option"); + g_assert_cmpstr(argv[2], ==, "arg"); + g_assert_null(argv[3]); +} + +static void test_sc_cleanup_args(void) +{ + // Check that NULL argument parser can be cleaned up + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args = NULL; + sc_cleanup_args(&args); + + // Check that a non-NULL argument parser can be cleaned up + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "snap.SNAP_NAME.APP_NAME", "/usr/lib/snapd/snap-exec", + NULL); + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + sc_cleanup_args(&args); + g_assert_null(args); +} + +static void test_sc_nonfatal_parse_args__typical_classic(void) +{ + // Test that typical invocation of snap-confine is parsed correctly. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--classic", + "snap.SNAP_NAME.APP_NAME", "/usr/lib/snapd/snap-exec", + "--option", "arg", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, true); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/lib/snapd/snap-confine"); + g_assert_cmpstr(argv[1], ==, "--option"); + g_assert_cmpstr(argv[2], ==, "arg"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__ubuntu_core_launcher(void) +{ + // Test that typical legacy invocation of snap-confine via the + // ubuntu-core-launcher symlink, with duplicated security tag, is parsed + // correctly. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/bin/ubuntu-core-launcher", + "snap.SNAP_NAME.APP_NAME", "snap.SNAP_NAME.APP_NAME", + "/usr/lib/snapd/snap-exec", "--option", "arg", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/bin/ubuntu-core-launcher"); + g_assert_cmpstr(argv[1], ==, "--option"); + g_assert_cmpstr(argv[2], ==, "arg"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__version(void) +{ + // Test that snap-confine --version is detected. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--version", "ignored", + "garbage", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_null(sc_args_security_tag(args)); + g_assert_null(sc_args_executable(args)); + g_assert_cmpint(sc_args_is_version_query(args), ==, true); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/lib/snapd/snap-confine"); + g_assert_cmpstr(argv[1], ==, "ignored"); + g_assert_cmpstr(argv[2], ==, "garbage"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__evil_input(void) +{ + // Check that calling without any arguments is reported as error. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + // NULL argcp/argvp attack + args = sc_nonfatal_parse_args(NULL, NULL, &err); + + g_assert_nonnull(err); + g_assert_null(args); + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argcp or argvp is NULL"); + + int argc; + char **argv; + + // NULL argv attack + argc = 0; + argv = NULL; + args = sc_nonfatal_parse_args(&argc, &argv, &err); + + g_assert_nonnull(err); + g_assert_null(args); + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argc is zero or argv is NULL"); + + // NULL argv[i] attack + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--version", "ignored", + "garbage", NULL); + argv[1] = NULL; // overwrite --version with NULL + args = sc_nonfatal_parse_args(&argc, &argv, &err); + + g_assert_nonnull(err); + g_assert_null(args); + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argument at index 1 is NULL"); +} + +static void test_sc_nonfatal_parse_args__nothing_to_parse(void) +{ + // Check that calling without any arguments is reported as error. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argc is zero or argv is NULL"); +} + +static void test_sc_nonfatal_parse_args__no_security_tag(void) +{ + // Check that lack of security tag is reported as error. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "Usage: snap-confine \n" + "\napplication or hook security tag was not provided"); + + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__no_executable(void) +{ + // Check that lack of security tag is reported as error. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "snap.SNAP_NAME.APP_NAME", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "Usage: snap-confine \n" + "\nexecutable name was not provided"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__unknown_option(void) +{ + // Check that unrecognized option switch is reported as error. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "--frozbonicator", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "Usage: snap-confine \n" + "\nunrecognized command line option: --frozbonicator"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__forwards_error(void) +{ + // Check that sc_nonfatal_parse_args() forwards errors. + if (g_test_subprocess()) { + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "--frozbonicator", NULL); + + // Call sc_nonfatal_parse_args() without an error handle + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + args = sc_nonfatal_parse_args(&argc, &argv, NULL); + (void)args; + + 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 + ("Usage: snap-confine \n" + "\nunrecognized command line option: --frozbonicator\n"); +} + +static void test_sc_nonfatal_parse_args__base_snap(void) +{ + // Check that --base specifies the name of the base snap. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--base", "base-snap", + "snap.SNAP_NAME.APP_NAME", "/usr/lib/snapd/snap-exec", + NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check the --base switch + g_assert_cmpstr(sc_args_base_snap(args), ==, "base-snap"); + // Check other arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); +} + +static void test_sc_nonfatal_parse_args__base_snap__missing_arg(void) +{ + // Check that --base specifies the name of the base snap. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--base", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "Usage: snap-confine \n" + "\nthe --base option requires an argument"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__base_snap__twice(void) +{ + // Check that --base specifies the name of the base snap. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", + "--base", "base1", "--base", "base2", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "Usage: snap-confine \n" + "\nthe --base option can be used only once"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/args/test_argc_argv", test_test_argc_argv); + g_test_add_func("/args/sc_cleanup_args", test_sc_cleanup_args); + g_test_add_func("/args/sc_nonfatal_parse_args/typical", + test_sc_nonfatal_parse_args__typical); + g_test_add_func("/args/sc_nonfatal_parse_args/typical_classic", + test_sc_nonfatal_parse_args__typical_classic); + g_test_add_func("/args/sc_nonfatal_parse_args/ubuntu_core_launcher", + test_sc_nonfatal_parse_args__ubuntu_core_launcher); + g_test_add_func("/args/sc_nonfatal_parse_args/version", + test_sc_nonfatal_parse_args__version); + g_test_add_func("/args/sc_nonfatal_parse_args/nothing_to_parse", + test_sc_nonfatal_parse_args__nothing_to_parse); + g_test_add_func("/args/sc_nonfatal_parse_args/evil_input", + test_sc_nonfatal_parse_args__evil_input); + g_test_add_func("/args/sc_nonfatal_parse_args/no_security_tag", + test_sc_nonfatal_parse_args__no_security_tag); + g_test_add_func("/args/sc_nonfatal_parse_args/no_executable", + test_sc_nonfatal_parse_args__no_executable); + g_test_add_func("/args/sc_nonfatal_parse_args/unknown_option", + test_sc_nonfatal_parse_args__unknown_option); + g_test_add_func("/args/sc_nonfatal_parse_args/forwards_error", + test_sc_nonfatal_parse_args__forwards_error); + g_test_add_func("/args/sc_nonfatal_parse_args/base_snap", + test_sc_nonfatal_parse_args__base_snap); + g_test_add_func("/args/sc_nonfatal_parse_args/base_snap/missing-arg", + test_sc_nonfatal_parse_args__base_snap__missing_arg); + g_test_add_func("/args/sc_nonfatal_parse_args/base_snap/twice", + test_sc_nonfatal_parse_args__base_snap__twice); +} diff --git a/cmd/snap-confine/snap-confine-args.c b/cmd/snap-confine/snap-confine-args.c new file mode 100644 index 00000000..7cfe7b67 --- /dev/null +++ b/cmd/snap-confine/snap-confine-args.c @@ -0,0 +1,255 @@ +/* + * 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-confine-args.h" + +#include + +#include "../libsnap-confine-private/utils.h" +#include "../libsnap-confine-private/string-utils.h" + +struct sc_args { + // The security tag that the application is intended to run with + char *security_tag; + // The executable that should be invoked + char *executable; + // Name of the base snap to use. + char *base_snap; + + // Flag indicating that --version was passed on command line. + bool is_version_query; + // Flag indicating that --classic was passed on command line. + bool is_classic_confinement; +}; + +struct sc_args *sc_nonfatal_parse_args(int *argcp, char ***argvp, + struct sc_error **errorp) +{ + struct sc_args *args = NULL; + struct sc_error *err = NULL; + + if (argcp == NULL || argvp == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, 0, + "cannot parse arguments, argcp or argvp is NULL"); + goto out; + } + // Use dereferenced versions of argcp and argvp for convenience. + int argc = *argcp; + char **const argv = *argvp; + + if (argc == 0 || argv == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, 0, + "cannot parse arguments, argc is zero or argv is NULL"); + goto out; + } + // Sanity check, look for NULL argv entries. + for (int i = 0; i < argc; ++i) { + if (argv[i] == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, 0, + "cannot parse arguments, argument at index %d is NULL", + i); + goto out; + } + } + + args = calloc(1, sizeof *args); + if (args == NULL) { + die("cannot allocate memory for command line arguments object"); + } + // Check if we're being called through the ubuntu-core-launcher symlink. + // When this happens we want to skip the first positional argument as it is + // the security tag repeated (legacy). + bool ignore_first_tag = false; + char *basename = strrchr(argv[0], '/'); + if (basename != NULL) { + // NOTE: this is safe because we, at most, may move to the NUL byte + // that compares to an empty string. + basename += 1; + if (strcmp(basename, "ubuntu-core-launcher") == 0) { + ignore_first_tag = true; + } + } + // Parse option switches. + int optind; + for (optind = 1; optind < argc; ++optind) { + // Look at all the options switches that start with the minus sign ('-') + if (argv[optind][0] != '-') { + // On first non-switch argument break the loop. The next loop looks + // just for non-option arguments. This ensures that options and + // positional arguments cannot be mixed. + break; + } + // Handle option switches + if (strcmp(argv[optind], "--version") == 0) { + args->is_version_query = true; + // NOTE: --version short-circuits the parser to finish + goto done; + } else if (strcmp(argv[optind], "--classic") == 0) { + args->is_classic_confinement = true; + } else if (strcmp(argv[optind], "--base") == 0) { + if (optind + 1 >= argc) { + err = + sc_error_init(SC_ARGS_DOMAIN, + SC_ARGS_ERR_USAGE, + "Usage: snap-confine \n" + "\n" + "the --base option requires an argument"); + goto out; + } + if (args->base_snap != NULL) { + err = + sc_error_init(SC_ARGS_DOMAIN, + SC_ARGS_ERR_USAGE, + "Usage: snap-confine \n" + "\n" + "the --base option can be used only once"); + goto out; + + } + args->base_snap = sc_strdup(argv[optind + 1]); + optind += 1; + } else { + // Report unhandled option switches + err = sc_error_init(SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE, + "Usage: snap-confine \n" + "\n" + "unrecognized command line option: %s", + argv[optind]); + goto out; + } + } + + // Parse positional arguments. + // + // NOTE: optind is not reset, we just continue from where we left off in + // the loop above. + for (; optind < argc; ++optind) { + if (args->security_tag == NULL) { + // The first positional argument becomes the security tag. + if (ignore_first_tag) { + // Unless we are called as ubuntu-core-launcher, then we just + // swallow and ignore that security tag altogether. + ignore_first_tag = false; + continue; + } + args->security_tag = sc_strdup(argv[optind]); + } else if (args->executable == NULL) { + // The second positional argument becomes the executable name. + args->executable = sc_strdup(argv[optind]); + // No more positional arguments are required. + // Stop the parsing process. + break; + } + } + + // Verify that all mandatory positional arguments are present. + // Ensure that we have the security tag + if (args->security_tag == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE, + "Usage: snap-confine \n" + "\n" + "application or hook security tag was not provided"); + goto out; + } + // Ensure that we have the executable name + if (args->executable == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE, + "Usage: snap-confine \n" + "\n" "executable name was not provided"); + goto out; + } + + int i; + done: + // "shift" the argument vector left, except for argv[0], to "consume" the + // arguments that were scanned / parsed correctly. + for (i = 1; optind + i < argc; ++i) { + argv[i] = argv[optind + i]; + } + argv[i] = NULL; + + // Write the updated argc back, argv is never modified. + *argcp = argc - optind; + + out: + // Don't return anything in case of an error. + if (err != NULL) { + sc_cleanup_args(&args); + } + // Forward the error and return + sc_error_forward(errorp, err); + return args; +} + +void sc_args_free(struct sc_args *args) +{ + if (args != NULL) { + free(args->security_tag); + args->security_tag = NULL; + free(args->executable); + args->executable = NULL; + free(args->base_snap); + args->base_snap = NULL; + free(args); + } +} + +void sc_cleanup_args(struct sc_args **ptr) +{ + sc_args_free(*ptr); + *ptr = NULL; +} + +bool sc_args_is_version_query(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain version query flag from NULL argument parser"); + } + return args->is_version_query; +} + +bool sc_args_is_classic_confinement(struct sc_args * args) +{ + if (args == NULL) { + die("cannot obtain classic confinement flag from NULL argument parser"); + } + return args->is_classic_confinement; +} + +const char *sc_args_security_tag(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain security tag from NULL argument parser"); + } + return args->security_tag; +} + +const char *sc_args_executable(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain executable from NULL argument parser"); + } + return args->executable; +} + +const char *sc_args_base_snap(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain base snap name from NULL argument parser"); + } + return args->base_snap; +} diff --git a/cmd/snap-confine/snap-confine-args.h b/cmd/snap-confine/snap-confine-args.h new file mode 100644 index 00000000..00a507c6 --- /dev/null +++ b/cmd/snap-confine/snap-confine-args.h @@ -0,0 +1,124 @@ +/* + * 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_SNAP_CONFINE_ARGS_H +#define SC_SNAP_CONFINE_ARGS_H + +#include + +#include "../libsnap-confine-private/error.h" + +/** + * Error domain for errors related to argument parsing. + **/ +#define SC_ARGS_DOMAIN "args" + +enum { + /** + * Error indicating that the command line arguments could not be parsed + * correctly and usage message should be displayed to the user. + **/ + SC_ARGS_ERR_USAGE = 1, +}; + +/** + * Opaque structure describing command-line arguments to snap-confine. + **/ +struct sc_args; + +/** + * Parse command line arguments for snap-confine. + * + * Snap confine understands very specific arguments. + * + * The argument vector can begin with "ubuntu-core-launcher" (with an optional + * path) which implies that the first arctual argument (argv[1]) is a copy of + * argv[2] and can be discarded. + * + * The argument vector is scanned, left to right, to look for switches that + * start with the minus sign ('-'). Recognized options are stored and + * memorized. Unrecognized options return an appropriate error object. + * + * Currently only one option is understood, that is "--version". It is simply + * scanned, memorized and discarded. The presence of this switch can be + * retrieved with sc_args_is_version_query(). + * + * After all the option switches are scanned it is expected to scan two more + * arguments: the security tag and the name of the executable to run. An error + * object is returned when those is missing. + * + * Both argc and argv are modified so the caller can look at the first unparsed + * argument at argc[0]. This is only done if argument parsing is successful. + **/ +__attribute__ ((warn_unused_result)) +struct sc_args *sc_nonfatal_parse_args(int *argcp, char ***argvp, + struct sc_error **errorp); + +/** + * Free the object describing command-line arguments to snap-confine. + **/ +void sc_args_free(struct sc_args *args); + +/** + * Cleanup an error with sc_args_free() + * + * This function is designed to be used with + * SC_CLEANUP(sc_cleanup_args). + **/ +void sc_cleanup_args(struct sc_args **ptr); + +/** + * Check if snap-confine was invoked with the --version switch. + **/ +bool sc_args_is_version_query(struct sc_args *args); + +/** + * Check if snap-confine was invoked with the --classic switch. + **/ +bool sc_args_is_classic_confinement(struct sc_args *args); + +/** + * Get the security tag passed to snap-confine. + * + * The return value may be NULL if snap-confine was invoked with --version. It + * is never NULL otherwise. + * + * The return value must not be freed(). It is bound to the lifetime of + * the argument parser. + **/ +const char *sc_args_security_tag(struct sc_args *args); + +/** + * Get the executable name passed to snap-confine. + * + * The return value may be NULL if snap-confine was invoked with --version. It + * is never NULL otherwise. + * + * The return value must not be freed(). It is bound to the lifetime of + * the argument parser. + **/ +const char *sc_args_executable(struct sc_args *args); + +/** + * Get the name of the base snap to use. + * + * The return value must not be freed(). It is bound to the lifetime of + * the argument parser. + **/ +const char *sc_args_base_snap(struct sc_args *args); + +#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..6ba07753 --- /dev/null +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -0,0 +1,519 @@ +# Author: Jamie Strandboge +#include + +@LIBEXECDIR@/snap-confine (attach_disconnected) { + # Include any additional files that snapd chose to generate. + # - for $HOME on NFS + # - for $HOME on encrypted media + # + # Those are discussed on https://forum.snapcraft.io/t/snapd-vs-upstream-kernel-vs-apparmor + # and https://forum.snapcraft.io/t/snaps-and-nfs-home/ + #include "/var/lib/snapd/apparmor/snap-confine" + + # We run privileged, so be fanatical about what we include and don't use + # any abstractions + /etc/ld.so.cache r, + /etc/ld.so.preload r, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}ld-*.so mrix, + # libc, you are funny + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libc{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpthread{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libreadline{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}librt{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libgcc_s.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libncursesw{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libresolv{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libselinux.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpcre.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libmount.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libblkid.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libuuid.so* mr, + # normal libs in order + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libapparmor.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libcgmanager.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libdl{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnih.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnih-dbus.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libdbus-1.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libudev.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libseccomp.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libcap.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, + /dev/tty rw, + + # cgroup: devices + capability sys_admin, + capability dac_read_search, + 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, + + # cgroup: freezer + # Allow creating per-snap cgroup freezers and adding snap command (task) + # invocations to the freezer. This allows for reliably enumerating all + # running tasks for the snap. In addition, allow enumerating processes in + # the cgroup to determine if it is occupied. + /sys/fs/cgroup/freezer/ r, + /sys/fs/cgroup/freezer/snap.*/ w, + /sys/fs/cgroup/freezer/snap.*/tasks w, + /sys/fs/cgroup/freezer/snap.*/cgroup.procs r, + + # querying udev + /etc/udev/udev.conf r, + /sys/**/uevent r, + /usr/lib/snapd/snap-device-helper ixr, # drop + /{,usr/}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, + # Reading available filesystems + @{PROC}/filesystems 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/bpf/*.bin r, + + # LP: #1668659 + mount options=(rw rbind) /snap/ -> /snap/, + mount options=(rw rshared) -> /snap/, + + # 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@/*/*/ -> /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) /{,run/}media/ -> /tmp/snap.rootfs_*/{,run/}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) /var/lib/extrausers/ -> /tmp/snap.rootfs_*/var/lib/extrausers/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/lib/extrausers/, + + mount options=(rw rbind) {,/usr}/lib{,32,64,x32}/modules/ -> /tmp/snap.rootfs_*{,/usr}/lib/modules/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*{,/usr}/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/, + + mount options=(rw rbind) /mnt/ -> /tmp/snap.rootfs_*/mnt/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/mnt/, + + # allow making host snap-exec available inside base snaps + mount options=(rw bind) @LIBEXECDIR@/ -> /tmp/snap.rootfs_*/usr/lib/snapd/, + mount options=(rw slave) -> /tmp/snap.rootfs_*/usr/lib/snapd/, + + # allow making re-execed host snap-exec available inside base snaps + mount options=(ro bind) @SNAP_MOUNT_DIR@/core/*/usr/lib/snapd/ -> /tmp/snap.rootfs_*/usr/lib/snapd/, + # allow making snapd snap tools available inside base snaps + mount options=(ro bind) @SNAP_MOUNT_DIR@/snapd/*/usr/lib/snapd/ -> /tmp/snap.rootfs_*/usr/lib/snapd/, + + mount options=(rw bind) /usr/bin/snapctl -> /tmp/snap.rootfs_*/usr/bin/snapctl, + mount options=(rw slave) -> /tmp/snap.rootfs_*/usr/bin/snapctl, + + # /etc/alternatives (classic) + mount options=(rw bind) @SNAP_MOUNT_DIR@/*/*/etc/alternatives/ -> /tmp/snap.rootfs_*/etc/alternatives/, + mount options=(rw bind) @SNAP_MOUNT_DIR@/*/*/etc/ssl/ -> /tmp/snap.rootfs_*/etc/ssl/, + mount options=(rw bind) @SNAP_MOUNT_DIR@/*/*/etc/nsswitch.conf -> /tmp/snap.rootfs_*/etc/nsswitch.conf, + # /etc/alternatives (core) + mount options=(rw bind) /etc/alternatives/ -> /tmp/snap.rootfs_*/etc/alternatives/, + mount options=(rw slave) -> /tmp/snap.rootfs_*/etc/alternatives/, + mount options=(rw slave) -> /tmp/snap.rootfs_*/etc/ssl/, + mount options=(rw slave) -> /tmp/snap.rootfs_*/etc/nsswitch.conf, + # 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 mediation in AppArmor is not complete. See LP: #1791711 + 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 user mount namespace + mount options=(rslave) -> /, + + # Allow reading the os-release file (possibly a symlink to /usr/lib). + /{etc/,usr/lib/}os-release r, + + # Allow creating /var/lib/snapd/hostfs, if missing + /var/lib/snapd/hostfs/ rw, + + # set up snap-specific private /tmp dir + capability chown, + /tmp/ rw, + /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. + + # nvidia handling, glob needs /usr/** and the launcher must be + # able to bind mount the nvidia dir + /sys/module/nvidia/version r, + /sys/**/drivers/nvidia{,_*}/* r, + /sys/**/nvidia*/uevent r, + /sys/module/nvidia{,_*}/* r, + /dev/nvidia[0-9]* r, + /dev/nvidiactl r, + /dev/nvidia-uvm r, + /usr/** r, + mount options=(rw bind) /usr/lib{,32}/nvidia-*/ -> /{tmp/snap.rootfs_*/,}var/lib/snapd/lib/gl{,32}/, + mount options=(rw bind) /usr/lib{,32}/nvidia-*/ -> /{tmp/snap.rootfs_*/,}var/lib/snapd/lib/gl{,32}/, + /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/{,*} w, + mount fstype=tmpfs options=(rw nodev noexec) none -> /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/, + mount options=(remount ro bind) -> /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/, + + # Vulkan support + /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/{,*} w, + mount fstype=tmpfs options=(rw nodev noexec) none -> /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/, + mount options=(remount ro bind) -> /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/, + + # GLVND EGL vendor + /tmp/snap.rootfs_*/var/lib/snapd/lib/glvnd/{,*} w, + mount fstype=tmpfs options=(rw nodev noexec) none -> /tmp/snap.rootfs_*/var/lib/snapd/lib/glvnd/, + mount options=(remount ro bind) -> /tmp/snap.rootfs_*/var/lib/snapd/lib/glvnd/, + + # create gl dirs as needed + /tmp/snap.rootfs_*/ r, + /tmp/snap.rootfs_*/var/ r, + /tmp/snap.rootfs_*/var/lib/ r, + /tmp/snap.rootfs_*/var/lib/snapd/ r, + /tmp/snap.rootfs_*/var/lib/snapd/lib/ r, + /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/ r, + /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/** rw, + /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/ r, + /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/** rw, + /tmp/snap.rootfs_*/var/lib/snapd/lib/glvnd/ r, + /tmp/snap.rootfs_*/var/lib/snapd/lib/glvnd/** rw, + + # 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, + + # Special case for *classic* snaps that are used by users with existing dirs + # in /var/lib/. Like jenkins, postgresql, mysql, puppet, ... + # (see https://forum.snapcraft.io/t/9717) + # TODO: this can be removed once we support home-dirs outside of /home + # better + /var/ r, + /var/lib/ r, + # These should both have 'owner' match but due to LP: #1466234, we can't + # yet + /var/lib/*/ r, + /var/lib/*/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, + + # Allow snap-confine to read snap contexts + /var/lib/snapd/context/snap.* r, + + # Allow snap-confine to unmount stale mount namespaces. + umount /run/snapd/ns/*.mnt, + /run/snapd/ns/snap.*.fstab w, + # Required to correctly unmount bound mount namespace. + # See LP: #1735459 for details. + umount /, + + # support for locking + /run/snapd/lock/ rw, + /run/snapd/lock/*.lock rwk, + + # support for the mount namespace sharing + capability sys_ptrace, + # allow snap-confine to read /proc/1/ns/mnt + ptrace read peer=unconfined, + # https://forum.snapcraft.io/t/custom-kernel-error-on-readlinkat-in-mount-namespace/6097/21 + ptrace trace peer=unconfined, + + 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=(int, alrm, exists) peer=@LIBEXECDIR@/snap-confine, + signal (receive) set=(exists) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + + # workaround for linux 4.13/upstream, see + # https://forum.snapcraft.io/t/snapd-2-27-6-2-in-debian-sid-blocked-on-apparmor-in-kernel-4-13-0-1/2813/3 + ptrace (trace, tracedby) peer=@LIBEXECDIR@/snap-confine, + + # Allow reading snap cookies. + /var/lib/snapd/cookie/snap.* r, + + # For aa_change_hat() to go into ^mount-namespace-capture-helper + @{PROC}/[0-9]*/attr/current w, + + # As a special exception allow snap-confine to write to anything in /var/lib. + # This code should be changed to allow delegation so that snap-confine can + # inherit any file descriptor and pass it to the invoked application but + # this is not possible in apparmor yet. + # See https://bugs.launchpad.net/snapd/+bug/1815869 + /var/lib/** rw, + + ^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, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}ld-*.so mrix, + # libc, you are funny + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libc{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpthread{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libreadline{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}librt{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libgcc_s.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libncursesw{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libresolv{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libselinux.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpcre.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libmount.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libblkid.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libuuid.so* mr, + # normal libs in order + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libapparmor.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libcgmanager.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libdl{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnih.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnih-dbus.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libdbus-1.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libudev.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libseccomp.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libcap.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, + + # Allow switching to snap-update-ns with a per-snap profile. + change_profile -> snap-update-ns.*, + + # Allow executing snap-update-ns when... + + # ...snap-confine is, conceptually, re-executing and uses snap-update-ns + # from the distribution package. This is also the location used when using + # the core/base snap on all-snap systems. The variants here represent + # various locations of libexecdir across distributions. + /usr/lib{,exec,64}/snapd/snap-update-ns r, + + # ...snap-confine is not, conceptually, re-executing and uses + # snap-update-ns from the distribution package but we are already inside + # the constructed mount namespace so we must traverse "hostfs". The + # variants here represent various locations of libexecdir across + # distributions. + /var/lib/snapd/hostfs/usr/lib{,exec,64}/snapd/snap-update-ns r, + + # ..snap-confine is, conceptually, re-executing and uses snap-update-ns + # from the core snap. Note that the location of the core snap varies from + # distribution to distribution. The variants here represent different + # locations of snap mount directory across distributions. + /{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-update-ns r, + + # ...snap-confine is, conceptually, re-executing and uses snap-update-ns + # from the core snap but we are already inside the constructed mount + # namespace. Here the apparmor kernel module re-constructs the path to + # snap-update-ns using the "hostfs" mount entry rather than the more + # "natural" /snap mount entry but we have no control over that. This is + # reported as (LP: #1716339). The variants here represent different + # locations of snap mount directory across distributions. + /var/lib/snapd/hostfs/{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-update-ns r, + + # Allow executing snap-discard-ns, just like the set for snap-update-ns + # above but with the key difference that snap-discard-ns does not + # have a dedicated profile so we need to inherit snap-confine's profile. + + /usr/lib{,exec,64}/snapd/snap-discard-ns rix, + /var/lib/snapd/hostfs/usr/lib{,exec,64}/snapd/snap-discard-ns rix, + /{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-discard-ns rix, + /var/lib/snapd/hostfs/{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-discard-ns rix, + + # Allow mounting /var/lib/jenkinks from the host into the snap. + mount options=(rw rbind) /var/lib/jenkins/ -> /tmp/snap.rootfs_*/var/lib/jenkins/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/lib/jenkins/, +} diff --git a/cmd/snap-confine/snap-confine.c b/cmd/snap-confine/snap-confine.c new file mode 100644 index 00000000..649dcbaa --- /dev/null +++ b/cmd/snap-confine/snap-confine.c @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2015-2018 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 +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/apparmor-support.h" +#include "../libsnap-confine-private/cgroup-freezer-support.h" +#include "../libsnap-confine-private/classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/feature.h" +#include "../libsnap-confine-private/locking.h" +#include "../libsnap-confine-private/secure-getenv.h" +#include "../libsnap-confine-private/snap.h" +#include "../libsnap-confine-private/utils.h" +#include "mount-support.h" +#include "ns-support.h" +#include "udev-support.h" +#include "user-support.h" +#include "cookie-support.h" +#include "snap-confine-args.h" +#ifdef HAVE_SECCOMP +#include "seccomp-support.h" +#endif // ifdef HAVE_SECCOMP + +// sc_maybe_fixup_permissions fixes incorrect permissions +// inside the mount namespace for /var/lib. Before 1ccce4 +// this directory was created with permissions 1777. +static void sc_maybe_fixup_permissions(void) +{ + struct stat buf; + if (stat("/var/lib", &buf) != 0) { + die("cannot stat /var/lib"); + } + if ((buf.st_mode & 0777) == 0777) { + if (chmod("/var/lib", 0755) != 0) { + die("cannot chmod /var/lib"); + } + if (chown("/var/lib", 0, 0) != 0) { + die("cannot chown /var/lib"); + } + } +} + +// sc_maybe_fixup_udev will remove incorrectly created udev tags +// that cause libudev on 16.04 to fail with "udev_enumerate_scan failed". +// See also: +// https://forum.snapcraft.io/t/weird-udev-enumerate-error/2360/17 +static void sc_maybe_fixup_udev(void) +{ + glob_t glob_res SC_CLEANUP(globfree) = { + .gl_pathv = NULL,.gl_pathc = 0,.gl_offs = 0,}; + const char *glob_pattern = "/run/udev/tags/snap_*/*nvidia*"; + int err = glob(glob_pattern, 0, NULL, &glob_res); + if (err == GLOB_NOMATCH) { + return; + } + if (err != 0) { + die("cannot search using glob pattern %s: %d", + glob_pattern, err); + } + // kill bogus udev tags for nvidia. They confuse udev, this + // undoes the damage from github.com/snapcore/snapd/pull/3671. + // + // The udev tagging of nvidia got reverted in: + // https://github.com/snapcore/snapd/pull/4022 + // but leftover files need to get removed or apps won't start + for (size_t i = 0; i < glob_res.gl_pathc; ++i) { + unlink(glob_res.gl_pathv[i]); + } +} + +int main(int argc, char **argv) +{ + // Use our super-defensive parser to figure out what we've been asked to do. + struct sc_error *err = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + args = sc_nonfatal_parse_args(&argc, &argv, &err); + sc_die_on_error(err); + + // We've been asked to print the version string so let's just do that. + if (sc_args_is_version_query(args)) { + printf("%s %s\n", PACKAGE, PACKAGE_VERSION); + return 0; + } + + const char *snap_instance = getenv("SNAP_INSTANCE_NAME"); + if (snap_instance == NULL) { + die("SNAP_INSTANCE_NAME is not set"); + } + sc_instance_name_validate(snap_instance, NULL); + + // Collect and validate the security tag and a few other things passed on + // command line. + const char *security_tag = sc_args_security_tag(args); + if (!verify_security_tag(security_tag, snap_instance)) { + die("security tag %s not allowed", security_tag); + } + const char *executable = sc_args_executable(args); + const char *base_snap_name = sc_args_base_snap(args) ? : "core"; + bool classic_confinement = sc_args_is_classic_confinement(args); + + sc_snap_name_validate(base_snap_name, NULL); + + debug("security tag: %s", security_tag); + debug("executable: %s", executable); + debug("confinement: %s", + classic_confinement ? "classic" : "non-classic"); + debug("base snap: %s", base_snap_name); + + // Who are we? + uid_t real_uid, effective_uid, saved_uid; + gid_t real_gid, effective_gid, saved_gid; + getresuid(&real_uid, &effective_uid, &saved_uid); + getresgid(&real_gid, &effective_gid, &saved_gid); + debug("ruid: %d, euid: %d, suid: %d", + real_uid, effective_uid, saved_uid); + debug("rgid: %d, egid: %d, sgid: %d", + real_gid, effective_gid, saved_gid); + + // snap-confine runs as both setuid root and setgid root. + // Temporarily drop group privileges here and reraise later + // as needed. + if (effective_gid == 0 && real_gid != 0) { + if (setegid(real_gid) != 0) { + die("cannot set effective group id to %d", real_gid); + } + } +#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 + + char *snap_context SC_CLEANUP(sc_cleanup_string) = NULL; + // Do no get snap context value if running a hook (we don't want to overwrite hook's SNAP_COOKIE) + if (!sc_is_hook_security_tag(security_tag)) { + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + snap_context = sc_cookie_get_from_snapd(snap_instance, &err); + if (err != NULL) { + error("%s\n", sc_error_msg(err)); + } + } + + struct sc_apparmor apparmor; + sc_init_apparmor_support(&apparmor); + if (!apparmor.is_confined && apparmor.mode != SC_AA_NOT_APPLICABLE + && getuid() != 0 && geteuid() == 0) { + // Refuse to run when this process is running unconfined on a system + // that supports AppArmor when the effective uid is root and the real + // id is non-root. This protects against, for example, unprivileged + // users trying to leverage the snap-confine in the core snap to + // escalate privileges. + die("snap-confine has elevated permissions and is not confined" + " but should be. Refusing to continue to avoid" + " permission escalation attacks"); + } + // TODO: check for similar situation and linux capabilities. + 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 { + /* snap-confine uses privately-shared /run/snapd/ns to store + * bind-mounted mount namespaces of each snap. In the case that + * snap-confine is invoked from the mount namespace it typically + * constructs, the said directory does not contain mount entries + * for preserved namespaces as those are only visible in the main, + * outer namespace. + * + * In order to operate in such an environment snap-confine must + * first re-associate its own process with another namespace in + * which the /run/snapd/ns directory is visible. The most obvious + * candidate is pid one, which definitely doesn't run in a + * snap-specific namespace, has a predictable PID and is long + * lived. + */ + sc_reassociate_with_pid1_mount_ns(); + // Do global initialization: + int global_lock_fd = sc_lock_global(); + // ensure that "/" or "/snap" is mounted with the + // "shared" option, see LP:#1668659 + debug("ensuring that snap mount directory is shared"); + sc_ensure_shared_snap_mount(); + debug("unsharing snap namespace directory"); + sc_initialize_mount_ns(); + sc_unlock(global_lock_fd); + + // Find and open snap-update-ns and snap-discard-ns from the same + // path as where we (snap-confine) were called. + int snap_update_ns_fd SC_CLEANUP(sc_cleanup_close) = -1; + snap_update_ns_fd = sc_open_snap_update_ns(); + int snap_discard_ns_fd SC_CLEANUP(sc_cleanup_close) = + -1; + snap_discard_ns_fd = sc_open_snap_discard_ns(); + + // Do per-snap initialization. + int snap_lock_fd = sc_lock_snap(snap_instance); + debug("initializing mount namespace: %s", + snap_instance); + struct sc_mount_ns *group = NULL; + group = sc_open_mount_ns(snap_instance); + + /* Stale mount namespace discarded or no mount namespace to + join. We need to construct a new mount namespace ourselves. + To capture it we will need a helper process so make one. */ + sc_fork_helper(group, &apparmor); + int retval = sc_join_preserved_ns(group, &apparmor, + base_snap_name, + snap_instance, + snap_discard_ns_fd); + if (retval == ESRCH) { + /* Create and populate the mount namespace. This performs all + of the bootstrapping mounts, pivots into the new root + filesystem and applies the per-snap mount profile using + snap-update-ns. */ + debug + ("unsharing the mount namespace (per-snap)"); + if (unshare(CLONE_NEWNS) < 0) { + die("cannot unshare the mount namespace"); + } + sc_populate_mount_ns(&apparmor, + snap_update_ns_fd, + base_snap_name, + snap_instance); + + /* Preserve the mount namespace. */ + sc_preserve_populated_mount_ns(group); + } + + /* Older versions of snap-confine created incorrect 777 permissions + for /var/lib and we need to fixup for systems that had their NS + created with an old version. */ + sc_maybe_fixup_permissions(); + sc_maybe_fixup_udev(); + + // Associate each snap process with a dedicated snap freezer + // control group. This simplifies testing if any processes + // belonging to a given snap are still alive. + // See the documentation of the function for details. + + if (getegid() != 0 && saved_gid == 0) { + // Temporarily raise egid so we can chown the freezer cgroup + // under LXD. + if (setegid(0) != 0) { + die("cannot set effective group id to root"); + } + } + sc_cgroup_freezer_join(snap_instance, getpid()); + if (geteuid() == 0 && real_gid != 0) { + if (setegid(real_gid) != 0) { + die("cannot set effective group id to %d", real_gid); + } + } + + /* User mount profiles do not apply to non-root users. */ + if (real_uid != 0) { + debug + ("joining preserved per-user mount namespace"); + retval = + sc_join_preserved_per_user_ns(group, + snap_instance); + if (retval == ESRCH) { + debug + ("unsharing the mount namespace (per-user)"); + if (unshare(CLONE_NEWNS) < 0) { + die("cannot unshare the mount namespace"); + } + sc_setup_user_mounts(&apparmor, + snap_update_ns_fd, + snap_instance); + /* Preserve the mount per-user namespace. But only if the + * experimental feature is enabled. This way if the feature is + * disabled user mount namespaces will still exist but will be + * entirely ephemeral. In addition the call + * sc_join_preserved_user_ns() will never find a preserved + * mount namespace and will always enter this code branch. */ + if (sc_feature_enabled + (SC_PER_USER_MOUNT_NAMESPACE)) { + sc_preserve_populated_per_user_mount_ns + (group); + } else { + debug + ("NOT preserving per-user mount namespace"); + } + } + } + + sc_unlock(snap_lock_fd); + + sc_close_mount_ns(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/local/sbin:" + "/usr/local/bin:" + "/usr/sbin:" + "/usr/bin:" + "/sbin:" + "/bin:" "/usr/games:" "/usr/local/games", 1); + // Ensure we set the various TMPDIRs to /tmp. + // One of the parts of setting up the mount namespace is to create a private /tmp + // directory (this is done in sc_populate_mount_ns() above). The host environment + // may point to a directory not accessible by snaps so we need to reset it here. + 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]); + } + } + 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 + if (sc_apply_seccomp_profile_for_security_tag(security_tag)) { + /* If the process is not explicitly unconfined then load the global + * profile as well. */ + sc_apply_global_seccomp_profile(); + } +#endif // ifdef HAVE_SECCOMP + if (snap_context != NULL) { + setenv("SNAP_COOKIE", snap_context, 1); + // for compatibility, if facing older snapd. + setenv("SNAP_CONTEXT", snap_context, 1); + } + // 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 executable + argv[0] = (char *)executable; + debug("execv(%s, %s...)", executable, argv[0]); + for (int i = 1; i < argc; ++i) { + debug(" argv[%i] = %s", i, argv[i]); + } + execv(executable, (char *const *)&argv[0]); + perror("execv failed"); + return 1; +} diff --git a/cmd/snap-confine/snap-confine.rst b/cmd/snap-confine/snap-confine.rst new file mode 100644 index 00000000..3141ef86 --- /dev/null +++ b/cmd/snap-confine/snap-confine.rst @@ -0,0 +1,188 @@ +============== + snap-confine +============== + +----------------------------------------------- +internal tool for confining snappy applications +----------------------------------------------- + +:Author: zygmunt.krynicki@canonical.com +:Date: 2017-09-18 +:Copyright: Canonical Ltd. +:Version: 2.28 +:Manual section: 8 +:Manual group: snappy + +SYNOPSIS +======== + + snap-confine [--classic] [--base BASE] SECURITY_TAG COMMAND [...ARGUMENTS] + +DESCRIPTION +=========== + +The `snap-confine` is a program used internally by `snapd` to construct the +execution environment for snap applications. + +OPTIONS +======= + +The `snap-confine` program accepts two options: + + `--classic` requests the so-called _classic_ _confinement_ in which + applications are not confined at all (like in classic systems, hence the + name). This disables the use of a dedicated, per-snap mount namespace. The + `snapd` service generates permissive apparmor and seccomp profiles that + allow everything. + + `--base BASE` directs snap-confine to use the given base snap as the root + filesystem. If omitted it defaults to the `core` snap. This is derived from + snap meta-data by `snapd` when starting the application process. + +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. + +The profile 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/bpf/$SECURITY_TAG.bin` file. This file is +**mandatory** and `snap-confine` will refuse to run without it. This +file contains the seccomp bpf binary program that is loaded into the +kernel by snap-confine. + +The file is generated with the `/usr/lib/snapd/snap-seccomp` compiler from the +`$SECURITY_TAG.src` file that uses 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` uses a helper process, `snap-update-ns`, to apply the mount +namespace profile to freshly constructed mount namespace. That tool looks for +the `/var/lib/snapd/mount/snap.$SNAP_NAME.fstab` file. If present it is read, +parsed and treated like a mostly-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). + +Certain additional features are enabled and conveyed through the use of mount +options prefixed with `x-snapd-`. + +As a security precaution only `bind` mounts are supported at this time. + +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` and `snap-update-ns` use the following files: + +`/var/lib/snapd/mount/snap.*.fstab`: + + Description of the mount profile. + +`/var/lib/snapd/seccomp/bpf/*.src`: + + Input for the /usr/lib/snapd/snap-seccomp profile compiler. + +`/var/lib/snapd/seccomp/bpf/*.bin`: + + Compiled seccomp bpf profile programs. + +`/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/snap-device-helper b/cmd/snap-confine/snap-device-helper new file mode 100755 index 00000000..b006443b --- /dev/null +++ b/cmd/snap-confine/snap-device-helper @@ -0,0 +1,73 @@ +#!/bin/sh +# udev callout to allow a snap to access a device node +set -e +# debugging +#exec >>/tmp/snap-device-helper.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; } + +NOSNAP="${APPNAME#snap_}" +[ "$NOSNAP" != "$APPNAME" ] || { echo "malformed appname $APPNAME" >&2; exit 1; } + +# FIXME: this will break for instances that are called "hook" :( +# Handle hooks first, the the nosnap part looks like this: +# - "$snap_hook_$hookname" +# - "$snap_$instance_hook_$hookname +# we need to make sure we change this to: +# - "$snap_hook.$hookname" +# - "$snap_$instance_hook.$hookname" +if [ -z "${NOSNAP##*_hook_hook_*}" ]; then + # $instance is 'hook'; $snap_hook_hook.$hookname -> $snap_hook_hook.$hookname + NOSNAP="${NOSNAP%_hook_*}_hook.${NOSNAP#*_hook_hook_}" +elif [ -z "${NOSNAP##*_hook_*}" ]; then + # $snap_$instance_hook_$hookname -> $snap_$instance_hook.$hookname + NOSNAP="${NOSNAP%_hook_*}_hook.${NOSNAP#*_hook_}" +fi + +# Now deal with app/instance untangling +if [ "${NOSNAP#*_*_}" = "${NOSNAP}" ]; then + # snap__ -> snap.. + SNAPAPP="snap.${NOSNAP%_*}.${NOSNAP#*_}" +else + # snap___ -> snap._. + SNAPAPP="snap.${NOSNAP%_*}.${NOSNAP#*_*_}" +fi + +DEVICES_CGROUP=${DEVICES_CGROUP:="/sys/fs/cgroup/devices"} +app_dev_cgroup="$DEVICES_CGROUP/$SNAPAPP" + +# The cgroup is only present after snap start so ignore any cgroup changes +# (eg, 'add' on boot, hotplug, hotunplug) when the cgroup doesn't exist +# yet. LP: #1762182. +if [ ! -e "$app_dev_cgroup" ]; then + exit 0 +fi + +# 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/snap-device-helper-test.c b/cmd/snap-confine/snap-device-helper-test.c new file mode 100644 index 00000000..428d9fa8 --- /dev/null +++ b/cmd/snap-confine/snap-device-helper-test.c @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2018 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 "../libsnap-confine-private/test-utils.h" + +#include +#include +#include +#include +#include +#include +#include + +// TODO: build at runtime +static const char *sdh_path_default = "snap-confine/snap-device-helper"; + +// A variant of unsetenv that is compatible with GDestroyNotify +static void my_unsetenv(const char *k) +{ + g_unsetenv(k); +} + +// A variant of rm_rf_tmp that calls g_free() on its parameter +static void rm_rf_tmp_free(gchar * dirpath) +{ + rm_rf_tmp(dirpath); + g_free(dirpath); +} + +static gchar *find_sdh_path(void) +{ + const char *sdh_from_env = g_getenv("SNAP_DEVICE_HELPER"); + if (sdh_from_env != NULL) { + return g_strdup(sdh_from_env); + } + return g_strdup(sdh_path_default); +} + +static int run_sdh(gchar * action, + gchar * appname, gchar * devpath, gchar * majmin) +{ + gchar *mod_appname = g_strdup(appname); + gchar *sdh_path = find_sdh_path(); + + // appnames have the following format: + // - snap.. + // - snap._. + // snap-device-helper expects: + // - snap__ + // - snap___ + for (size_t i = 0; i < strlen(mod_appname); i++) { + if (mod_appname[i] == '.') { + mod_appname[i] = '_'; + } + } + g_debug("appname modified from %s to %s", appname, mod_appname); + + GError *err = NULL; + + GPtrArray *argv = g_ptr_array_new(); + g_ptr_array_add(argv, sdh_path); + g_ptr_array_add(argv, action); + g_ptr_array_add(argv, mod_appname); + g_ptr_array_add(argv, devpath); + g_ptr_array_add(argv, majmin); + g_ptr_array_add(argv, NULL); + + int status = 0; + + gboolean ret = g_spawn_sync(NULL, (gchar **) argv->pdata, NULL, 0, + NULL, NULL, NULL, NULL, &status, &err); + g_free(mod_appname); + g_free(sdh_path); + g_ptr_array_unref(argv); + + if (!ret) { + g_debug("failed with: %s", err->message); + g_error_free(err); + return -2; + } + + return WEXITSTATUS(status); +} + +struct sdh_test_data { + char *action; + // snap.foo.bar + char *app; + // snap_foo_bar + char *mangled_appname; + char *file_with_data; + char *file_with_no_data; +}; + +static void test_sdh_action(gconstpointer test_data) +{ + struct sdh_test_data *td = (struct sdh_test_data *)test_data; + + gchar *mock_dir = g_dir_make_tmp(NULL, NULL); + g_assert_nonnull(mock_dir); + g_test_queue_destroy((GDestroyNotify) rm_rf_tmp_free, mock_dir); + + gchar *app_dir = g_build_filename(mock_dir, td->app, NULL); + gchar *with_data = g_build_filename(mock_dir, + td->app, + td->file_with_data, + NULL); + gchar *without_data = g_build_filename(mock_dir, + td->app, + td->file_with_no_data, + NULL); + gchar *data = NULL; + + g_assert(g_mkdir_with_parents(app_dir, 0755) == 0); + g_free(app_dir); + + g_debug("mock cgroup dir: %s", mock_dir); + + g_setenv("DEVICES_CGROUP", mock_dir, TRUE); + + g_test_queue_destroy((GDestroyNotify) my_unsetenv, "DEVICES_CGROUP"); + + int ret = + run_sdh(td->action, td->app, "/devices/foo/block/sda/sda4", "8:4"); + g_assert_cmpint(ret, ==, 0); + g_assert_true(g_file_get_contents(with_data, &data, NULL, NULL)); + g_assert_cmpstr(data, ==, "b 8:4 rwm\n"); + g_clear_pointer(&data, g_free); + g_assert(g_remove(with_data) == 0); + + g_assert_false(g_file_get_contents(without_data, &data, NULL, NULL)); + + ret = run_sdh(td->action, td->mangled_appname, "/devices/foo/tty/ttyS0", "4:64"); + g_assert_cmpint(ret, ==, 0); + g_assert_true(g_file_get_contents(with_data, &data, NULL, NULL)); + g_assert_cmpstr(data, ==, "c 4:64 rwm\n"); + g_clear_pointer(&data, g_free); + g_assert(g_remove(with_data) == 0); + + g_assert_false(g_file_get_contents(without_data, &data, NULL, NULL)); + + g_free(with_data); + g_free(without_data); +} + +static void test_sdh_err(void) +{ + // missing appname + int ret = run_sdh("add", "", "/devices/foo/block/sda/sda4", "8:4"); + g_assert_cmpint(ret, ==, 1); + // malformed appname + ret = run_sdh("add", "foo_bar", "/devices/foo/block/sda/sda4", "8:4"); + g_assert_cmpint(ret, ==, 1); + // missing devpath + ret = run_sdh("add", "snap_foo_bar", "", "8:4"); + g_assert_cmpint(ret, ==, 1); + // missing device major:minor numbers + ret = run_sdh("add", "snap_foo_bar", "/devices/foo/block/sda/sda4", ""); + g_assert_cmpint(ret, ==, 0); + + // mock some stuff so that we can reach the 'action' checks + gchar *mock_dir = g_dir_make_tmp(NULL, NULL); + g_assert_nonnull(mock_dir); + g_test_queue_destroy((GDestroyNotify) rm_rf_tmp_free, mock_dir); + + gchar *app_dir = g_build_filename(mock_dir, "snap.foo.bar", NULL); + g_assert(g_mkdir_with_parents(app_dir, 0755) == 0); + g_free(app_dir); + g_setenv("DEVICES_CGROUP", mock_dir, TRUE); + + g_test_queue_destroy((GDestroyNotify) my_unsetenv, "DEVICES_CGROUP"); + + ret = + run_sdh("badaction", "snap_foo_bar", "/devices/foo/block/sda/sda4", + "8:4"); + g_assert_cmpint(ret, ==, 1); +} + +static struct sdh_test_data add_data = + { "add", "snap.foo.bar", "snap_foo_bar", "devices.allow", "devices.deny" }; +static struct sdh_test_data change_data = + { "change", "snap.foo.bar", "snap_foo_bar", "devices.allow", "devices.deny" }; +static struct sdh_test_data remove_data = + { "remove", "snap.foo.bar", "snap_foo_bar", "devices.deny", "devices.allow" }; +static struct sdh_test_data instance_add_data = + { "add", "snap.foo_bar.baz", "snap_foo_bar_baz", "devices.allow", "devices.deny" }; +static struct sdh_test_data instance_change_data = + { "change", "snap.foo_bar.baz", "snap_foo_bar_baz", "devices.allow", "devices.deny" }; +static struct sdh_test_data instance_remove_data = + { "remove", "snap.foo_bar.baz", "snap_foo_bar_baz", "devices.deny", "devices.allow" }; +static struct sdh_test_data add_hook_data = + { "add", "snap.foo.hook.configure", "snap_foo_hook_configure", "devices.allow", "devices.deny" }; +static struct sdh_test_data instance_add_hook_data = + { "add", "snap.foo_bar.hook.configure", "snap_foo_bar_hook_configure", "devices.allow", "devices.deny" }; +static struct sdh_test_data instance_add_instance_name_is_hook_data = + { "add", "snap.foo_hook.hook.configure", "snap_foo_hook_hook_configure", "devices.allow", "devices.deny" }; + +static void __attribute__ ((constructor)) init(void) +{ + + g_test_add_data_func("/snap-device-helper/add", + &add_data, test_sdh_action); + g_test_add_data_func("/snap-device-helper/change", &change_data, + test_sdh_action); + g_test_add_data_func("/snap-device-helper/remove", &remove_data, + test_sdh_action); + g_test_add_func("/snap-device-helper/err", test_sdh_err); + g_test_add_data_func("/snap-device-helper/parallel/add", + &instance_add_data, test_sdh_action); + g_test_add_data_func("/snap-device-helper/parallel/change", + &instance_change_data, test_sdh_action); + g_test_add_data_func("/snap-device-helper/parallel/remove", + &instance_remove_data, test_sdh_action); + // hooks + g_test_add_data_func("/snap-device-helper/hook/add", + &add_hook_data, test_sdh_action); + g_test_add_data_func("/snap-device-helper/hook/parallel/add", + &instance_add_hook_data, test_sdh_action); + g_test_add_data_func("/snap-device-helper/hook-name-hook/parallel/add", + &instance_add_instance_name_is_hook_data, test_sdh_action); +} 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 0000000000000000000000000000000000000000..34d8b57690b7e8133b73a799e4bd470e7c47d8f6 GIT binary patch literal 427 zcmV;c0aX5&jRaN%NFV_O0JxNLP0VZb%PQy4fPWY1qHz=2-OFRY>?qI^dQplJc^zEW zlyRWPtL6IdhsvnpS3$?APa};lq4O_Ap2#WX{ICdgZ<>h{OB+QR{8T$<)it#RQ;UDv zo1&Xvq_!6MnVPkhDzPZp-u_v+iG@fsOst&m@-QZ{HYLBpm|BMB0RRyJ00FdMQ(|># zY-Au)X=iR_av&&EVs&Y3WFSIyX>4R5L}hSgZe(R{V|gG!a${&|c4Z(-WqBzeJYsce zY-D6DbZ>8Lb1h_Lc4cfpY-w|Jb1q?QX>W9BE@Wk5X<=?IZ*pfoh`0n30RjLb1p-zC zNFV|mF9r(<2nPcK1{DYb2?`4Y76JnS0v-VZ7k~f?2@nT;U_;3Udp>|o1OSx3R@!!G zeO2yv*U_X3=WZ+PDC*k<$x^81WV$O(@N(X!KIC@!eb=xSnKhFGzZ}c=yL0 zrd@Jf1&OKj`DLF)gj<}2xF~|^ufn$emLArzJ30U+BH$|lghxX(opymUmyCa05JsK* VihyKrl%!aG4JdUEL$`+VD?>j~u}}a2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cdadd277936c5971b14b3e4006a9e202c0408c60 GIT binary patch literal 759 zcmV7sEH+TF`zzw9W`6M9jK5_uh5 z*OYOf$E)S~?}y5$;#Wb(sZS$~FQM}e1yhTE z+MA-AU!=Ac`I(xvmMXC*+1~zHxrv2HG)%0V@A5DvvNk2Z!kAiy<^cc^0RRC21N;o< z(laL6Ac08}a>QLeh=(IpVovfd+y^k$RhmwdvlVqrUBzPweH4R?zfCm?2xC?C)*N!J zr#LZI>x}))QYUeTN7*H>_wA$Z!mrR5Ym!s!Pz__GhS~vAuldxk0qCXNQ`7|L(}~1% z?$J&%)ffS5L6|LIX*f>Q&JG+$Cu4q^ly+uF#lxhVnv zU2hs`Jn!Ui>z0s`V=50{4nki!=B-RCbJFh#OZz5>PKLlT!5vD$`J{|^)OJMoliKLL zmu#8!wAyKlBNJN*o=vo1Q(|>#Y-Au)X=iR_av&&EVs&Y3WFSIyX>4R5L}hSgZe(R{ zV|gG!a${&|c4Z(-WqBzeJYsceY-D6DbZ>8Lb1h_Lc4cfpY-w|Jb1q?QX>W9BE@Wk5 zX<=?IZ*pfoh`0n30RjLb1p-zCNFV|mF9r(<2nPcK1{DYb2?`4Y76JnS0v-VZ7k~f? z2@nT;U_;3Udp>|o1OSx3R@!!GeO2yv*U_X3=WZ+PDC*k<$x^81WV$O(@N(X!KIC@! zeb=xSnKhFGzZ}c=yL0rd@Jf1&OKj`DLF)gj<}2xF~|^ufn$emLArzJ30U+ pBH$|lghxX(opymUmyCa05JsK*ihyKrl%!aG4JdUEL$`+VD?^GfP~ZRn literal 0 HcmV?d00001 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/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-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..00727918 --- /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 --ctime | 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..4eb3a66c --- /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 --ctime | 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/regression/lp-1599608/task.yaml b/cmd/snap-confine/spread-tests/regression/lp-1599608/task.yaml new file mode 100644 index 00000000..34579521 --- /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/snap-device-helper 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/snap-device-helper 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.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 snap-device-helper" + 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/snap-device-helper + echo 'echo TESTVAR=$TESTVAR >> /run/udev/spread-test.out' >> ./squashfs-root/lib/udev/snap-device-helper + mksquashfs ./squashfs-root $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1 | sed 's/.orig//') -comp xz -no-fragments + 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.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.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.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..803ae74e --- /dev/null +++ b/cmd/snap-confine/spread-tests/release.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# 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..a547b0d6 --- /dev/null +++ b/cmd/snap-confine/spread-tests/spread-prepare.sh @@ -0,0 +1,179 @@ +#!/bin/bash +# 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 +# shellcheck disable=SC1091 +{ + 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 + # shellcheck disable=SC1090 + { + . "$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 + gdebi --quiet --apt-line ./distro-packaging/debian/control | xargs -r apt-get install --quiet -y + + # 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/udev-support.c b/cmd/snap-confine/udev-support.c new file mode 100644 index 00000000..aea1e35e --- /dev/null +++ b/cmd/snap-confine/udev-support.c @@ -0,0 +1,302 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/snap.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" +#include "udev-support.h" + +static void +_run_snappy_app_dev_add_majmin(struct snappy_udev *udev_s, + const char *path, unsigned major, unsigned minor) +{ + int status = 0; + pid_t pid = fork(); + if (pid < 0) { + die("cannot fork support process for device cgroup assignment"); + } + if (pid == 0) { + uid_t real_uid, effective_uid, saved_uid; + if (getresuid(&real_uid, &effective_uid, &saved_uid) != 0) + die("cannot get real, effective and saved 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("cannot set user ID to zero"); + char buf[64] = { 0 }; + // pass snappy-add-dev an empty environment so the + // user-controlled environment can't be used to subvert + // snappy-add-dev + char *env[] = { NULL }; + if (minor == UINT_MAX) { + sc_must_snprintf(buf, sizeof(buf), "%u:*", major); + } else { + sc_must_snprintf(buf, sizeof(buf), "%u:%u", major, + minor); + } + debug("running snap-device-helper add %s %s %s", + udev_s->tagname, path, buf); + // This code runs inside the core snap. We have two paths + // for the udev helper. + // + // First try new "snap-device-helper" path first but + // when running against an older core snap fallback to + // the old name. + if (access("/usr/lib/snapd/snap-device-helper", X_OK) == 0) + execle("/usr/lib/snapd/snap-device-helper", + "/usr/lib/snapd/snap-device-helper", "add", + udev_s->tagname, path, buf, NULL, env); + else + 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)); +} + +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("cannot find device from syspath %s", path); + dev_t devnum = udev_device_get_devnum(d); + udev_device_unref(d); + + _run_snappy_app_dev_add_majmin(udev_s, path, major(devnum), minor(devnum)); +} + +/* + * 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; + + udev_s->tagname[0] = '\0'; + udev_s->tagname_len = 0; + // TAG+="snap_" (udev doesn't like '.' in the tag name) + udev_s->tagname_len = sc_must_snprintf(udev_s->tagname, MAX_BUF, + "%s", security_tag); + for (size_t 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, + }; + + 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] = { 0 }; + + sc_must_snprintf(cgroup_dir, sizeof(cgroup_dir), + "/sys/fs/cgroup/devices/%s/", security_tag); + + if (mkdir(cgroup_dir, 0755) < 0 && errno != EEXIST) + die("cannot create cgroup hierarchy %s", cgroup_dir); + + // move ourselves into it + char cgroup_file[PATH_MAX] = { 0 }; + sc_must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir, + "tasks"); + + char buf[128] = { 0 }; + sc_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. + sc_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 glob for current and future PTY slaves. We unconditionally add + // them since we use a devpts newinstance. Unix98 PTY slaves major + // are 136-143. + // https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/devices.txt + for (unsigned pty_major = 136; pty_major <= 143; pty_major++) { + // '/dev/pts/slaves' is only used for debugging and by + // /usr/lib/snapd/snap-device-helper to determine if it is a block + // device, so just use something to indicate what the + // addition is for + _run_snappy_app_dev_add_majmin(udev_s, "/dev/pts/slaves", + pty_major, UINT_MAX); + } + + // nvidia modules are proprietary and therefore aren't in sysfs and + // can't be udev tagged. For now, just add existing nvidia devices to + // the cgroup unconditionally (AppArmor will still mediate the access). + // We'll want to rethink this if snapd needs to mediate access to other + // proprietary devices. + // + // Device major and minor numbers are described in (though nvidia-uvm + // currently isn't listed): + // https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/devices.txt + char nv_path[15] = { 0 }; // /dev/nvidiaXXX + const char *nvctl_path = "/dev/nvidiactl"; + const char *nvuvm_path = "/dev/nvidia-uvm"; + const char *nvidia_modeset_path = "/dev/nvidia-modeset"; + + struct stat sbuf; + + // /dev/nvidia0 through /dev/nvidia254 + for (unsigned nv_minor = 0; nv_minor < 255; nv_minor++) { + sc_must_snprintf(nv_path, sizeof(nv_path), "/dev/nvidia%u", + nv_minor); + + // Stop trying to find devices after one is not found. In this + // manner, we'll add /dev/nvidia0 and /dev/nvidia1 but stop + // trying to find nvidia3 - nvidia254 if nvidia2 is not found. + if (stat(nv_path, &sbuf) != 0) { + break; + } + _run_snappy_app_dev_add_majmin(udev_s, nv_path, + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); + } + + // /dev/nvidiactl + if (stat(nvctl_path, &sbuf) == 0) { + _run_snappy_app_dev_add_majmin(udev_s, nvctl_path, + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); + } + // /dev/nvidia-uvm + if (stat(nvuvm_path, &sbuf) == 0) { + _run_snappy_app_dev_add_majmin(udev_s, nvuvm_path, + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); + } + // /dev/nvidia-modeset + if (stat(nvidia_modeset_path, &sbuf) == 0) { + _run_snappy_app_dev_add_majmin(udev_s, nvidia_modeset_path, + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); + } + // /dev/uhid isn't represented in sysfs, so add it to the device cgroup + // if it exists and let AppArmor handle the mediation + if (stat("/dev/uhid", &sbuf) == 0) { + _run_snappy_app_dev_add_majmin(udev_s, "/dev/uhid", + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); + } + // 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/user-support.c b/cmd/snap-confine/user-support.c new file mode 100644 index 00000000..fee292ed --- /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 "../libsnap-confine-private/utils.h" + +void setup_user_data(void) +{ + 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(void) +{ + 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..859d04dd --- /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); +void setup_user_xdg_runtime_dir(void); +void mkpath(const char *const path); + +#endif diff --git a/cmd/snap-discard-ns/snap-discard-ns.c b/cmd/snap-discard-ns/snap-discard-ns.c new file mode 100644 index 00000000..b8167853 --- /dev/null +++ b/cmd/snap-discard-ns/snap-discard-ns.c @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2015-2018 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 . + * + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/error.h" +#include "../libsnap-confine-private/locking.h" +#include "../libsnap-confine-private/snap.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +#ifndef NSFS_MAGIC +#define NSFS_MAGIC 0x6e736673 +#endif + +int main(int argc, char** argv) { + if (argc != 2 && argc != 3) { + printf("Usage: snap-discard-ns [--from-snap-confine] \n"); + return 0; + } + const char* snap_instance_name; + bool from_snap_confine; + + if (argc == 3) { + if (!sc_streq(argv[1], "--from-snap-confine")) { + die("unexpected argument %s", argv[1]); + } + from_snap_confine = true; + snap_instance_name = argv[2]; + } else { + from_snap_confine = false; + snap_instance_name = argv[1]; + } + + struct sc_error* err = NULL; + sc_instance_name_validate(snap_instance_name, &err); + sc_die_on_error(err); + + int snap_lock_fd = -1; + if (from_snap_confine) { + sc_verify_snap_lock(snap_instance_name); + } else { + /* Grab the lock holding the snap instance. This prevents races from + * concurrently executing snap-confine. The lock is explicitly released + * during normal operation but it is not preserved across the life-cycle of + * the process anyway so no attempt is made to unlock it ahead of any call + * to die() */ + snap_lock_fd = sc_lock_snap(snap_instance_name); + } + debug("discarding mount namespaces of snap %s", snap_instance_name); + + const char* ns_dir_path = "/run/snapd/ns"; + int ns_dir_fd = open(ns_dir_path, O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); + if (ns_dir_fd < 0) { + /* The directory may legitimately not exist if no snap has started to + * prepare it. This is not an error condition. */ + if (errno == ENOENT) { + return 0; + } + die("cannot open path %s", ns_dir_path); + } + + /* Move to the namespace directory. This is used so that we don't need to + * traverse the path over and over in our upcoming umount2(2) calls. */ + if (fchdir(ns_dir_fd) < 0) { + die("cannot move to directory %s", ns_dir_path); + } + + /* Create shell patterns that describe the things we are interested in: + * + * Preserved mount namespaces to unmount and unlink: + * - "$SNAP_INSTANCE_NAME.mnt" + * - "$SNAP_INSTANCE_NAME.[0-9]+.mnt" + * + * Applied mount profiles to unlink: + * - "snap.$SNAP_INSTANCE_NAME.fstab" + * - "snap.$SNAP_INSTANCE_NAME.[0-9]+.fstab" + * + * Use PATH_MAX as the size of each buffer since those can store any file + * name. */ + char sys_fstab_pattern[PATH_MAX]; + char usr_fstab_pattern[PATH_MAX]; + char sys_mnt_pattern[PATH_MAX]; + char usr_mnt_pattern[PATH_MAX]; + sc_must_snprintf(sys_fstab_pattern, sizeof sys_fstab_pattern, "snap\\.%s\\.fstab", snap_instance_name); + sc_must_snprintf(usr_fstab_pattern, sizeof usr_fstab_pattern, "snap\\.%s\\.*\\.fstab", snap_instance_name); + sc_must_snprintf(sys_mnt_pattern, sizeof sys_mnt_pattern, "%s\\.mnt", snap_instance_name); + sc_must_snprintf(usr_mnt_pattern, sizeof usr_mnt_pattern, "%s\\.*\\.mnt", snap_instance_name); + + DIR* ns_dir = fdopendir(ns_dir_fd); + if (ns_dir == NULL) { + die("cannot fdopendir"); + } + /* ns_dir_fd is now owned by ns_dir and will not be closed. */ + + while (true) { + /* Reset errno ahead of any call to readdir to differentiate errors + * from legitimate end of directory. */ + errno = 0; + struct dirent* dent = readdir(ns_dir); + if (dent == NULL) { + if (errno != 0) { + die("cannot read next directory entry"); + } + /* We've seen the whole directory. */ + break; + } + + /* We use dnet->d_name a lot so let's shorten it. */ + const char* dname = dent->d_name; + + /* Check the four patterns that we have against the name and set the + * two should flags to decide further actions. Note that we always + * unlink matching files so that is not reflected in the structure. */ + bool should_unmount = false; + bool should_unlink = false; + struct variant { + const char* pattern; + bool unmount; + }; + struct variant variants[4] = { + {.pattern = sys_mnt_pattern, .unmount = true}, + {.pattern = usr_mnt_pattern, .unmount = true}, + {.pattern = sys_fstab_pattern}, + {.pattern = usr_fstab_pattern}, + }; + for (size_t i = 0; i < sizeof variants / sizeof *variants; ++i) { + struct variant* v = &variants[i]; + debug("checking if %s matches %s", dname, v->pattern); + int match_result = fnmatch(v->pattern, dname, 0); + if (match_result == FNM_NOMATCH) { + continue; + } else if (match_result == 0) { + should_unmount |= v->unmount; + should_unlink = true; + debug("file %s matches pattern %s", dname, v->pattern); + /* One match is enough. */ + break; + } else if (match_result < 0) { + die("cannot execute match against pattern %s", v->pattern); + } + } + + /* Stat the candidate directory entry to know what we are dealing with. + */ + struct stat file_info; + if (fstatat(ns_dir_fd, dname, &file_info, AT_SYMLINK_NOFOLLOW) < 0) { + die("cannot inspect file %s", dname); + } + + /* We are only interested in regular files. The .mnt files, even if + * bind-mounted, appear as regular files and not as symbolic links due + * to the peculiarities of the Linux kernel. */ + if (!S_ISREG(file_info.st_mode)) { + continue; + } + + if (should_unmount) { + /* If we are asked to unmount the file double check that it is + * really a preserved mount namespace since the error code from + * umount2(2) is inconclusive. */ + int path_fd = openat(ns_dir_fd, dname, O_PATH | O_CLOEXEC | O_NOFOLLOW); + if (path_fd < 0) { + die("cannot open path %s", dname); + } + struct statfs fs_info; + if (fstatfs(path_fd, &fs_info) < 0) { + die("cannot inspect file-system at %s", dname); + } + close(path_fd); + if (fs_info.f_type == NSFS_MAGIC || fs_info.f_type == PROC_SUPER_MAGIC) { + debug("unmounting %s", dname); + if (umount2(dname, MNT_DETACH | UMOUNT_NOFOLLOW) < 0) { + die("cannot unmount %s", dname); + } + } + } + + if (should_unlink) { + debug("unlinking %s", dname); + if (unlinkat(ns_dir_fd, dname, 0) < 0) { + die("cannot unlink %s", dname); + } + } + } + + /* Close the directory and release the lock, we're done. */ + if (closedir(ns_dir) < 0) { + die("cannot close directory"); + } + if (snap_lock_fd != -1) { + sc_unlock(snap_lock_fd); + } + return 0; +} diff --git a/cmd/snap-discard-ns/snap-discard-ns.rst b/cmd/snap-discard-ns/snap-discard-ns.rst new file mode 100644 index 00000000..0951a192 --- /dev/null +++ b/cmd/snap-discard-ns/snap-discard-ns.rst @@ -0,0 +1,62 @@ +================ + snap-discard-ns +================ + +------------------------------------------------------------------------ +internal tool for discarding preserved namespaces of snappy applications +------------------------------------------------------------------------ + +:Author: zygmunt.krynicki@canonical.com +:Date: 2018-10-17 +:Copyright: Canonical Ltd. +:Version: 2.36 +:Manual section: 5 +:Manual group: snappy + +SYNOPSIS +======== + + snap-discard-ns [--from-snap-confine] SNAP_INSTANCE_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 --from-snap-confine option is used internally by snap-confine to tell +snap-discard-ns that it is invoked from snap-confine and can disable locking. + +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_INSTNACE_NAME.mnt`: +`/run/snapd/ns/$SNAP_INSTNACE_NAME.*.mnt`: + + The preserved mount namespace that is unmounted and removed by + `snap-discard-ns`. The second form is for the per-user mount namespace. + +`/run/snapd/ns/snap.$SNAP_INSTNACE_NAME.fstab`: +`/run/snapd/ns/snap.$SNAP_INSTNACE_NAME.*.fstab`: + + The current mount profile of a preserved mount namespace that is removed + by `snap-discard-ns`. + +BUGS +==== + +Please report all bugs with https://bugs.launchpad.net/snapd/+filebug diff --git a/cmd/snap-exec/export_test.go b/cmd/snap-exec/export_test.go new file mode 100644 index 00000000..295e1fbd --- /dev/null +++ b/cmd/snap-exec/export_test.go @@ -0,0 +1,51 @@ +// -*- 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 + +var ( + ExpandEnvCmdArgs = expandEnvCmdArgs + FindCommand = findCommand + ParseArgs = parseArgs + Run = run + ExecApp = execApp + ExecHook = execHook +) + +func MockSyscallExec(f func(argv0 string, argv []string, envv []string) (err error)) func() { + origSyscallExec := syscallExec + syscallExec = f + return func() { + syscallExec = origSyscallExec + } +} + +func SetOptsCommand(s string) { + opts.Command = s +} +func GetOptsCommand() string { + return opts.Command +} + +func SetOptsHook(s string) { + opts.Hook = s +} +func GetOptsHook() string { + return opts.Hook +} diff --git a/cmd/snap-exec/main.go b/cmd/snap-exec/main.go new file mode 100644 index 00000000..ed01d34f --- /dev/null +++ b/cmd/snap-exec/main.go @@ -0,0 +1,248 @@ +// -*- 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/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapenv" +) + +// 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 init() { + // plug/slot sanitization not used nor possible from snap-exec, make it no-op + snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {} +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "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 execHook(snapApp, revision, opts.Hook) + } + + return execApp(snapApp, revision, opts.Command, extraArgs) +} + +const defaultShell = "/bin/bash" + +func findCommand(app *snap.AppInfo, command string) (string, error) { + var cmd string + switch command { + case "shell": + cmd = defaultShell + case "complete": + if app.Completer != "" { + cmd = defaultShell + } + case "stop": + cmd = app.StopCommand + case "reload": + cmd = app.ReloadCommand + case "post-stop": + cmd = app.PostStopCommand + case "", "gdb": + 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 absoluteCommandChain(snapInfo *snap.Info, commandChain []string) []string { + chain := make([]string, 0, len(commandChain)) + snapMountDir := snapInfo.MountDir() + + for _, element := range commandChain { + chain = append(chain, filepath.Join(snapMountDir, element)) + } + + return chain +} + +// expandEnvCmdArgs takes the string list of commandline arguments +// and expands any $VAR with the given var from the env argument. +func expandEnvCmdArgs(args []string, env map[string]string) []string { + cmdArgs := make([]string, 0, len(args)) + for _, arg := range args { + maybeExpanded := os.Expand(arg, func(k string) string { + return env[k] + }) + if maybeExpanded != "" { + cmdArgs = append(cmdArgs, maybeExpanded) + } + } + return cmdArgs +} + +func execApp(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 + } + + // build the environment from the yaml, translating TMPDIR and + // similar variables back from where they were hidden when + // invoking the setuid snap-confine. + env := []string{} + for _, kv := range os.Environ() { + if strings.HasPrefix(kv, snapenv.PreservedUnsafePrefix) { + kv = kv[len(snapenv.PreservedUnsafePrefix):] + } + env = append(env, kv) + } + env = append(env, osutil.SubstituteEnv(app.Env())...) + + // strings.Split() is ok here because we validate all app fields and the + // whitelist is pretty strict (see snap/validate.go:appContentWhitelist) + // (see also overlord/snapstate/check_snap.go's normPath) + tmpArgv := strings.Split(cmdAndArgs, " ") + cmd := tmpArgv[0] + cmdArgs := expandEnvCmdArgs(tmpArgv[1:], osutil.EnvMap(env)) + + // run the command + fullCmd := []string{filepath.Join(app.Snap.MountDir(), cmd)} + switch command { + case "shell": + fullCmd[0] = defaultShell + cmdArgs = nil + case "complete": + fullCmd[0] = defaultShell + cmdArgs = []string{ + dirs.CompletionHelper, + filepath.Join(app.Snap.MountDir(), app.Completer), + } + case "gdb": + fullCmd = append(fullCmd, fullCmd[0]) + fullCmd[0] = filepath.Join(dirs.CoreLibExecDir, "snap-gdb-shim") + } + fullCmd = append(fullCmd, cmdArgs...) + fullCmd = append(fullCmd, args...) + + fullCmd = append(absoluteCommandChain(app.Snap, app.CommandChain), fullCmd...) + + if err := syscallExec(fullCmd[0], fullCmd, env); err != nil { + return fmt.Errorf("cannot exec %q: %s", fullCmd[0], err) + } + // this is never reached except in tests + return nil +} + +func execHook(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(), osutil.SubstituteEnv(hook.Env())...) + + // run the hook + cmd := append(absoluteCommandChain(hook.Snap, hook.CommandChain), filepath.Join(hook.Snap.HooksDir(), hook.Name)) + return syscallExec(cmd[0], cmd, env) +} diff --git a/cmd/snap-exec/main_test.go b/cmd/snap-exec/main_test.go new file mode 100644 index 00000000..014dcb40 --- /dev/null +++ b/cmd/snap-exec/main_test.go @@ -0,0 +1,467 @@ +// -*- 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" + "os/exec" + "path/filepath" + "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" + + snapExec "github.com/snapcore/snapd/cmd/snap-exec" +) + +// 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 + snapExec.SetOptsCommand("") + snapExec.SetOptsHook("") +} + +func (s *snapExecSuite) TearDown(c *C) { + dirs.SetRootDir("/") +} + +var mockYaml = []byte(`name: snapname +version: 1.0 +apps: + app: + command: run-app cmd-arg1 $SNAP_DATA + stop-command: stop-app + post-stop-command: post-stop-app + environment: + BASE_PATH: /some/path + LD_LIBRARY_PATH: ${BASE_PATH}/lib + MY_PATH: $PATH + app2: + command: run-app2 + stop-command: stop-app2 + post-stop-command: post-stop-app2 + command-chain: [chain1, chain2] + nostop: + command: nostop +`) + +var mockHookYaml = []byte(`name: snapname +version: 1.0 +hooks: + configure: +`) + +var mockHookCommandChainYaml = []byte(`name: snapname +version: 1.0 +hooks: + configure: + command-chain: [chain1, chain2] +`) + +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 := snapExec.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 := snapExec.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 $SNAP_DATA`}, + {cmd: "stop", expected: "stop-app"}, + {cmd: "post-stop", expected: "post-stop-app"}, + } { + cmd, err := snapExec.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 = snapExec.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 = snapExec.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), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + execEnv := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + execEnv = env + return nil + }) + defer restore() + + // launch and verify its run the right way + err := snapExec.ExecApp("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, "BASE_PATH=/some/path") + c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path/lib") + c.Check(execEnv, testutil.Contains, fmt.Sprintf("MY_PATH=%s", os.Getenv("PATH"))) +} + +func (s *snapExecSuite) TestSnapExecAppCommandChainIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + return nil + }) + defer restore() + + chain1_path := fmt.Sprintf("%s/snapname/42/chain1", dirs.SnapMountDir) + chain2_path := fmt.Sprintf("%s/snapname/42/chain2", dirs.SnapMountDir) + app_path := fmt.Sprintf("%s/snapname/42/run-app2", dirs.SnapMountDir) + stop_path := fmt.Sprintf("%s/snapname/42/stop-app2", dirs.SnapMountDir) + post_stop_path := fmt.Sprintf("%s/snapname/42/post-stop-app2", dirs.SnapMountDir) + + for _, t := range []struct { + cmd string + args []string + expected []string + }{ + // Normal command + {expected: []string{chain1_path, chain2_path, app_path}}, + {args: []string{"arg1", "arg2"}, expected: []string{chain1_path, chain2_path, app_path, "arg1", "arg2"}}, + + // Stop command + {cmd: "stop", expected: []string{chain1_path, chain2_path, stop_path}}, + {cmd: "stop", args: []string{"arg1", "arg2"}, expected: []string{chain1_path, chain2_path, stop_path, "arg1", "arg2"}}, + + // Post-stop command + {cmd: "post-stop", expected: []string{chain1_path, chain2_path, post_stop_path}}, + {cmd: "post-stop", args: []string{"arg1", "arg2"}, expected: []string{chain1_path, chain2_path, post_stop_path, "arg1", "arg2"}}, + } { + err := snapExec.ExecApp("snapname.app2", "42", t.cmd, t.args) + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, t.expected[0]) + c.Check(execArgs, DeepEquals, t.expected) + } +} + +func (s *snapExecSuite) TestSnapExecHookIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockHookYaml), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + return nil + }) + defer restore() + + // launch and verify it ran correctly + err := snapExec.ExecHook("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) TestSnapExecHookCommandChainIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockHookCommandChainYaml), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + return nil + }) + defer restore() + + chain1_path := fmt.Sprintf("%s/snapname/42/chain1", dirs.SnapMountDir) + chain2_path := fmt.Sprintf("%s/snapname/42/chain2", dirs.SnapMountDir) + hook_path := fmt.Sprintf("%s/snapname/42/meta/hooks/configure", dirs.SnapMountDir) + + err := snapExec.ExecHook("snapname", "42", "configure") + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, chain1_path) + c.Check(execArgs, DeepEquals, []string{chain1_path, chain2_path, hook_path}) +} + +func (s *snapExecSuite) TestSnapExecHookMissingHookIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockHookYaml), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + err := snapExec.ExecHook("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 := snapExec.ParseArgs([]string{"--command=shell", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, IsNil) + c.Assert(snapExec.GetOptsCommand(), Equals, "shell") + c.Assert(snapApp, DeepEquals, "snapname.app") + c.Assert(rest, DeepEquals, []string{"--arg1", "arg2"}) +} + +func (s *snapExecSuite) TestSnapExecErrorsOnUnknown(c *C) { + _, _, err := snapExec.ParseArgs([]string{"--command=shell", "--unknown", "snapname.app", "--arg1", "arg2"}) + c.Check(err, ErrorMatches, "unknown flag `unknown'") +} + +func (s *snapExecSuite) TestSnapExecErrorsOnMissingSnapApp(c *C) { + _, _, err := snapExec.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), &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 :) + restore := snapExec.MockSyscallExec(actuallyExec) + defer restore() + + // run it + os.Args = []string{"snap-exec", "snapname.app", "foo", "--bar=baz", "foobar"} + err = snapExec.Run() + c.Assert(err, IsNil) + + c.Assert(canaryFile, testutil.FileEquals, `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), &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 :) + restore := snapExec.MockSyscallExec(actuallyExec) + defer restore() + + // run it + os.Args = []string{"snap-exec", "--hook=configure", "snapname"} + err := snapExec.Run() + c.Assert(err, IsNil) + + c.Assert(canaryFile, testutil.FileEquals, "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), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + execEnv := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + execEnv = env + return nil + }) + defer restore() + + // launch and verify its run the right way + err := snapExec.ExecApp("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/lib") + + // launch and verify shell still runs the command chain + err = snapExec.ExecApp("snapname.app2", "42", "shell", []string{"-c", "echo foo"}) + c.Assert(err, IsNil) + chain1 := fmt.Sprintf("%s/snapname/42/chain1", dirs.SnapMountDir) + chain2 := fmt.Sprintf("%s/snapname/42/chain2", dirs.SnapMountDir) + c.Check(execArgv0, Equals, chain1) + c.Check(execArgs, DeepEquals, []string{chain1, chain2, "/bin/bash", "-c", "echo foo"}) +} + +func (s *snapExecSuite) TestSnapExecAppIntegrationWithVars(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + execEnv := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + execEnv = env + return nil + }) + defer restore() + + // setup env + os.Setenv("SNAP_DATA", "/var/snap/snapname/42") + defer os.Unsetenv("SNAP_DATA") + + // launch and verify its run the right way + err := snapExec.ExecApp("snapname.app", "42", "", []string{"user-arg1"}) + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/run-app", dirs.SnapMountDir)) + c.Check(execArgs, DeepEquals, []string{execArgv0, "cmd-arg1", "/var/snap/snapname/42", "user-arg1"}) + c.Check(execEnv, testutil.Contains, "BASE_PATH=/some/path") + c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path/lib") + c.Check(execEnv, testutil.Contains, fmt.Sprintf("MY_PATH=%s", os.Getenv("PATH"))) +} + +func (s *snapExecSuite) TestSnapExecExpandEnvCmdArgs(c *C) { + for _, t := range []struct { + args []string + env map[string]string + expected []string + }{ + { + args: []string{"foo"}, + env: nil, + expected: []string{"foo"}, + }, + { + args: []string{"$var"}, + env: map[string]string{"var": "value"}, + expected: []string{"value"}, + }, + { + args: []string{"foo", "$not_existing"}, + env: nil, + expected: []string{"foo"}, + }, + { + args: []string{"foo", "$var", "baz"}, + env: map[string]string{"var": "bar", "unrelated": "env"}, + expected: []string{"foo", "bar", "baz"}, + }, + } { + c.Check(snapExec.ExpandEnvCmdArgs(t.args, t.env), DeepEquals, t.expected) + + } +} diff --git a/cmd/snap-failure/cmd_snapd.go b/cmd/snap-failure/cmd_snapd.go new file mode 100644 index 00000000..8a153a30 --- /dev/null +++ b/cmd/snap-failure/cmd_snapd.go @@ -0,0 +1,130 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +func init() { + const ( + short = "Run snapd failure handling" + long = "" + ) + + if _, err := parser.AddCommand("snapd", short, long, &cmdSnapd{}); err != nil { + panic(err) + } + +} + +// We do not import anything from snapd here for safety reasons so make a +// copy of the relevant struct data we care about. +type sideInfo struct { + Revision string `json:"revision"` +} + +type snapSeq struct { + Current string `json:"current"` + Sequence []*sideInfo `json:"sequence"` +} + +type cmdSnapd struct{} + +var errNoSnapd = errors.New("no snapd sequence file found") + +func prevRevision(snapName string) (string, error) { + seqFile := filepath.Join(dirs.SnapSeqDir, snapName+".json") + content, err := ioutil.ReadFile(seqFile) + if os.IsNotExist(err) { + return "", errNoSnapd + } + if err != nil { + return "", err + } + + var seq snapSeq + if err := json.Unmarshal(content, &seq); err != nil { + return "", err + } + + var prev string + for i, si := range seq.Sequence { + if seq.Current == si.Revision { + if i == 0 { + return "", fmt.Errorf("no revision to go back to") + } + prev = seq.Sequence[i-1].Revision + break + } + } + if prev == "" { + return "", fmt.Errorf("internal error: current not found in sequence: %v %v", seq.Current, seq.Sequence) + } + + return prev, nil +} + +// FIXME: also do error reporting via errtracker +func (c *cmdSnapd) Execute(args []string) error { + // find previous snapd + prevRev, err := prevRevision("snapd") + if err != nil { + if err == errNoSnapd { + return nil + } + return err + } + // stop the socket unit so that we can start snapd on its own + output, err := exec.Command("systemctl", "stop", "snapd.socket").CombinedOutput() + if err != nil { + return osutil.OutputErr(output, err) + } + + // start previous snapd + snapdPath := filepath.Join(dirs.SnapMountDir, "snapd", prevRev, "/usr/lib/snapd/snapd") + cmd := exec.Command(snapdPath) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "SNAPD_REVERT_TO_REV="+prevRev) + output, err = cmd.CombinedOutput() + if err != nil { + return osutil.OutputErr(output, err) + } + + // at this point our manually started snapd stopped and + // removed the /run/snap* sockets (this is a feature of + // golang) - we need to restart snapd.socket to make them + // available again. + output, err = exec.Command("systemctl", "restart", "snapd.socket").CombinedOutput() + if err != nil { + return osutil.OutputErr(output, err) + } + + return nil +} diff --git a/cmd/snap-failure/cmd_snapd_test.go b/cmd/snap-failure/cmd_snapd_test.go new file mode 100644 index 00000000..cd7bae01 --- /dev/null +++ b/cmd/snap-failure/cmd_snapd_test.go @@ -0,0 +1,38 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + + failure "github.com/snapcore/snapd/cmd/snap-failure" +) + +func (r *failureSuite) TestRun(c *C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + os.Args = []string{"snap-failure", "snapd"} + err := failure.Run() + c.Check(err, IsNil) + c.Check(r.Stdout(), HasLen, 0) + c.Check(r.Stderr(), HasLen, 0) +} diff --git a/cmd/snap-failure/export_test.go b/cmd/snap-failure/export_test.go new file mode 100644 index 00000000..dd94cf1f --- /dev/null +++ b/cmd/snap-failure/export_test.go @@ -0,0 +1,25 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 + +var ( + Run = run + ParseArgs = parseArgs +) diff --git a/cmd/snap-failure/main.go b/cmd/snap-failure/main.go new file mode 100644 index 00000000..82f9aaf7 --- /dev/null +++ b/cmd/snap-failure/main.go @@ -0,0 +1,77 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + + // TODO: consider not using go-flags at all + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/logger" +) + +var ( + Stdout io.Writer = os.Stdout + Stderr io.Writer = os.Stderr + + opts struct{} + parser *flags.Parser = flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) +) + +const ( + shortHelp = "Handle snapd daemon failures" + longHelp = ` +snap-failure is a tool that handles failures of the snapd daemon and +reverts if appropriate. +` +) + +func init() { + err := logger.SimpleSetup() + if err != nil { + fmt.Fprintf(Stderr, "WARNING: failed to activate logging: %v\n", err) + } +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run() error { + if err := parseArgs(os.Args[1:]); err != nil { + return err + } + + return nil +} + +func parseArgs(args []string) error { + parser.ShortDescription = shortHelp + parser.LongDescription = longHelp + + _, err := parser.ParseArgs(args) + return err +} diff --git a/cmd/snap-failure/main_test.go b/cmd/snap-failure/main_test.go new file mode 100644 index 00000000..15c6eff8 --- /dev/null +++ b/cmd/snap-failure/main_test.go @@ -0,0 +1,75 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "testing" + + . "gopkg.in/check.v1" + + failure "github.com/snapcore/snapd/cmd/snap-failure" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type failureSuite struct { + testutil.BaseTest + + rootdir string + + stdout *bytes.Buffer + stderr *bytes.Buffer +} + +func (r *failureSuite) SetUpTest(c *C) { + r.stdout = bytes.NewBuffer(nil) + r.stderr = bytes.NewBuffer(nil) + + oldStdout := failure.Stdout + r.AddCleanup(func() { failure.Stdout = oldStdout }) + failure.Stdout = r.stdout + + oldStderr := failure.Stderr + r.AddCleanup(func() { failure.Stderr = oldStderr }) + failure.Stderr = r.stderr + + r.rootdir = c.MkDir() + dirs.SetRootDir(r.rootdir) + r.AddCleanup(func() { dirs.SetRootDir("/") }) +} + +func (r *failureSuite) Stdout() string { + return r.stdout.String() +} + +func (r *failureSuite) Stderr() string { + return r.stderr.String() +} + +var _ = Suite(&failureSuite{}) + +func (r *failureSuite) TestUnknownArg(c *C) { + err := failure.ParseArgs([]string{}) + c.Check(err, ErrorMatches, "Please specify the snapd command") +} diff --git a/cmd/snap-gdb-shim/snap-gdb-shim.c b/cmd/snap-gdb-shim/snap-gdb-shim.c new file mode 100644 index 00000000..0a7e32e6 --- /dev/null +++ b/cmd/snap-gdb-shim/snap-gdb-shim.c @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 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 "../libsnap-confine-private/utils.h" + +int main(int argc, char **argv) +{ + if (sc_is_debug_enabled()) { + for (int i = 0; i < argc; i++) { + printf("-%s-\n", argv[i]); + } + } + // signal gdb to stop here + printf("\n\n"); + printf("Welcome to `snap run --gdb`.\n"); + printf("You are right before your application is execed():\n"); + printf("- set any options you may need\n"); + printf("- use 'cont' to start\n"); + printf("\n\n"); + raise(SIGTRAP); + + const char *executable = argv[1]; + execv(executable, (char *const *)&argv[1]); + perror("execv failed"); + // very different exit code to make an execve failure easy to distinguish + return 101; +} diff --git a/cmd/snap-mgmt/snap-mgmt.sh.in b/cmd/snap-mgmt/snap-mgmt.sh.in new file mode 100644 index 00000000..b441da39 --- /dev/null +++ b/cmd/snap-mgmt/snap-mgmt.sh.in @@ -0,0 +1,190 @@ +#!/bin/bash + +# Overlord management of snapd for package manager actions. +# Implements actions that would be invoked in %pre(un) actions for snapd. +# Derived from the snapd.postrm scriptlet used in the Ubuntu packaging for +# snapd. + +set -e + +SNAP_MOUNT_DIR="@SNAP_MOUNT_DIR@" + +show_help() { + exec cat <<'EOF' +Usage: snap-mgmt.sh [OPTIONS] + +A simple script to cleanup snap installations. + +optional arguments: + --help Show this help message and exit + --snap-mount-dir= Provide a path to be used as $SNAP_MOUNT_DIR + --purge Purge all data from $SNAP_MOUNT_DIR +EOF +} + +SNAP_UNIT_PREFIX="$(systemd-escape -p ${SNAP_MOUNT_DIR})" + +systemctl_stop() { + unit="$1" + if systemctl is-active -q "$unit"; then + echo "Stopping $unit" + systemctl stop -q "$unit" || true + fi +} + +purge() { + # shellcheck disable=SC1091 + distribution=$(. /etc/os-release; echo "${ID}-${VERSION_ID}") + + if [ "$distribution" = "ubuntu-14.04" ]; then + # snap.mount.service is a trusty thing + systemctl_stop snap.mount.service + else + # undo any bind mount to ${SNAP_MOUNT_DIR} that resulted from LP:#1668659 + # (that bug can't happen in trusty -- and doing this would mess up snap.mount.service there) + if grep -q "${SNAP_MOUNT_DIR} ${SNAP_MOUNT_DIR}" /proc/self/mountinfo; then + umount -l "${SNAP_MOUNT_DIR}" || true + fi + fi + + units=$(systemctl list-unit-files --no-legend --full | grep -vF snap.mount.service || true) + # *.snap mount points + mounts=$(echo "$units" | grep "^${SNAP_UNIT_PREFIX}[-.].*\\.mount" | cut -f1 -d ' ') + # services from snaps + services=$(echo "$units" | grep '^snap\..*\.service' | cut -f1 -d ' ') + for unit in $services $mounts; do + # ensure its really a snap 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 echo "$unit" | grep -q '.*\.mount' ; then + # Transform ${SNAP_MOUNT_DIR}/core/3440 -> core/3440 removing any + # extra / preceding snap name, eg: + # /var/lib/snapd/snap/core/3440 -> core/3440 + # /snap/core/3440 -> core/3440 + # /snap/core//3440 -> core/3440 + # NOTE: we could have used `systemctl show $unit -p Where --value` + # but systemd 204 shipped with Ubuntu 14.04 does not support this + snap_rev=$(systemctl show "$unit" -p Where | sed -e 's#Where=##' -e "s#$SNAP_MOUNT_DIR##" -e 's#^/*##') + snap=$(echo "$snap_rev" |cut -f1 -d/) + rev=$(echo "$snap_rev" |cut -f2 -d/) + if [ -n "$snap" ]; then + echo "Removing snap $snap" + # aliases + if [ -d "${SNAP_MOUNT_DIR}/bin" ]; then + find "${SNAP_MOUNT_DIR}/bin" -maxdepth 1 -lname "$snap" -delete + find "${SNAP_MOUNT_DIR}/bin" -maxdepth 1 -lname "$snap.*" -delete + fi + # generated binaries + rm -f "${SNAP_MOUNT_DIR}/bin/$snap" + rm -f "${SNAP_MOUNT_DIR}/bin/$snap".* + # snap mount dir + umount -l "${SNAP_MOUNT_DIR}/$snap/$rev" 2> /dev/null || true + rm -rf "${SNAP_MOUNT_DIR:?}/$snap/$rev" + rm -f "${SNAP_MOUNT_DIR}/$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_MOUNT_DIR}/$snap" "/var/snap/$snap"; do + if [ -d "$d" ]; then + rmdir --ignore-fail-on-non-empty "$d" + fi + done + # udev rules + find /etc/udev/rules.d -name "*-snap.${snap}.rules" -execdir rm -f "{}" \; + # dbus policy files + if [ -d /etc/dbus-1/system.d ]; then + find /etc/dbus-1/system.d -name "snap.${snap}.*.conf" -execdir rm -f "{}" \; + fi + # timer files + find /etc/systemd/system -name "snap.${snap}.*.timer" | while read -r f; do + systemctl_stop "$(basename "$f")" + rm -f "$f" + done + fi + 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 + if [ -d /run/snapd/ns ]; then + if [ "$(find /run/snapd/ns/ -name "*.mnt" | wc -l)" -gt 0 ]; then + for mnt in /run/snapd/ns/*.mnt; do + umount -l "$mnt" || true + rm -f "$mnt" + done + fi + if [ "$(find /run/snapd/ns/ -name "*.fstab" | wc -l)" -gt 0 ]; then + for fstab in /run/snapd/ns/*.fstab; do + rm -f "$fstab" + done + fi + umount -l /run/snapd/ns/ || true + fi + + echo "Removing downloaded snaps" + rm -rf /var/lib/snapd/snaps/* + + echo "Removing features exported from snapd to helper tools" + rm -rf /var/lib/snapd/features + + echo "Final directory cleanup" + rm -rf "${SNAP_MOUNT_DIR}" + rm -rf /var/snap + + echo "Removing leftover snap shared state data" + rm -rf /var/lib/snapd/desktop/applications/* + rm -rf /var/lib/snapd/seccomp/bpf/* + rm -rf /var/lib/snapd/device/* + rm -rf /var/lib/snapd/assertions/* + rm -rf /var/lib/snapd/cookie/* + rm -rf /var/lib/snapd/cache/* + rm -rf /var/lib/snapd/mount/* + rm -rf /var/lib/snapd/sequence/* + rm -rf /var/lib/snapd/apparmor/* + rm -f /var/lib/snapd/state.json + rm -f /var/lib/snapd/system-key + + echo "Removing snapd catalog cache" + rm -f /var/cache/snapd/* + + if test -d /etc/apparmor.d; then + # Remove auto-generated rules for snap-confine from the 'core' snap + echo "Removing extra snap-confine apparmor rules" + # shellcheck disable=SC2046 + rm -f /etc/apparmor.d/$(echo "$SNAP_UNIT_PREFIX" | tr '-' '.').core.*.usr.lib.snapd.snap-confine + fi +} + +while [ -n "$1" ]; do + case "$1" in + --help) + show_help + exit + ;; + --snap-mount-dir=*) + SNAP_MOUNT_DIR=${1#*=} + SNAP_UNIT_PREFIX=$(systemd-escape -p "$SNAP_MOUNT_DIR") + shift + ;; + --purge) + purge + shift + ;; + *) + echo "Unknown command: $1" + exit 1 + ;; + esac +done diff --git a/cmd/snap-repair/cmd_done_retry_skip.go b/cmd/snap-repair/cmd_done_retry_skip.go new file mode 100644 index 00000000..3144e5db --- /dev/null +++ b/cmd/snap-repair/cmd_done_retry_skip.go @@ -0,0 +1,82 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "strconv" +) + +func init() { + cmd, err := parser.AddCommand("done", "Signal repair is done", "", &cmdDone{}) + if err != nil { + + panic(err) + } + cmd.Hidden = true + + cmd, err = parser.AddCommand("skip", "Signal repair should be skipped", "", &cmdSkip{}) + if err != nil { + panic(err) + } + cmd.Hidden = true + + cmd, err = parser.AddCommand("retry", "Signal repair must be retried next time", "", &cmdRetry{}) + if err != nil { + panic(err) + } + cmd.Hidden = true +} + +func writeToStatusFD(msg string) error { + statusFdStr := os.Getenv("SNAP_REPAIR_STATUS_FD") + if statusFdStr == "" { + return fmt.Errorf("cannot find SNAP_REPAIR_STATUS_FD environment") + } + fd, err := strconv.Atoi(statusFdStr) + if err != nil { + return fmt.Errorf("cannot parse SNAP_REPAIR_STATUS_FD environment: %s", err) + } + f := os.NewFile(uintptr(fd), "") + defer f.Close() + if _, err := f.Write([]byte(msg + "\n")); err != nil { + return err + } + return nil +} + +type cmdDone struct{} + +func (c *cmdDone) Execute(args []string) error { + return writeToStatusFD("done") +} + +type cmdSkip struct{} + +func (c *cmdSkip) Execute([]string) error { + return writeToStatusFD("skip") +} + +type cmdRetry struct{} + +func (c *cmdRetry) Execute([]string) error { + return writeToStatusFD("retry") +} diff --git a/cmd/snap-repair/cmd_done_retry_skip_test.go b/cmd/snap-repair/cmd_done_retry_skip_test.go new file mode 100644 index 00000000..ecdf15eb --- /dev/null +++ b/cmd/snap-repair/cmd_done_retry_skip_test.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "io/ioutil" + "os" + "strconv" + "syscall" + + . "gopkg.in/check.v1" + + repair "github.com/snapcore/snapd/cmd/snap-repair" +) + +func (r *repairSuite) TestStatusNoStatusFdEnv(c *C) { + for _, s := range []string{"done", "skip", "retry"} { + err := repair.ParseArgs([]string{s}) + c.Check(err, ErrorMatches, "cannot find SNAP_REPAIR_STATUS_FD environment") + } +} + +func (r *repairSuite) TestStatusBadStatusFD(c *C) { + for _, s := range []string{"done", "skip", "retry"} { + os.Setenv("SNAP_REPAIR_STATUS_FD", "123456789") + defer os.Unsetenv("SNAP_REPAIR_STATUS_FD") + + err := repair.ParseArgs([]string{s}) + c.Check(err, ErrorMatches, `write : bad file descriptor`) + } +} + +func (r *repairSuite) TestStatusUnparsableStatusFD(c *C) { + for _, s := range []string{"done", "skip", "retry"} { + os.Setenv("SNAP_REPAIR_STATUS_FD", "xxx") + defer os.Unsetenv("SNAP_REPAIR_STATUS_FD") + + err := repair.ParseArgs([]string{s}) + c.Check(err, ErrorMatches, `cannot parse SNAP_REPAIR_STATUS_FD environment: strconv.*: parsing "xxx": invalid syntax`) + } +} + +func (r *repairSuite) TestStatusHappy(c *C) { + for _, s := range []string{"done", "skip", "retry"} { + rp, wp, err := os.Pipe() + c.Assert(err, IsNil) + defer rp.Close() + defer wp.Close() + + fd, e := syscall.Dup(int(wp.Fd())) + c.Assert(e, IsNil) + wp.Close() + + os.Setenv("SNAP_REPAIR_STATUS_FD", strconv.Itoa(fd)) + defer os.Unsetenv("SNAP_REPAIR_STATUS_FD") + + err = repair.ParseArgs([]string{s}) + c.Check(err, IsNil) + + status, err := ioutil.ReadAll(rp) + c.Assert(err, IsNil) + c.Check(string(status), Equals, s+"\n") + } +} diff --git a/cmd/snap-repair/cmd_list.go b/cmd/snap-repair/cmd_list.go new file mode 100644 index 00000000..fa24106c --- /dev/null +++ b/cmd/snap-repair/cmd_list.go @@ -0,0 +1,74 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + "text/tabwriter" +) + +func init() { + const ( + short = "Lists repairs run on this device" + long = "" + ) + + if _, err := parser.AddCommand("list", short, long, &cmdList{}); err != nil { + panic(err) + } + +} + +type cmdList struct{} + +func (c *cmdList) Execute([]string) error { + w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0) + defer w.Flush() + + // FIXME: this will not currently list the repairs that are + // skipped because of e.g. wrong architecture + + // directory structure is: + // var/lib/snapd/run/repairs/ + // canonical/ + // 1/ + // r0.retry + // r0.script + // r1.done + // r1.script + // 2/ + // r3.done + // r3.script + repairTraces, err := newRepairTraces("*", "*") + if err != nil { + return err + } + if len(repairTraces) == 0 { + fmt.Fprintf(Stderr, "no repairs yet\n") + return nil + } + + fmt.Fprintf(w, "Repair\tRev\tStatus\tSummary\n") + for _, t := range repairTraces { + fmt.Fprintf(w, "%s\t%v\t%s\t%s\n", t.Repair(), t.Revision(), t.Status(), t.Summary()) + } + + return nil +} diff --git a/cmd/snap-repair/cmd_list_test.go b/cmd/snap-repair/cmd_list_test.go new file mode 100644 index 00000000..6f4e992d --- /dev/null +++ b/cmd/snap-repair/cmd_list_test.go @@ -0,0 +1,47 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" + + repair "github.com/snapcore/snapd/cmd/snap-repair" +) + +func (r *repairSuite) TestListNoRepairsYet(c *C) { + err := repair.ParseArgs([]string{"list"}) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, "") + c.Check(r.Stderr(), Equals, "no repairs yet\n") +} + +func (r *repairSuite) TestListRepairsSimple(c *C) { + makeMockRepairState(c) + + err := repair.ParseArgs([]string{"list"}) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, `Repair Rev Status Summary +canonical-1 3 retry repair one +my-brand-1 1 done my-brand repair one +my-brand-2 2 skip my-brand repair two +my-brand-3 0 running my-brand repair three +`) + c.Check(r.Stderr(), Equals, "") +} diff --git a/cmd/snap-repair/cmd_run.go b/cmd/snap-repair/cmd_run.go new file mode 100644 index 00000000..b31db07a --- /dev/null +++ b/cmd/snap-repair/cmd_run.go @@ -0,0 +1,102 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +func init() { + const ( + short = "Fetch and run repair assertions as necessary for the device" + long = "" + ) + + if _, err := parser.AddCommand("run", short, long, &cmdRun{}); err != nil { + panic(err) + } + +} + +type cmdRun struct{} + +var baseURL *url.URL + +func init() { + var baseurl string + if osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + baseurl = "https://api.staging.snapcraft.io/v2/" + } else { + baseurl = "https://api.snapcraft.io/v2/" + } + + var err error + baseURL, err = url.Parse(baseurl) + if err != nil { + panic(fmt.Sprintf("cannot setup base url: %v", err)) + } +} + +func (c *cmdRun) Execute(args []string) error { + if err := os.MkdirAll(dirs.SnapRunRepairDir, 0755); err != nil { + return err + } + flock, err := osutil.NewFileLock(filepath.Join(dirs.SnapRunRepairDir, "lock")) + if err != nil { + return err + } + err = flock.TryLock() + if err == osutil.ErrAlreadyLocked { + return fmt.Errorf("cannot run, another snap-repair run already executing") + } + if err != nil { + return err + } + defer flock.Unlock() + + run := NewRunner() + run.BaseURL = baseURL + err = run.LoadState() + if err != nil { + return err + } + + for { + repair, err := run.Next("canonical") + if err == ErrRepairNotFound { + // no more repairs + break + } + if err != nil { + return err + } + + if err := repair.Run(); err != nil { + return err + } + } + return nil +} diff --git a/cmd/snap-repair/cmd_run_test.go b/cmd/snap-repair/cmd_run_test.go new file mode 100644 index 00000000..01f61cbb --- /dev/null +++ b/cmd/snap-repair/cmd_run_test.go @@ -0,0 +1,78 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/sysdb" + repair "github.com/snapcore/snapd/cmd/snap-repair" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +func (r *repairSuite) TestRun(c *C) { + defer release.MockOnClassic(false)() + + r1 := sysdb.InjectTrusted(r.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{r.repairRootAcctKey}) + defer r2() + + r.freshState(c) + + const script = `#!/bin/sh +echo "happy output" +echo "done" >&$SNAP_REPAIR_STATUS_FD +exit 0 +` + seqRepairs := r.signSeqRepairs(c, []string{makeMockRepair(script)}) + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + repair.MockBaseURL(mockServer.URL) + + origArgs := os.Args + defer func() { os.Args = origArgs }() + os.Args = []string{"snap-repair", "run"} + err := repair.Run() + c.Check(err, IsNil) + c.Check(r.Stdout(), HasLen, 0) + + c.Check(osutil.FileExists(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.done")), Equals, true) +} + +func (r *repairSuite) TestRunAlreadyLocked(c *C) { + err := os.MkdirAll(dirs.SnapRunRepairDir, 0700) + c.Assert(err, IsNil) + flock, err := osutil.NewFileLock(filepath.Join(dirs.SnapRunRepairDir, "lock")) + c.Assert(err, IsNil) + err = flock.Lock() + c.Assert(err, IsNil) + defer flock.Unlock() + + err = repair.ParseArgs([]string{"run"}) + c.Check(err, ErrorMatches, `cannot run, another snap-repair run already executing`) +} diff --git a/cmd/snap-repair/cmd_show.go b/cmd/snap-repair/cmd_show.go new file mode 100644 index 00000000..c95d2a91 --- /dev/null +++ b/cmd/snap-repair/cmd_show.go @@ -0,0 +1,91 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + "io" + "strings" +) + +func init() { + const ( + short = "Shows specific repairs run on this device" + long = "" + ) + + if _, err := parser.AddCommand("show", short, long, &cmdShow{}); err != nil { + panic(err) + } + +} + +type cmdShow struct { + Positional struct { + Repair []string `positional-arg-name:""` + } `positional-args:"yes"` +} + +func showRepairDetails(w io.Writer, repair string) error { + i := strings.LastIndex(repair, "-") + if i < 0 { + return fmt.Errorf("cannot parse repair %q", repair) + } + brand := repair[:i] + seq := repair[i+1:] + + repairTraces, err := newRepairTraces(brand, seq) + if err != nil { + return err + } + if len(repairTraces) == 0 { + return fmt.Errorf("cannot find repair \"%s-%s\"", brand, seq) + } + + for _, trace := range repairTraces { + fmt.Fprintf(w, "repair: %s\n", trace.Repair()) + fmt.Fprintf(w, "revision: %s\n", trace.Revision()) + fmt.Fprintf(w, "status: %s\n", trace.Status()) + fmt.Fprintf(w, "summary: %s\n", trace.Summary()) + + fmt.Fprintf(w, "script:\n") + if err := trace.WriteScriptIndented(w, 2); err != nil { + fmt.Fprintf(w, "%serror: %s\n", indentPrefix(2), err) + } + + fmt.Fprintf(w, "output:\n") + if err := trace.WriteOutputIndented(w, 2); err != nil { + fmt.Fprintf(w, "%serror: %s\n", indentPrefix(2), err) + } + } + + return nil +} + +func (c *cmdShow) Execute([]string) error { + for _, repair := range c.Positional.Repair { + if err := showRepairDetails(Stdout, repair); err != nil { + return err + } + fmt.Fprintf(Stdout, "\n") + } + + return nil +} diff --git a/cmd/snap-repair/cmd_show_test.go b/cmd/snap-repair/cmd_show_test.go new file mode 100644 index 00000000..8796e218 --- /dev/null +++ b/cmd/snap-repair/cmd_show_test.go @@ -0,0 +1,142 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "fmt" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + repair "github.com/snapcore/snapd/cmd/snap-repair" + "github.com/snapcore/snapd/dirs" +) + +func (r *repairSuite) TestShowRepairSingle(c *C) { + makeMockRepairState(c) + + err := repair.ParseArgs([]string{"show", "canonical-1"}) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, `repair: canonical-1 +revision: 3 +status: retry +summary: repair one +script: + #!/bin/sh + echo retry output +output: + retry output + +`) + +} + +func (r *repairSuite) TestShowRepairMultiple(c *C) { + makeMockRepairState(c) + + // repair.ParseArgs() always appends to its internal slice: + // cmdShow.Positional.Repair. To workaround this we create a + // new cmdShow here + err := repair.NewCmdShow("canonical-1", "my-brand-1", "my-brand-2").Execute(nil) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, `repair: canonical-1 +revision: 3 +status: retry +summary: repair one +script: + #!/bin/sh + echo retry output +output: + retry output + +repair: my-brand-1 +revision: 1 +status: done +summary: my-brand repair one +script: + #!/bin/sh + echo done output +output: + done output + +repair: my-brand-2 +revision: 2 +status: skip +summary: my-brand repair two +script: + #!/bin/sh + echo skip output +output: + skip output + +`) +} + +func (r *repairSuite) TestShowRepairErrorNoRepairDir(c *C) { + dirs.SetRootDir(c.MkDir()) + + err := repair.NewCmdShow("canonical-1").Execute(nil) + c.Check(err, ErrorMatches, `cannot find repair "canonical-1"`) +} + +func (r *repairSuite) TestShowRepairSingleWithoutScript(c *C) { + makeMockRepairState(c) + scriptPath := filepath.Join(dirs.SnapRepairRunDir, "canonical/1", "r3.script") + err := os.Remove(scriptPath) + c.Assert(err, IsNil) + + err = repair.NewCmdShow("canonical-1").Execute(nil) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, fmt.Sprintf(`repair: canonical-1 +revision: 3 +status: retry +summary: repair one +script: + error: open %s: no such file or directory +output: + retry output + +`, scriptPath)) + +} + +func (r *repairSuite) TestShowRepairSingleUnreadableOutput(c *C) { + makeMockRepairState(c) + scriptPath := filepath.Join(dirs.SnapRepairRunDir, "canonical/1", "r3.retry") + err := os.Chmod(scriptPath, 0000) + c.Assert(err, IsNil) + defer os.Chmod(scriptPath, 0644) + + err = repair.NewCmdShow("canonical-1").Execute(nil) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, fmt.Sprintf(`repair: canonical-1 +revision: 3 +status: retry +summary: - +script: + #!/bin/sh + echo retry output +output: + error: open %s: permission denied + +`, scriptPath)) + +} diff --git a/cmd/snap-repair/export_test.go b/cmd/snap-repair/export_test.go new file mode 100644 index 00000000..e5666232 --- /dev/null +++ b/cmd/snap-repair/export_test.go @@ -0,0 +1,141 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "net/url" + "time" + + "gopkg.in/retry.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/httputil" +) + +var ( + ParseArgs = parseArgs + Run = run +) + +func MockBaseURL(baseurl string) (restore func()) { + orig := baseURL + u, err := url.Parse(baseurl) + if err != nil { + panic(err) + } + baseURL = u + return func() { + baseURL = orig + } +} + +func MockFetchRetryStrategy(strategy retry.Strategy) (restore func()) { + originalFetchRetryStrategy := fetchRetryStrategy + fetchRetryStrategy = strategy + return func() { + fetchRetryStrategy = originalFetchRetryStrategy + } +} + +func MockPeekRetryStrategy(strategy retry.Strategy) (restore func()) { + originalPeekRetryStrategy := peekRetryStrategy + peekRetryStrategy = strategy + return func() { + peekRetryStrategy = originalPeekRetryStrategy + } +} + +func MockMaxRepairScriptSize(maxSize int) (restore func()) { + originalMaxSize := maxRepairScriptSize + maxRepairScriptSize = maxSize + return func() { + maxRepairScriptSize = originalMaxSize + } +} + +func MockTrustedRepairRootKeys(keys []*asserts.AccountKey) (restore func()) { + original := trustedRepairRootKeys + trustedRepairRootKeys = keys + return func() { + trustedRepairRootKeys = original + } +} + +func TrustedRepairRootKeys() []*asserts.AccountKey { + return trustedRepairRootKeys +} + +func (run *Runner) BrandModel() (brand, model string) { + return run.state.Device.Brand, run.state.Device.Model +} + +func (run *Runner) SetStateModified(modified bool) { + run.stateModified = modified +} + +func (run *Runner) SetBrandModel(brand, model string) { + run.state.Device.Brand = brand + run.state.Device.Model = model +} + +func (run *Runner) TimeLowerBound() time.Time { + return run.state.TimeLowerBound +} + +func (run *Runner) TLSTime() time.Time { + return httputil.BaseTransport(run.cli).TLSClientConfig.Time() +} + +func (run *Runner) Sequence(brand string) []*RepairState { + return run.state.Sequences[brand] +} + +func (run *Runner) SetSequence(brand string, sequence []*RepairState) { + if run.state.Sequences == nil { + run.state.Sequences = make(map[string][]*RepairState) + } + run.state.Sequences[brand] = sequence +} + +func MockDefaultRepairTimeout(d time.Duration) (restore func()) { + orig := defaultRepairTimeout + defaultRepairTimeout = d + return func() { + defaultRepairTimeout = orig + } +} + +func MockErrtrackerReportRepair(mock func(string, string, string, map[string]string) (string, error)) (restore func()) { + prev := errtrackerReportRepair + errtrackerReportRepair = mock + return func() { errtrackerReportRepair = prev } +} + +func MockTimeNow(f func() time.Time) (restore func()) { + origTimeNow := timeNow + timeNow = f + return func() { timeNow = origTimeNow } +} + +func NewCmdShow(args ...string) *cmdShow { + cmdShow := &cmdShow{} + cmdShow.Positional.Repair = args + return cmdShow +} diff --git a/cmd/snap-repair/main.go b/cmd/snap-repair/main.go new file mode 100644 index 00000000..8b578356 --- /dev/null +++ b/cmd/snap-repair/main.go @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + "io" + "os" + + // TODO: consider not using go-flags at all + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/release" +) + +var ( + Stdout io.Writer = os.Stdout + Stderr io.Writer = os.Stderr + + opts struct{} + parser *flags.Parser = flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) +) + +const ( + shortHelp = "Repair an Ubuntu Core system" + longHelp = ` +snap-repair is a tool to fetch and run repair assertions +which are used to do emergency repairs on the device. +` +) + +func init() { + err := logger.SimpleSetup() + if err != nil { + fmt.Fprintf(Stderr, "WARNING: failed to activate logging: %v\n", err) + } +} + +var errOnClassic = fmt.Errorf("cannot use snap-repair on a classic system") + +func main() { + if err := run(); err != nil { + fmt.Fprintf(Stderr, "error: %v\n", err) + if err != errOnClassic { + os.Exit(1) + } + } +} + +func run() error { + if release.OnClassic { + return errOnClassic + } + httputil.SetUserAgentFromVersion(cmd.Version, "snap-repair") + + if err := parseArgs(os.Args[1:]); err != nil { + return err + } + + return nil +} + +func parseArgs(args []string) error { + parser.ShortDescription = shortHelp + parser.LongDescription = longHelp + + _, err := parser.ParseArgs(args) + return err +} diff --git a/cmd/snap-repair/main_test.go b/cmd/snap-repair/main_test.go new file mode 100644 index 00000000..af955fcd --- /dev/null +++ b/cmd/snap-repair/main_test.go @@ -0,0 +1,99 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "bytes" + "testing" + + . "gopkg.in/check.v1" + + repair "github.com/snapcore/snapd/cmd/snap-repair" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type repairSuite struct { + testutil.BaseTest + baseRunnerSuite + + rootdir string + + stdout *bytes.Buffer + stderr *bytes.Buffer + + restore func() +} + +func (r *repairSuite) SetUpSuite(c *C) { + r.baseRunnerSuite.SetUpSuite(c) + r.restore = httputil.SetUserAgentFromVersion("", "") +} + +func (r *repairSuite) TearDownSuite(c *C) { + r.restore() +} + +func (r *repairSuite) SetUpTest(c *C) { + r.BaseTest.SetUpTest(c) + r.baseRunnerSuite.SetUpTest(c) + + r.stdout = bytes.NewBuffer(nil) + r.stderr = bytes.NewBuffer(nil) + + oldStdout := repair.Stdout + r.AddCleanup(func() { repair.Stdout = oldStdout }) + repair.Stdout = r.stdout + + oldStderr := repair.Stderr + r.AddCleanup(func() { repair.Stderr = oldStderr }) + repair.Stderr = r.stderr + + r.rootdir = c.MkDir() + dirs.SetRootDir(r.rootdir) + r.AddCleanup(func() { dirs.SetRootDir("/") }) +} + +func (r *repairSuite) Stdout() string { + return r.stdout.String() +} + +func (r *repairSuite) Stderr() string { + return r.stderr.String() +} + +var _ = Suite(&repairSuite{}) + +func (r *repairSuite) TestUnknownArg(c *C) { + err := repair.ParseArgs([]string{}) + c.Check(err, ErrorMatches, "Please specify one command of: list, run or show") +} + +func (r *repairSuite) TestRunOnClassic(c *C) { + defer release.MockOnClassic(true)() + + err := repair.Run() + c.Check(err, ErrorMatches, "cannot use snap-repair on a classic system") +} diff --git a/cmd/snap-repair/runner.go b/cmd/snap-repair/runner.go new file mode 100644 index 00000000..31b1aaa1 --- /dev/null +++ b/cmd/snap-repair/runner.go @@ -0,0 +1,993 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "bufio" + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + "gopkg.in/retry.v1" + + "github.com/snapcore/snapd/arch" + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/errtracker" + "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/strutil" +) + +var ( + // TODO: move inside the repairs themselves? + defaultRepairTimeout = 30 * time.Minute +) + +var errtrackerReportRepair = errtracker.ReportRepair + +// Repair is a runnable repair. +type Repair struct { + *asserts.Repair + + run *Runner + sequence int +} + +func (r *Repair) RunDir() string { + return filepath.Join(dirs.SnapRepairRunDir, r.BrandID(), strconv.Itoa(r.RepairID())) +} + +func (r *Repair) String() string { + return fmt.Sprintf("%s-%v", r.BrandID(), r.RepairID()) +} + +// SetStatus sets the status of the repair in the state and saves the latter. +func (r *Repair) SetStatus(status RepairStatus) { + brandID := r.BrandID() + cur := *r.run.state.Sequences[brandID][r.sequence-1] + cur.Status = status + r.run.setRepairState(brandID, cur) + r.run.SaveState() +} + +// makeRepairSymlink ensures $dir/repair exists and is a symlink to +// /usr/lib/snapd/snap-repair +func makeRepairSymlink(dir string) (err error) { + // make "repair" binary available to the repair scripts via symlink + // to the real snap-repair + if err = os.MkdirAll(dir, 0755); err != nil { + return err + } + + old := filepath.Join(dirs.CoreLibExecDir, "snap-repair") + new := filepath.Join(dir, "repair") + if err := os.Symlink(old, new); err != nil && !os.IsExist(err) { + return err + } + + return nil +} + +// Run executes the repair script leaving execution trail files on disk. +func (r *Repair) Run() error { + // write the script to disk + rundir := r.RunDir() + err := os.MkdirAll(rundir, 0775) + if err != nil { + return err + } + + // ensure the script can use "repair done" + repairToolsDir := filepath.Join(dirs.SnapRunRepairDir, "tools") + if err := makeRepairSymlink(repairToolsDir); err != nil { + return err + } + + baseName := fmt.Sprintf("r%d", r.Revision()) + script := filepath.Join(rundir, baseName+".script") + err = osutil.AtomicWriteFile(script, r.Body(), 0700, 0) + if err != nil { + return err + } + + logPath := filepath.Join(rundir, baseName+".running") + logf, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer logf.Close() + + fmt.Fprintf(logf, "repair: %s\n", r) + fmt.Fprintf(logf, "revision: %d\n", r.Revision()) + fmt.Fprintf(logf, "summary: %s\n", r.Summary()) + fmt.Fprintf(logf, "output:\n") + + statusR, statusW, err := os.Pipe() + if err != nil { + return err + } + defer statusR.Close() + defer statusW.Close() + + logger.Debugf("executing %s", script) + + // run the script + env := os.Environ() + // we need to hardcode FD=3 because this is the FD after + // exec.Command() forked. there is no way in go currently + // to run something right after fork() in the child to + // know the fd. However because go will close all fds + // except the ones in "cmd.ExtraFiles" we are safe to set "3" + env = append(env, "SNAP_REPAIR_STATUS_FD=3") + env = append(env, "SNAP_REPAIR_RUN_DIR="+rundir) + // inject repairToolDir into PATH so that the script can use + // `repair {done,skip,retry}` + var havePath bool + for i, envStr := range env { + if strings.HasPrefix(envStr, "PATH=") { + newEnv := fmt.Sprintf("%s:%s", strings.TrimSuffix(envStr, ":"), repairToolsDir) + env[i] = newEnv + havePath = true + } + } + if !havePath { + env = append(env, "PATH=/usr/sbin:/usr/bin:/sbin:/bin:"+repairToolsDir) + } + + workdir := filepath.Join(rundir, "work") + if err := os.MkdirAll(workdir, 0700); err != nil { + return err + } + + cmd := exec.Command(script) + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Env = env + cmd.Dir = workdir + cmd.ExtraFiles = []*os.File{statusW} + cmd.Stdout = logf + cmd.Stderr = logf + if err = cmd.Start(); err != nil { + return err + } + statusW.Close() + + // wait for repair to finish or timeout + var scriptErr error + killTimerCh := time.After(defaultRepairTimeout) + doneCh := make(chan error) + go func() { + doneCh <- cmd.Wait() + close(doneCh) + }() + select { + case scriptErr = <-doneCh: + // done + case <-killTimerCh: + if err := osutil.KillProcessGroup(cmd); err != nil { + logger.Noticef("cannot kill timed out repair %s: %s", r, err) + } + scriptErr = fmt.Errorf("repair did not finish within %s", defaultRepairTimeout) + } + // read repair status pipe, use the last value + status := readStatus(statusR) + statusPath := filepath.Join(rundir, baseName+"."+status.String()) + + // if the script had an error exit status still honor what we + // read from the status-pipe, however report the error + if scriptErr != nil { + scriptErr = fmt.Errorf("repair %s revision %d failed: %s", r, r.Revision(), scriptErr) + if err := r.errtrackerReport(scriptErr, status, logPath); err != nil { + logger.Noticef("cannot report error to errtracker: %s", err) + } + // ensure the error is present in the output log + fmt.Fprintf(logf, "\n%s", scriptErr) + } + if err := os.Rename(logPath, statusPath); err != nil { + return err + } + r.SetStatus(status) + + return nil +} + +func readStatus(r io.Reader) RepairStatus { + var status RepairStatus + scanner := bufio.NewScanner(r) + for scanner.Scan() { + switch strings.TrimSpace(scanner.Text()) { + case "done": + status = DoneStatus + // TODO: support having a script skip over many and up to a given repair-id # + case "skip": + status = SkipStatus + } + } + if scanner.Err() != nil { + return RetryStatus + } + return status +} + +// errtrackerReport reports an repairErr with the given logPath to the +// snap error tracker. +func (r *Repair) errtrackerReport(repairErr error, status RepairStatus, logPath string) error { + errMsg := repairErr.Error() + + scriptOutput, err := ioutil.ReadFile(logPath) + if err != nil { + logger.Noticef("cannot read %s", logPath) + } + s := fmt.Sprintf("%s/%d", r.BrandID(), r.RepairID()) + + dupSig := fmt.Sprintf("%s\n%s\noutput:\n%s", s, errMsg, scriptOutput) + extra := map[string]string{ + "Revision": strconv.Itoa(r.Revision()), + "BrandID": r.BrandID(), + "RepairID": strconv.Itoa(r.RepairID()), + "Status": status.String(), + } + _, err = errtrackerReportRepair(s, errMsg, dupSig, extra) + return err +} + +// Runner implements fetching, tracking and running repairs. +type Runner struct { + BaseURL *url.URL + cli *http.Client + + state state + stateModified bool + + // sequenceNext keeps track of the next integer id in a brand sequence to considered in this run, see Next. + sequenceNext map[string]int +} + +// NewRunner returns a Runner. +func NewRunner() *Runner { + run := &Runner{ + sequenceNext: make(map[string]int), + } + opts := httputil.ClientOptions{ + MayLogBody: false, + TLSConfig: &tls.Config{ + Time: run.now, + }, + } + run.cli = httputil.NewHTTPClient(&opts) + return run +} + +var ( + fetchRetryStrategy = retry.LimitCount(7, retry.LimitTime(90*time.Second, + retry.Exponential{ + Initial: 500 * time.Millisecond, + Factor: 2.5, + }, + )) + + peekRetryStrategy = retry.LimitCount(5, retry.LimitTime(44*time.Second, + retry.Exponential{ + Initial: 300 * time.Millisecond, + Factor: 2.5, + }, + )) +) + +var ( + ErrRepairNotFound = errors.New("repair not found") + ErrRepairNotModified = errors.New("repair was not modified") +) + +var ( + maxRepairScriptSize = 24 * 1024 * 1024 +) + +// Fetch retrieves a stream with the repair with the given ids and any +// auxiliary assertions. If revision>=0 the request will include an +// If-None-Match header with an ETag for the revision, and +// ErrRepairNotModified is returned if the revision is still current. +func (run *Runner) Fetch(brandID string, repairID int, revision int) (*asserts.Repair, []asserts.Assertion, error) { + u, err := run.BaseURL.Parse(fmt.Sprintf("repairs/%s/%d", brandID, repairID)) + if err != nil { + return nil, nil, err + } + + var r []asserts.Assertion + resp, err := httputil.RetryRequest(u.String(), func() (*http.Response, error) { + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", httputil.UserAgent()) + req.Header.Set("Accept", "application/x.ubuntu.assertion") + if revision >= 0 { + req.Header.Set("If-None-Match", fmt.Sprintf(`"%d"`, revision)) + } + return run.cli.Do(req) + }, func(resp *http.Response) error { + if resp.StatusCode == 200 { + logger.Debugf("fetching repair %s-%d", brandID, repairID) + // decode assertions + dec := asserts.NewDecoderWithTypeMaxBodySize(resp.Body, map[*asserts.AssertionType]int{ + asserts.RepairType: maxRepairScriptSize, + }) + for { + a, err := dec.Decode() + if err == io.EOF { + break + } + if err != nil { + return err + } + r = append(r, a) + } + if len(r) == 0 { + return io.ErrUnexpectedEOF + } + } + return nil + }, fetchRetryStrategy) + + if err != nil { + return nil, nil, err + } + + moveTimeLowerBound := true + defer func() { + if moveTimeLowerBound { + t, _ := http.ParseTime(resp.Header.Get("Date")) + run.moveTimeLowerBound(t) + } + }() + + switch resp.StatusCode { + case 200: + // ok + case 304: + // not modified + return nil, nil, ErrRepairNotModified + case 404: + return nil, nil, ErrRepairNotFound + default: + moveTimeLowerBound = false + return nil, nil, fmt.Errorf("cannot fetch repair, unexpected status %d", resp.StatusCode) + } + + repair, aux, err := checkStream(brandID, repairID, r) + if err != nil { + return nil, nil, fmt.Errorf("cannot fetch repair, %v", err) + } + + if repair.Revision() <= revision { + // this shouldn't happen but if it does we behave like + // all the rest of assertion infrastructure and ignore + // the now superseded revision + return nil, nil, ErrRepairNotModified + } + + return repair, aux, err +} + +func checkStream(brandID string, repairID int, r []asserts.Assertion) (repair *asserts.Repair, aux []asserts.Assertion, err error) { + if len(r) == 0 { + return nil, nil, fmt.Errorf("empty repair assertions stream") + } + var ok bool + repair, ok = r[0].(*asserts.Repair) + if !ok { + return nil, nil, fmt.Errorf("unexpected first assertion %q", r[0].Type().Name) + } + + if repair.BrandID() != brandID || repair.RepairID() != repairID { + return nil, nil, fmt.Errorf("repair id mismatch %s/%d != %s/%d", repair.BrandID(), repair.RepairID(), brandID, repairID) + } + + return repair, r[1:], nil +} + +type peekResp struct { + Headers map[string]interface{} `json:"headers"` +} + +// Peek retrieves the headers for the repair with the given ids. +func (run *Runner) Peek(brandID string, repairID int) (headers map[string]interface{}, err error) { + u, err := run.BaseURL.Parse(fmt.Sprintf("repairs/%s/%d", brandID, repairID)) + if err != nil { + return nil, err + } + + var rsp peekResp + + resp, err := httputil.RetryRequest(u.String(), func() (*http.Response, error) { + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", httputil.UserAgent()) + req.Header.Set("Accept", "application/json") + return run.cli.Do(req) + }, func(resp *http.Response) error { + rsp.Headers = nil + if resp.StatusCode == 200 { + dec := json.NewDecoder(resp.Body) + return dec.Decode(&rsp) + } + return nil + }, peekRetryStrategy) + + if err != nil { + return nil, err + } + + moveTimeLowerBound := true + defer func() { + if moveTimeLowerBound { + t, _ := http.ParseTime(resp.Header.Get("Date")) + run.moveTimeLowerBound(t) + } + }() + + switch resp.StatusCode { + case 200: + // ok + case 404: + return nil, ErrRepairNotFound + default: + moveTimeLowerBound = false + return nil, fmt.Errorf("cannot peek repair headers, unexpected status %d", resp.StatusCode) + } + + headers = rsp.Headers + if headers["brand-id"] != brandID || headers["repair-id"] != strconv.Itoa(repairID) { + return nil, fmt.Errorf("cannot peek repair headers, repair id mismatch %s/%s != %s/%d", headers["brand-id"], headers["repair-id"], brandID, repairID) + } + + return headers, nil +} + +// deviceInfo captures information about the device. +type deviceInfo struct { + Brand string `json:"brand"` + Model string `json:"model"` +} + +// RepairStatus represents the possible statuses of a repair. +type RepairStatus int + +const ( + RetryStatus RepairStatus = iota + SkipStatus + DoneStatus +) + +func (rs RepairStatus) String() string { + switch rs { + case RetryStatus: + return "retry" + case SkipStatus: + return "skip" + case DoneStatus: + return "done" + default: + return "unknown" + } +} + +// RepairState holds the current revision and status of a repair in a sequence of repairs. +type RepairState struct { + Sequence int `json:"sequence"` + Revision int `json:"revision"` + Status RepairStatus `json:"status"` +} + +// state holds the atomically updated control state of the runner with sequences of repairs and their states. +type state struct { + Device deviceInfo `json:"device"` + Sequences map[string][]*RepairState `json:"sequences,omitempty"` + TimeLowerBound time.Time `json:"time-lower-bound"` +} + +func (run *Runner) setRepairState(brandID string, state RepairState) { + if run.state.Sequences == nil { + run.state.Sequences = make(map[string][]*RepairState) + } + sequence := run.state.Sequences[brandID] + if state.Sequence > len(sequence) { + run.stateModified = true + run.state.Sequences[brandID] = append(sequence, &state) + } else if *sequence[state.Sequence-1] != state { + run.stateModified = true + sequence[state.Sequence-1] = &state + } +} + +func (run *Runner) readState() error { + r, err := os.Open(dirs.SnapRepairStateFile) + if err != nil { + return err + } + defer r.Close() + dec := json.NewDecoder(r) + return dec.Decode(&run.state) +} + +func (run *Runner) moveTimeLowerBound(t time.Time) { + if t.After(run.state.TimeLowerBound) { + run.stateModified = true + run.state.TimeLowerBound = t.UTC() + } +} + +var timeNow = time.Now + +func (run *Runner) now() time.Time { + now := timeNow().UTC() + if now.Before(run.state.TimeLowerBound) { + return run.state.TimeLowerBound + } + return now +} + +func (run *Runner) initState() error { + if err := os.MkdirAll(dirs.SnapRepairDir, 0775); err != nil { + return fmt.Errorf("cannot create repair state directory: %v", err) + } + // best-effort remove old + os.Remove(dirs.SnapRepairStateFile) + run.state = state{} + // initialize time lower bound with image built time/seed.yaml time + info, err := os.Stat(filepath.Join(dirs.SnapSeedDir, "seed.yaml")) + if err != nil { + return err + } + run.moveTimeLowerBound(info.ModTime()) + // initialize device info + if err := run.initDeviceInfo(); err != nil { + return err + } + run.stateModified = true + return run.SaveState() +} + +func trustedBackstore(trusted []asserts.Assertion) asserts.Backstore { + trustedBS := asserts.NewMemoryBackstore() + for _, t := range trusted { + trustedBS.Put(t.Type(), t) + } + return trustedBS +} + +func checkAuthorityID(a asserts.Assertion, trusted asserts.Backstore) error { + assertType := a.Type() + if assertType != asserts.AccountKeyType && assertType != asserts.AccountType { + return nil + } + // check that account and account-key assertions are signed by + // a trusted authority + acctID := a.AuthorityID() + _, err := trusted.Get(asserts.AccountType, []string{acctID}, asserts.AccountType.MaxSupportedFormat()) + if err != nil && !asserts.IsNotFound(err) { + return err + } + if asserts.IsNotFound(err) { + return fmt.Errorf("%v not signed by trusted authority: %s", a.Ref(), acctID) + } + return nil +} + +func verifySignatures(a asserts.Assertion, workBS asserts.Backstore, trusted asserts.Backstore) error { + if err := checkAuthorityID(a, trusted); err != nil { + return err + } + acctKeyMaxSuppFormat := asserts.AccountKeyType.MaxSupportedFormat() + + seen := make(map[string]bool) + bottom := false + for !bottom { + u := a.Ref().Unique() + if seen[u] { + return fmt.Errorf("circular assertions") + } + seen[u] = true + signKey := []string{a.SignKeyID()} + key, err := trusted.Get(asserts.AccountKeyType, signKey, acctKeyMaxSuppFormat) + if err != nil && !asserts.IsNotFound(err) { + return err + } + if err == nil { + bottom = true + } else { + key, err = workBS.Get(asserts.AccountKeyType, signKey, acctKeyMaxSuppFormat) + if err != nil && !asserts.IsNotFound(err) { + return err + } + if asserts.IsNotFound(err) { + return fmt.Errorf("cannot find public key %q", signKey[0]) + } + if err := checkAuthorityID(key, trusted); err != nil { + return err + } + } + if err := asserts.CheckSignature(a, key.(*asserts.AccountKey), nil, time.Time{}); err != nil { + return err + } + a = key + } + return nil +} + +func (run *Runner) initDeviceInfo() error { + const errPrefix = "cannot set device information: " + + workBS := asserts.NewMemoryBackstore() + assertSeedDir := filepath.Join(dirs.SnapSeedDir, "assertions") + dc, err := ioutil.ReadDir(assertSeedDir) + if err != nil { + return err + } + var model *asserts.Model + for _, fi := range dc { + fn := filepath.Join(assertSeedDir, fi.Name()) + f, err := os.Open(fn) + if err != nil { + // best effort + continue + } + dec := asserts.NewDecoder(f) + for { + a, err := dec.Decode() + if err != nil { + // best effort + break + } + switch a.Type() { + case asserts.ModelType: + if model != nil { + return fmt.Errorf(errPrefix + "multiple models in seed assertions") + } + model = a.(*asserts.Model) + case asserts.AccountType, asserts.AccountKeyType: + workBS.Put(a.Type(), a) + } + } + } + if model == nil { + return fmt.Errorf(errPrefix + "no model assertion in seed data") + } + trustedBS := trustedBackstore(sysdb.Trusted()) + if err := verifySignatures(model, workBS, trustedBS); err != nil { + return fmt.Errorf(errPrefix+"%v", err) + } + acctPK := []string{model.BrandID()} + acctMaxSupFormat := asserts.AccountType.MaxSupportedFormat() + acct, err := trustedBS.Get(asserts.AccountType, acctPK, acctMaxSupFormat) + if err != nil { + var err error + acct, err = workBS.Get(asserts.AccountType, acctPK, acctMaxSupFormat) + if err != nil { + return fmt.Errorf(errPrefix + "no brand account assertion in seed data") + } + } + if err := verifySignatures(acct, workBS, trustedBS); err != nil { + return fmt.Errorf(errPrefix+"%v", err) + } + run.state.Device.Brand = model.BrandID() + run.state.Device.Model = model.Model() + return nil +} + +// LoadState loads the repairs' state from disk, and (re)initializes it if it's missing or corrupted. +func (run *Runner) LoadState() error { + err := run.readState() + if err == nil { + return nil + } + // error => initialize from scratch + if !os.IsNotExist(err) { + logger.Noticef("cannor read repair state: %v", err) + } + return run.initState() +} + +// SaveState saves the repairs' state to disk. +func (run *Runner) SaveState() error { + if !run.stateModified { + return nil + } + m, err := json.Marshal(&run.state) + if err != nil { + return fmt.Errorf("cannot marshal repair state: %v", err) + } + err = osutil.AtomicWriteFile(dirs.SnapRepairStateFile, m, 0600, 0) + if err != nil { + return fmt.Errorf("cannot save repair state: %v", err) + } + run.stateModified = false + return nil +} + +func stringList(headers map[string]interface{}, name string) ([]string, error) { + v, ok := headers[name] + if !ok { + return nil, nil + } + l, ok := v.([]interface{}) + if !ok { + return nil, fmt.Errorf("header %q is not a list", name) + } + r := make([]string, len(l)) + for i, v := range l { + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("header %q contains non-string elements", name) + } + r[i] = s + } + return r, nil +} + +// Applicable returns whether a repair with the given headers is applicable to the device. +func (run *Runner) Applicable(headers map[string]interface{}) bool { + if headers["disabled"] == "true" { + return false + } + series, err := stringList(headers, "series") + if err != nil { + return false + } + if len(series) != 0 && !strutil.ListContains(series, release.Series) { + return false + } + archs, err := stringList(headers, "architectures") + if err != nil { + return false + } + if len(archs) != 0 && !strutil.ListContains(archs, arch.UbuntuArchitecture()) { + return false + } + brandModel := fmt.Sprintf("%s/%s", run.state.Device.Brand, run.state.Device.Model) + models, err := stringList(headers, "models") + if err != nil { + return false + } + if len(models) != 0 && !strutil.ListContains(models, brandModel) { + // model prefix matching: brand/prefix* + hit := false + for _, patt := range models { + if strings.HasSuffix(patt, "*") && strings.ContainsRune(patt, '/') { + if strings.HasPrefix(brandModel, strings.TrimSuffix(patt, "*")) { + hit = true + break + } + } + } + if !hit { + return false + } + } + return true +} + +var errSkip = errors.New("repair unnecessary on this system") + +func (run *Runner) fetch(brandID string, repairID int) (repair *asserts.Repair, aux []asserts.Assertion, err error) { + headers, err := run.Peek(brandID, repairID) + if err != nil { + return nil, nil, err + } + if !run.Applicable(headers) { + return nil, nil, errSkip + } + return run.Fetch(brandID, repairID, -1) +} + +func (run *Runner) refetch(brandID string, repairID, revision int) (repair *asserts.Repair, aux []asserts.Assertion, err error) { + return run.Fetch(brandID, repairID, revision) +} + +func (run *Runner) saveStream(brandID string, repairID int, repair *asserts.Repair, aux []asserts.Assertion) error { + d := filepath.Join(dirs.SnapRepairAssertsDir, brandID, strconv.Itoa(repairID)) + err := os.MkdirAll(d, 0775) + if err != nil { + return err + } + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + r := append([]asserts.Assertion{repair}, aux...) + for _, a := range r { + if err := enc.Encode(a); err != nil { + return fmt.Errorf("cannot encode repair assertions %s-%d for saving: %v", brandID, repairID, err) + } + } + p := filepath.Join(d, fmt.Sprintf("r%d.repair", r[0].Revision())) + return osutil.AtomicWriteFile(p, buf.Bytes(), 0600, 0) +} + +func (run *Runner) readSavedStream(brandID string, repairID, revision int) (repair *asserts.Repair, aux []asserts.Assertion, err error) { + d := filepath.Join(dirs.SnapRepairAssertsDir, brandID, strconv.Itoa(repairID)) + p := filepath.Join(d, fmt.Sprintf("r%d.repair", revision)) + f, err := os.Open(p) + if err != nil { + return nil, nil, err + } + defer f.Close() + + dec := asserts.NewDecoder(f) + var r []asserts.Assertion + for { + a, err := dec.Decode() + if err == io.EOF { + break + } + if err != nil { + return nil, nil, fmt.Errorf("cannot decode repair assertions %s-%d from disk: %v", brandID, repairID, err) + } + r = append(r, a) + } + return checkStream(brandID, repairID, r) +} + +func (run *Runner) makeReady(brandID string, sequenceNext int) (repair *asserts.Repair, err error) { + sequence := run.state.Sequences[brandID] + var aux []asserts.Assertion + var state RepairState + if sequenceNext <= len(sequence) { + // consider retries + state = *sequence[sequenceNext-1] + if state.Status != RetryStatus { + return nil, errSkip + } + var err error + repair, aux, err = run.refetch(brandID, state.Sequence, state.Revision) + if err != nil { + if err != ErrRepairNotModified { + logger.Noticef("cannot refetch repair %s-%d, will retry what is on disk: %v", brandID, sequenceNext, err) + } + // try to use what we have already on disk + repair, aux, err = run.readSavedStream(brandID, state.Sequence, state.Revision) + if err != nil { + return nil, err + } + } + } else { + // fetch the next repair in the sequence + // assumes no gaps, each repair id is present so far, + // possibly skipped + var err error + repair, aux, err = run.fetch(brandID, sequenceNext) + if err != nil && err != errSkip { + return nil, err + } + state = RepairState{ + Sequence: sequenceNext, + } + if err == errSkip { + // TODO: store headers to justify decision + state.Status = SkipStatus + run.setRepairState(brandID, state) + return nil, errSkip + } + } + // verify with signatures + if err := run.Verify(repair, aux); err != nil { + return nil, fmt.Errorf("cannot verify repair %s-%d: %v", brandID, state.Sequence, err) + } + if err := run.saveStream(brandID, state.Sequence, repair, aux); err != nil { + return nil, err + } + state.Revision = repair.Revision() + if !run.Applicable(repair.Headers()) { + state.Status = SkipStatus + run.setRepairState(brandID, state) + return nil, errSkip + } + run.setRepairState(brandID, state) + return repair, nil +} + +// Next returns the next repair for the brand id sequence to run/retry or ErrRepairNotFound if there is none atm. It updates the state as required. +func (run *Runner) Next(brandID string) (*Repair, error) { + sequenceNext := run.sequenceNext[brandID] + if sequenceNext == 0 { + sequenceNext = 1 + } + for { + repair, err := run.makeReady(brandID, sequenceNext) + // SaveState is a no-op unless makeReady modified the state + stateErr := run.SaveState() + if err != nil && err != errSkip && err != ErrRepairNotFound { + // err is a non trivial error, just log the SaveState error and report err + if stateErr != nil { + logger.Noticef("%v", stateErr) + } + return nil, err + } + if stateErr != nil { + return nil, stateErr + } + if err == ErrRepairNotFound { + return nil, ErrRepairNotFound + } + + sequenceNext += 1 + run.sequenceNext[brandID] = sequenceNext + if err == errSkip { + continue + } + + return &Repair{ + Repair: repair, + run: run, + sequence: sequenceNext - 1, + }, nil + } +} + +// Limit trust to specific keys while there's no delegation or limited +// keys support. The obtained assertion stream may also include +// account keys that are directly or indirectly signed by a trusted +// key. +var ( + trustedRepairRootKeys []*asserts.AccountKey +) + +// Verify verifies that the repair is properly signed by the specific +// trusted root keys or by account keys in the stream (passed via aux) +// directly or indirectly signed by a trusted key. +func (run *Runner) Verify(repair *asserts.Repair, aux []asserts.Assertion) error { + workBS := asserts.NewMemoryBackstore() + for _, a := range aux { + if a.Type() != asserts.AccountKeyType { + continue + } + err := workBS.Put(asserts.AccountKeyType, a) + if err != nil { + return err + } + } + trustedBS := asserts.NewMemoryBackstore() + for _, t := range trustedRepairRootKeys { + trustedBS.Put(asserts.AccountKeyType, t) + } + for _, t := range sysdb.Trusted() { + if t.Type() == asserts.AccountType { + trustedBS.Put(asserts.AccountType, t) + } + } + + return verifySignatures(repair, workBS, trustedBS) +} diff --git a/cmd/snap-repair/runner_test.go b/cmd/snap-repair/runner_test.go new file mode 100644 index 00000000..279854c6 --- /dev/null +++ b/cmd/snap-repair/runner_test.go @@ -0,0 +1,1778 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + . "gopkg.in/check.v1" + "gopkg.in/retry.v1" + + "github.com/snapcore/snapd/arch" + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/sysdb" + repair "github.com/snapcore/snapd/cmd/snap-repair" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/testutil" +) + +type baseRunnerSuite struct { + tmpdir string + + seedTime time.Time + t0 time.Time + + storeSigning *assertstest.StoreStack + + brandSigning *assertstest.SigningDB + brandAcct *asserts.Account + brandAcctKey *asserts.AccountKey + + modelAs *asserts.Model + + seedAssertsDir string + + repairRootAcctKey *asserts.AccountKey + repairsAcctKey *asserts.AccountKey + + repairsSigning *assertstest.SigningDB + + restoreLogger func() +} + +func (s *baseRunnerSuite) SetUpSuite(c *C) { + s.storeSigning = assertstest.NewStoreStack("canonical", nil) + + brandPrivKey, _ := assertstest.GenerateKey(752) + + s.brandAcct = assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{ + "account-id": "my-brand", + }, "") + s.brandAcctKey = assertstest.NewAccountKey(s.storeSigning, s.brandAcct, nil, brandPrivKey.PublicKey(), "") + s.brandSigning = assertstest.NewSigningDB("my-brand", brandPrivKey) + + modelAs, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{ + "series": "16", + "brand-id": "my-brand", + "model": "my-model-2", + "architecture": "armhf", + "gadget": "gadget", + "kernel": "kernel", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + s.modelAs = modelAs.(*asserts.Model) + + repairRootKey, _ := assertstest.GenerateKey(1024) + + s.repairRootAcctKey = assertstest.NewAccountKey(s.storeSigning.RootSigning, s.storeSigning.TrustedAccount, nil, repairRootKey.PublicKey(), "") + + repairsKey, _ := assertstest.GenerateKey(752) + + repairRootSigning := assertstest.NewSigningDB("canonical", repairRootKey) + + s.repairsAcctKey = assertstest.NewAccountKey(repairRootSigning, s.storeSigning.TrustedAccount, nil, repairsKey.PublicKey(), "") + + s.repairsSigning = assertstest.NewSigningDB("canonical", repairsKey) +} + +func (s *baseRunnerSuite) SetUpTest(c *C) { + _, s.restoreLogger = logger.MockLogger() + + s.tmpdir = c.MkDir() + dirs.SetRootDir(s.tmpdir) + + s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "assertions") + + // dummy seed yaml + err := os.MkdirAll(dirs.SnapSeedDir, 0755) + c.Assert(err, IsNil) + seedYamlFn := filepath.Join(dirs.SnapSeedDir, "seed.yaml") + err = ioutil.WriteFile(seedYamlFn, nil, 0644) + c.Assert(err, IsNil) + seedTime, err := time.Parse(time.RFC3339, "2017-08-11T15:49:49Z") + c.Assert(err, IsNil) + err = os.Chtimes(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), seedTime, seedTime) + c.Assert(err, IsNil) + s.seedTime = seedTime + + s.t0 = time.Now().UTC().Truncate(time.Minute) +} + +func (s *baseRunnerSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") + s.restoreLogger() +} + +func (s *baseRunnerSuite) signSeqRepairs(c *C, repairs []string) []string { + var seq []string + for _, rpr := range repairs { + decoded, err := asserts.Decode([]byte(rpr)) + c.Assert(err, IsNil) + signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") + c.Assert(err, IsNil) + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + enc.Encode(signed) + enc.Encode(s.repairsAcctKey) + seq = append(seq, buf.String()) + } + return seq +} + +const freshStateJSON = `{"device":{"brand":"my-brand","model":"my-model"},"time-lower-bound":"2017-08-11T15:49:49Z"}` + +func (s *baseRunnerSuite) freshState(c *C) { + err := os.MkdirAll(dirs.SnapRepairDir, 0775) + c.Assert(err, IsNil) + err = ioutil.WriteFile(dirs.SnapRepairStateFile, []byte(freshStateJSON), 0600) + c.Assert(err, IsNil) +} + +type runnerSuite struct { + baseRunnerSuite + + restore func() +} + +func (s *runnerSuite) SetUpSuite(c *C) { + s.baseRunnerSuite.SetUpSuite(c) + s.restore = httputil.SetUserAgentFromVersion("1", "snap-repair") +} + +func (s *runnerSuite) TearDownSuite(c *C) { + s.restore() +} + +var _ = Suite(&runnerSuite{}) + +var ( + testKey = `type: account-key +authority-id: canonical +account-id: canonical +name: repair +public-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj +since: 2015-11-16T15:04:00Z +body-length: 149 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AcZrBFaFwYABAvCX5A8dTcdLdhdiuy2YRHO5CAfM5InQefkKOhNMUq2yfi3Sk6trUHxskhZkPnm4 +NKx2yRr332q7AJXQHLX+DrZ29ycyoQ2NQGO3eAfQ0hjAAQFYBF8SSh5SutPu5XCVABEBAAE= + +AXNpZw== +` + + testRepair = `type: repair +authority-id: canonical +brand-id: canonical +repair-id: 2 +summary: repair two +architectures: + - amd64 + - arm64 +series: + - 16 +models: + - xyz/frobinator +timestamp: 2017-03-30T12:22:16Z +body-length: 7 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +script + + +AXNpZw== +` + testHeadersResp = `{"headers": +{"architectures":["amd64","arm64"],"authority-id":"canonical","body-length":"7","brand-id":"canonical","models":["xyz/frobinator"],"repair-id":"2","series":["16"],"sign-key-sha3-384":"KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj","timestamp":"2017-03-30T12:22:16Z","type":"repair"}}` +) + +func mustParseURL(s string) *url.URL { + u, err := url.Parse(s) + if err != nil { + panic(err) + } + return u +} + +func (s *runnerSuite) mockBrokenTimeNowSetToEpoch(c *C, runner *repair.Runner) (restore func()) { + epoch := time.Unix(0, 0) + r := repair.MockTimeNow(func() time.Time { + return epoch + }) + c.Check(runner.TLSTime().Equal(epoch), Equals, true) + return r +} + +func (s *runnerSuite) checkBrokenTimeNowMitigated(c *C, runner *repair.Runner) { + c.Check(runner.TLSTime().Before(s.t0), Equals, false) +} + +func (s *runnerSuite) TestFetchJustRepair(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ua := r.Header.Get("User-Agent") + c.Check(strings.Contains(ua, "snap-repair"), Equals, true) + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + c.Check(r.URL.Path, Equals, "/repairs/canonical/2") + io.WriteString(w, testRepair) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + r := s.mockBrokenTimeNowSetToEpoch(c, runner) + defer r() + + repair, aux, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, IsNil) + c.Check(repair, NotNil) + c.Check(aux, HasLen, 0) + c.Check(repair.BrandID(), Equals, "canonical") + c.Check(repair.RepairID(), Equals, 2) + c.Check(repair.Body(), DeepEquals, []byte("script\n")) + + s.checkBrokenTimeNowMitigated(c, runner) +} + +func (s *runnerSuite) TestFetchScriptTooBig(c *C) { + restore := repair.MockMaxRepairScriptSize(4) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + c.Check(r.URL.Path, Equals, "/repairs/canonical/2") + io.WriteString(w, testRepair) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, ErrorMatches, `assertion body length 7 exceeds maximum body size 4 for "repair".*`) + c.Assert(n, Equals, 1) +} + +var ( + testRetryStrategy = retry.LimitCount(5, retry.LimitTime(1*time.Second, + retry.Exponential{ + Initial: 1 * time.Millisecond, + Factor: 1, + }, + )) +) + +func (s *runnerSuite) TestFetch500(c *C) { + restore := repair.MockFetchRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(500) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, ErrorMatches, "cannot fetch repair, unexpected status 500") + c.Assert(n, Equals, 5) +} + +func (s *runnerSuite) TestFetchEmpty(c *C) { + restore := repair.MockFetchRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(200) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, Equals, io.ErrUnexpectedEOF) + c.Assert(n, Equals, 5) +} + +func (s *runnerSuite) TestFetchBroken(c *C) { + restore := repair.MockFetchRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(200) + io.WriteString(w, "xyz:") + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, Equals, io.ErrUnexpectedEOF) + c.Assert(n, Equals, 5) +} + +func (s *runnerSuite) TestFetchNotFound(c *C) { + restore := repair.MockFetchRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(404) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + r := s.mockBrokenTimeNowSetToEpoch(c, runner) + defer r() + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, Equals, repair.ErrRepairNotFound) + c.Assert(n, Equals, 1) + + s.checkBrokenTimeNowMitigated(c, runner) +} + +func (s *runnerSuite) TestFetchIfNoneMatchNotModified(c *C) { + restore := repair.MockFetchRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + c.Check(r.Header.Get("If-None-Match"), Equals, `"0"`) + w.WriteHeader(304) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + r := s.mockBrokenTimeNowSetToEpoch(c, runner) + defer r() + + _, _, err := runner.Fetch("canonical", 2, 0) + c.Assert(err, Equals, repair.ErrRepairNotModified) + c.Assert(n, Equals, 1) + + s.checkBrokenTimeNowMitigated(c, runner) +} + +func (s *runnerSuite) TestFetchIgnoreSupersededRevision(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, testRepair) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, 2) + c.Assert(err, Equals, repair.ErrRepairNotModified) +} + +func (s *runnerSuite) TestFetchIdMismatch(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + io.WriteString(w, testRepair) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 4, -1) + c.Assert(err, ErrorMatches, `cannot fetch repair, repair id mismatch canonical/2 != canonical/4`) +} + +func (s *runnerSuite) TestFetchWrongFirstType(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + c.Check(r.URL.Path, Equals, "/repairs/canonical/2") + io.WriteString(w, testKey) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, ErrorMatches, `cannot fetch repair, unexpected first assertion "account-key"`) +} + +func (s *runnerSuite) TestFetchRepairPlusKey(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + c.Check(r.URL.Path, Equals, "/repairs/canonical/2") + io.WriteString(w, testRepair) + io.WriteString(w, "\n") + io.WriteString(w, testKey) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + repair, aux, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, IsNil) + c.Check(repair, NotNil) + c.Check(aux, HasLen, 1) + _, ok := aux[0].(*asserts.AccountKey) + c.Check(ok, Equals, true) +} + +func (s *runnerSuite) TestPeek(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ua := r.Header.Get("User-Agent") + c.Check(strings.Contains(ua, "snap-repair"), Equals, true) + c.Check(r.Header.Get("Accept"), Equals, "application/json") + c.Check(r.URL.Path, Equals, "/repairs/canonical/2") + io.WriteString(w, testHeadersResp) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + r := s.mockBrokenTimeNowSetToEpoch(c, runner) + defer r() + + h, err := runner.Peek("canonical", 2) + c.Assert(err, IsNil) + c.Check(h["series"], DeepEquals, []interface{}{"16"}) + c.Check(h["architectures"], DeepEquals, []interface{}{"amd64", "arm64"}) + c.Check(h["models"], DeepEquals, []interface{}{"xyz/frobinator"}) + + s.checkBrokenTimeNowMitigated(c, runner) +} + +func (s *runnerSuite) TestPeek500(c *C) { + restore := repair.MockPeekRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(500) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, err := runner.Peek("canonical", 2) + c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500") + c.Assert(n, Equals, 5) +} + +func (s *runnerSuite) TestPeekInvalid(c *C) { + restore := repair.MockPeekRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(200) + io.WriteString(w, "{") + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, err := runner.Peek("canonical", 2) + c.Assert(err, Equals, io.ErrUnexpectedEOF) + c.Assert(n, Equals, 5) +} + +func (s *runnerSuite) TestPeekNotFound(c *C) { + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(404) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + r := s.mockBrokenTimeNowSetToEpoch(c, runner) + defer r() + + _, err := runner.Peek("canonical", 2) + c.Assert(err, Equals, repair.ErrRepairNotFound) + c.Assert(n, Equals, 1) + + s.checkBrokenTimeNowMitigated(c, runner) +} + +func (s *runnerSuite) TestPeekIdMismatch(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Accept"), Equals, "application/json") + io.WriteString(w, testHeadersResp) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, err := runner.Peek("canonical", 4) + c.Assert(err, ErrorMatches, `cannot peek repair headers, repair id mismatch canonical/2 != canonical/4`) +} + +func (s *runnerSuite) TestLoadState(c *C) { + s.freshState(c) + + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + brand, model := runner.BrandModel() + c.Check(brand, Equals, "my-brand") + c.Check(model, Equals, "my-model") +} + +func (s *runnerSuite) initSeed(c *C) { + err := os.MkdirAll(s.seedAssertsDir, 0775) + c.Assert(err, IsNil) +} + +func (s *runnerSuite) writeSeedAssert(c *C, fname string, a asserts.Assertion) { + err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, fname), asserts.Encode(a), 0644) + c.Assert(err, IsNil) +} + +func (s *runnerSuite) rmSeedAssert(c *C, fname string) { + err := os.Remove(filepath.Join(s.seedAssertsDir, fname)) + c.Assert(err, IsNil) +} + +func (s *runnerSuite) TestLoadStateInitState(c *C) { + // sanity + c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false) + c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false) + // setup realistic seed/assertions + r := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r() + s.initSeed(c) + s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey("")) + s.writeSeedAssert(c, "brand.account", s.brandAcct) + s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey) + s.writeSeedAssert(c, "model", s.modelAs) + + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, true) + + brand, model := runner.BrandModel() + c.Check(brand, Equals, "my-brand") + c.Check(model, Equals, "my-model-2") + + c.Check(runner.TimeLowerBound().Equal(s.seedTime), Equals, true) +} + +func (s *runnerSuite) TestLoadStateInitDeviceInfoFail(c *C) { + // sanity + c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false) + c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false) + // setup realistic seed/assertions + r := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r() + s.initSeed(c) + + const errPrefix = "cannot set device information: " + tests := []struct { + breakFunc func() + expectedErr string + }{ + {func() { s.rmSeedAssert(c, "model") }, errPrefix + "no model assertion in seed data"}, + {func() { s.rmSeedAssert(c, "brand.account") }, errPrefix + "no brand account assertion in seed data"}, + {func() { s.rmSeedAssert(c, "brand.account-key") }, errPrefix + `cannot find public key.*`}, + {func() { + // broken signature + blob := asserts.Encode(s.brandAcct) + err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, "brand.account"), blob[:len(blob)-3], 0644) + c.Assert(err, IsNil) + }, errPrefix + "cannot decode signature:.*"}, + {func() { s.writeSeedAssert(c, "model2", s.modelAs) }, errPrefix + "multiple models in seed assertions"}, + } + + for _, test := range tests { + s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey("")) + s.writeSeedAssert(c, "brand.account", s.brandAcct) + s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey) + s.writeSeedAssert(c, "model", s.modelAs) + + test.breakFunc() + + runner := repair.NewRunner() + err := runner.LoadState() + c.Check(err, ErrorMatches, test.expectedErr) + } +} + +func (s *runnerSuite) TestTLSTime(c *C) { + s.freshState(c) + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + epoch := time.Unix(0, 0) + r := repair.MockTimeNow(func() time.Time { + return epoch + }) + defer r() + c.Check(runner.TLSTime().Equal(s.seedTime), Equals, true) +} + +func makeReadOnly(c *C, dir string) (restore func()) { + // skip tests that need this because uid==0 does not honor + // write permissions in directories (yay, unix) + if os.Getuid() == 0 { + // FIXME: we could use osutil.Chattr() here + c.Skip("too lazy to make path readonly as root") + } + err := os.Chmod(dir, 0555) + c.Assert(err, IsNil) + return func() { + err := os.Chmod(dir, 0755) + c.Assert(err, IsNil) + } +} + +func (s *runnerSuite) TestLoadStateInitStateFail(c *C) { + restore := makeReadOnly(c, filepath.Dir(dirs.SnapSeedDir)) + defer restore() + + runner := repair.NewRunner() + err := runner.LoadState() + c.Check(err, ErrorMatches, `cannot create repair state directory:.*`) +} + +func (s *runnerSuite) TestSaveStateFail(c *C) { + s.freshState(c) + + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + + restore := makeReadOnly(c, dirs.SnapRepairDir) + defer restore() + + // no error because this is a no-op + err = runner.SaveState() + c.Check(err, IsNil) + + // mark as modified + runner.SetStateModified(true) + + err = runner.SaveState() + c.Check(err, ErrorMatches, `cannot save repair state:.*`) +} + +func (s *runnerSuite) TestSaveState(c *C) { + s.freshState(c) + + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + + runner.SetSequence("canonical", []*repair.RepairState{ + {Sequence: 1, Revision: 3}, + }) + // mark as modified + runner.SetStateModified(true) + + err = runner.SaveState() + c.Assert(err, IsNil) + + c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, `{"device":{"brand":"my-brand","model":"my-model"},"sequences":{"canonical":[{"sequence":1,"revision":3,"status":0}]},"time-lower-bound":"2017-08-11T15:49:49Z"}`) +} + +func (s *runnerSuite) TestApplicable(c *C) { + s.freshState(c) + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + + scenarios := []struct { + headers map[string]interface{} + applicable bool + }{ + {nil, true}, + {map[string]interface{}{"series": []interface{}{"18"}}, false}, + {map[string]interface{}{"series": []interface{}{"18", "16"}}, true}, + {map[string]interface{}{"series": "18"}, false}, + {map[string]interface{}{"series": []interface{}{18}}, false}, + {map[string]interface{}{"architectures": []interface{}{arch.UbuntuArchitecture()}}, true}, + {map[string]interface{}{"architectures": []interface{}{"other-arch"}}, false}, + {map[string]interface{}{"architectures": []interface{}{"other-arch", arch.UbuntuArchitecture()}}, true}, + {map[string]interface{}{"architectures": arch.UbuntuArchitecture()}, false}, + {map[string]interface{}{"models": []interface{}{"my-brand/my-model"}}, true}, + {map[string]interface{}{"models": []interface{}{"other-brand/other-model"}}, false}, + {map[string]interface{}{"models": []interface{}{"other-brand/other-model", "my-brand/my-model"}}, true}, + {map[string]interface{}{"models": "my-brand/my-model"}, false}, + // model prefix matches + {map[string]interface{}{"models": []interface{}{"my-brand/*"}}, true}, + {map[string]interface{}{"models": []interface{}{"my-brand/my-mod*"}}, true}, + {map[string]interface{}{"models": []interface{}{"my-brand/xxx*"}}, false}, + {map[string]interface{}{"models": []interface{}{"my-brand/my-mod*", "my-brand/xxx*"}}, true}, + {map[string]interface{}{"models": []interface{}{"my*"}}, false}, + {map[string]interface{}{"disabled": "true"}, false}, + {map[string]interface{}{"disabled": "false"}, true}, + } + + for _, scen := range scenarios { + ok := runner.Applicable(scen.headers) + c.Check(ok, Equals, scen.applicable, Commentf("%v", scen)) + } +} + +var ( + nextRepairs = []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +timestamp: 2017-07-01T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptA + + +AXNpZw==`, + `type: repair +authority-id: canonical +brand-id: canonical +repair-id: 2 +summary: repair two +series: + - 33 +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`, + `type: repair +revision: 2 +authority-id: canonical +brand-id: canonical +repair-id: 3 +summary: repair three rev2 +series: + - 16 +timestamp: 2017-07-03T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptC + + +AXNpZw== +`} + + repair3Rev4 = `type: repair +revision: 4 +authority-id: canonical +brand-id: canonical +repair-id: 3 +summary: repair three rev4 +series: + - 16 +timestamp: 2017-07-03T12:00:00Z +body-length: 9 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptC2 + + +AXNpZw== +` + + repair4 = `type: repair +authority-id: canonical +brand-id: canonical +repair-id: 4 +summary: repair four +timestamp: 2017-07-03T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptD + + +AXNpZw== +` +) + +func makeMockServer(c *C, seqRepairs *[]string, redirectFirst bool) *httptest.Server { + var mockServer *httptest.Server + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ua := r.Header.Get("User-Agent") + c.Check(strings.Contains(ua, "snap-repair"), Equals, true) + + urlPath := r.URL.Path + if redirectFirst && r.Header.Get("Accept") == asserts.MediaType { + if !strings.HasPrefix(urlPath, "/final/") { + // redirect + finalURL := mockServer.URL + "/final" + r.URL.Path + w.Header().Set("Location", finalURL) + w.WriteHeader(302) + return + } + urlPath = strings.TrimPrefix(urlPath, "/final") + } + + c.Check(strings.HasPrefix(urlPath, "/repairs/canonical/"), Equals, true) + + seq, err := strconv.Atoi(strings.TrimPrefix(urlPath, "/repairs/canonical/")) + c.Assert(err, IsNil) + + if seq > len(*seqRepairs) { + w.WriteHeader(404) + return + } + + rpr := []byte((*seqRepairs)[seq-1]) + dec := asserts.NewDecoder(bytes.NewBuffer(rpr)) + repair, err := dec.Decode() + c.Assert(err, IsNil) + + switch r.Header.Get("Accept") { + case "application/json": + b, err := json.Marshal(map[string]interface{}{ + "headers": repair.Headers(), + }) + c.Assert(err, IsNil) + w.Write(b) + case asserts.MediaType: + etag := fmt.Sprintf(`"%d"`, repair.Revision()) + if strings.Contains(r.Header.Get("If-None-Match"), etag) { + w.WriteHeader(304) + return + } + w.Write(rpr) + } + })) + + c.Assert(mockServer, NotNil) + + return mockServer +} + +func (s *runnerSuite) TestTrustedRepairRootKeys(c *C) { + acctKeys := repair.TrustedRepairRootKeys() + c.Check(acctKeys, HasLen, 1) + c.Check(acctKeys[0].AccountID(), Equals, "canonical") + c.Check(acctKeys[0].PublicKeyID(), Equals, "nttW6NfBXI_E-00u38W-KH6eiksfQNXuI7IiumoV49_zkbhM0sYTzSnFlwZC-W4t") +} + +func (s *runnerSuite) TestVerify(c *C) { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + runner := repair.NewRunner() + + a, err := s.repairsSigning.Sign(asserts.RepairType, map[string]interface{}{ + "brand-id": "canonical", + "repair-id": "2", + "summary": "repair two", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, []byte("#script"), "") + c.Assert(err, IsNil) + rpr := a.(*asserts.Repair) + + err = runner.Verify(rpr, []asserts.Assertion{s.repairsAcctKey}) + c.Check(err, IsNil) +} + +func (s *runnerSuite) signSeqRepairs(c *C, repairs []string) []string { + var seq []string + for _, rpr := range repairs { + decoded, err := asserts.Decode([]byte(rpr)) + c.Assert(err, IsNil) + signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") + c.Assert(err, IsNil) + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + enc.Encode(signed) + enc.Encode(s.repairsAcctKey) + seq = append(seq, buf.String()) + } + return seq +} + +func (s *runnerSuite) loadSequences(c *C) map[string][]*repair.RepairState { + data, err := ioutil.ReadFile(dirs.SnapRepairStateFile) + c.Assert(err, IsNil) + var x struct { + Sequences map[string][]*repair.RepairState `json:"sequences"` + } + err = json.Unmarshal(data, &x) + c.Assert(err, IsNil) + return x.Sequences +} + +func (s *runnerSuite) testNext(c *C, redirectFirst bool) { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + seqRepairs := s.signSeqRepairs(c, nextRepairs) + + mockServer := makeMockServer(c, &seqRepairs, redirectFirst) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + rpr, err := runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 1) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "1", "r0.repair")), Equals, true) + + rpr, err = runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 3) + c.Check(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "3", "r2.repair"), testutil.FileEquals, seqRepairs[2]) + + // no more + rpr, err = runner.Next("canonical") + c.Check(err, Equals, repair.ErrRepairNotFound) + + expectedSeq := []*repair.RepairState{ + {Sequence: 1}, + {Sequence: 2, Status: repair.SkipStatus}, + {Sequence: 3, Revision: 2}, + } + c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) + // on disk + seqs := s.loadSequences(c) + c.Check(seqs["canonical"], DeepEquals, expectedSeq) + + // start fresh run with new runner + // will refetch repair 3 + signed := s.signSeqRepairs(c, []string{repair3Rev4, repair4}) + seqRepairs[2] = signed[0] + seqRepairs = append(seqRepairs, signed[1]) + + runner = repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + rpr, err = runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 1) + + rpr, err = runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 3) + // refetched new revision! + c.Check(rpr.Revision(), Equals, 4) + c.Check(rpr.Body(), DeepEquals, []byte("scriptC2\n")) + + // new repair + rpr, err = runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 4) + c.Check(rpr.Body(), DeepEquals, []byte("scriptD\n")) + + // no more + rpr, err = runner.Next("canonical") + c.Check(err, Equals, repair.ErrRepairNotFound) + + c.Check(runner.Sequence("canonical"), DeepEquals, []*repair.RepairState{ + {Sequence: 1}, + {Sequence: 2, Status: repair.SkipStatus}, + {Sequence: 3, Revision: 4}, + {Sequence: 4}, + }) +} + +func (s *runnerSuite) TestNext(c *C) { + redirectFirst := false + s.testNext(c, redirectFirst) +} + +func (s *runnerSuite) TestNextRedirect(c *C) { + redirectFirst := true + s.testNext(c, redirectFirst) +} + +func (s *runnerSuite) TestNextImmediateSkip(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +series: + - 33 +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`} + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + // not applicable => not returned + _, err := runner.Next("canonical") + c.Check(err, Equals, repair.ErrRepairNotFound) + + expectedSeq := []*repair.RepairState{ + {Sequence: 1, Status: repair.SkipStatus}, + } + c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) + // on disk + seqs := s.loadSequences(c) + c.Check(seqs["canonical"], DeepEquals, expectedSeq) +} + +func (s *runnerSuite) TestNextRefetchSkip(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +series: + - 16 +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`} + + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + seqRepairs = s.signSeqRepairs(c, seqRepairs) + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + _, err := runner.Next("canonical") + c.Assert(err, IsNil) + + expectedSeq := []*repair.RepairState{ + {Sequence: 1}, + } + c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) + // on disk + seqs := s.loadSequences(c) + c.Check(seqs["canonical"], DeepEquals, expectedSeq) + + // new fresh run, repair becomes now unapplicable + seqRepairs[0] = `type: repair +authority-id: canonical +revision: 1 +brand-id: canonical +repair-id: 1 +summary: repair one rev1 +series: + - 16 +disabled: true +timestamp: 2017-07-02T12:00:00Z +body-length: 7 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptX + +AXNpZw==` + + seqRepairs = s.signSeqRepairs(c, seqRepairs) + + runner = repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + _, err = runner.Next("canonical") + c.Check(err, Equals, repair.ErrRepairNotFound) + + expectedSeq = []*repair.RepairState{ + {Sequence: 1, Revision: 1, Status: repair.SkipStatus}, + } + c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) + // on disk + seqs = s.loadSequences(c) + c.Check(seqs["canonical"], DeepEquals, expectedSeq) +} + +func (s *runnerSuite) TestNext500(c *C) { + restore := repair.MockPeekRetryStrategy(testRetryStrategy) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + _, err := runner.Next("canonical") + c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500") +} + +func (s *runnerSuite) TestNextNotFound(c *C) { + s.freshState(c) + + restore := repair.MockPeekRetryStrategy(testRetryStrategy) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + // sanity + c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, freshStateJSON) + + _, err := runner.Next("canonical") + c.Assert(err, Equals, repair.ErrRepairNotFound) + + // we saved new time lower bound + t1 := runner.TimeLowerBound() + expected := strings.Replace(freshStateJSON, "2017-08-11T15:49:49Z", t1.Format(time.RFC3339), 1) + c.Check(expected, Not(Equals), freshStateJSON) + c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, expected) +} + +func (s *runnerSuite) TestNextSaveStateError(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +series: + - 33 +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`} + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + // break SaveState + restore := makeReadOnly(c, dirs.SnapRepairDir) + defer restore() + + _, err := runner.Next("canonical") + c.Check(err, ErrorMatches, `cannot save repair state:.*`) +} + +func (s *runnerSuite) TestNextVerifyNoKey(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`} + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + _, err := runner.Next("canonical") + c.Check(err, ErrorMatches, `cannot verify repair canonical-1: cannot find public key.*`) + + c.Check(runner.Sequence("canonical"), HasLen, 0) +} + +func (s *runnerSuite) TestNextVerifySelfSigned(c *C) { + randoKey, _ := assertstest.GenerateKey(752) + + randomSigning := assertstest.NewSigningDB("canonical", randoKey) + randoKeyEncoded, err := asserts.EncodePublicKey(randoKey.PublicKey()) + c.Assert(err, IsNil) + acctKey, err := randomSigning.Sign(asserts.AccountKeyType, map[string]interface{}{ + "authority-id": "canonical", + "account-id": "canonical", + "public-key-sha3-384": randoKey.PublicKey().ID(), + "name": "repairs", + "since": time.Now().UTC().Format(time.RFC3339), + }, randoKeyEncoded, "") + c.Assert(err, IsNil) + + rpr, err := randomSigning.Sign(asserts.RepairType, map[string]interface{}{ + "brand-id": "canonical", + "repair-id": "1", + "summary": "repair one", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, []byte("scriptB\n"), "") + c.Assert(err, IsNil) + + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + enc.Encode(rpr) + enc.Encode(acctKey) + seqRepairs := []string{buf.String()} + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + _, err = runner.Next("canonical") + c.Check(err, ErrorMatches, `cannot verify repair canonical-1: circular assertions`) + + c.Check(runner.Sequence("canonical"), HasLen, 0) +} + +func (s *runnerSuite) TestNextVerifyAllKeysOK(c *C) { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + decoded, err := asserts.Decode([]byte(nextRepairs[0])) + c.Assert(err, IsNil) + signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") + c.Assert(err, IsNil) + + // stream with all keys (any order) works as well + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + enc.Encode(signed) + enc.Encode(s.storeSigning.TrustedKey) + enc.Encode(s.repairRootAcctKey) + enc.Encode(s.repairsAcctKey) + seqRepairs := []string{buf.String()} + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + rpr, err := runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 1) +} + +func (s *runnerSuite) TestRepairSetStatus(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`} + + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + seqRepairs = s.signSeqRepairs(c, seqRepairs) + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + rpr, err := runner.Next("canonical") + c.Assert(err, IsNil) + + rpr.SetStatus(repair.DoneStatus) + + expectedSeq := []*repair.RepairState{ + {Sequence: 1, Status: repair.DoneStatus}, + } + c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) + // on disk + seqs := s.loadSequences(c) + c.Check(seqs["canonical"], DeepEquals, expectedSeq) +} + +func (s *runnerSuite) TestRepairBasicRun(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +series: + - 16 +timestamp: 2017-07-02T12:00:00Z +body-length: 7 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +exit 0 + + +AXNpZw==`} + + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + seqRepairs = s.signSeqRepairs(c, seqRepairs) + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + rpr, err := runner.Next("canonical") + c.Assert(err, IsNil) + + rpr.Run() + c.Check(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.script"), testutil.FileEquals, "exit 0\n") +} + +func makeMockRepair(script string) string { + return fmt.Sprintf(`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +series: + - 16 +timestamp: 2017-07-02T12:00:00Z +body-length: %d +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +%s + +AXNpZw==`, len(script), script) +} + +func verifyRepairStatus(c *C, status repair.RepairStatus) { + c.Check(dirs.SnapRepairStateFile, testutil.FileContains, fmt.Sprintf(`{"device":{"brand":"","model":""},"sequences":{"canonical":[{"sequence":1,"revision":0,"status":%d}`, status)) +} + +// tests related to correct execution of script +type runScriptSuite struct { + baseRunnerSuite + + seqRepairs []string + + mockServer *httptest.Server + runner *repair.Runner + + runDir string + + restoreErrTrackerReportRepair func() + errReport struct { + repair string + errMsg string + dupSig string + extra map[string]string + } +} + +var _ = Suite(&runScriptSuite{}) + +func (s *runScriptSuite) SetUpTest(c *C) { + s.baseRunnerSuite.SetUpTest(c) + + s.mockServer = makeMockServer(c, &s.seqRepairs, false) + + s.runner = repair.NewRunner() + s.runner.BaseURL = mustParseURL(s.mockServer.URL) + s.runner.LoadState() + + s.runDir = filepath.Join(dirs.SnapRepairRunDir, "canonical", "1") + + s.restoreErrTrackerReportRepair = repair.MockErrtrackerReportRepair(s.errtrackerReportRepair) +} + +func (s *runScriptSuite) TearDownTest(c *C) { + s.baseRunnerSuite.TearDownTest(c) + + s.restoreErrTrackerReportRepair() + s.mockServer.Close() +} + +func (s *runScriptSuite) errtrackerReportRepair(repair, errMsg, dupSig string, extra map[string]string) (string, error) { + s.errReport.repair = repair + s.errReport.errMsg = errMsg + s.errReport.dupSig = dupSig + s.errReport.extra = extra + + return "some-oops-id", nil +} + +func (s *runScriptSuite) testScriptRun(c *C, mockScript string) *repair.Repair { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) + + rpr, err := s.runner.Next("canonical") + c.Assert(err, IsNil) + + err = rpr.Run() + c.Assert(err, IsNil) + + c.Check(filepath.Join(s.runDir, "r0.script"), testutil.FileEquals, mockScript) + + return rpr +} + +func (s *runScriptSuite) verifyRundir(c *C, names []string) { + dirents, err := ioutil.ReadDir(s.runDir) + c.Assert(err, IsNil) + c.Assert(dirents, HasLen, len(names)) + for i := range dirents { + c.Check(dirents[i].Name(), Matches, names[i]) + } +} + +type byMtime []os.FileInfo + +func (m byMtime) Len() int { return len(m) } +func (m byMtime) Less(i, j int) bool { return m[i].ModTime().Before(m[j].ModTime()) } +func (m byMtime) Swap(i, j int) { m[i], m[j] = m[j], m[i] } + +func (s *runScriptSuite) verifyOutput(c *C, name, expectedOutput string) { + c.Check(filepath.Join(s.runDir, name), testutil.FileEquals, expectedOutput) + // ensure correct permissions + fi, err := os.Stat(filepath.Join(s.runDir, name)) + c.Assert(err, IsNil) + c.Check(fi.Mode(), Equals, os.FileMode(0600)) +} + +func (s *runScriptSuite) TestRepairBasicRunHappy(c *C) { + script := `#!/bin/sh +echo "happy output" +echo "done" >&$SNAP_REPAIR_STATUS_FD +exit 0 +` + s.seqRepairs = []string{makeMockRepair(script)} + s.testScriptRun(c, script) + // verify + s.verifyRundir(c, []string{ + `^r0.done$`, + `^r0.script$`, + `^work$`, + }) + s.verifyOutput(c, "r0.done", `repair: canonical-1 +revision: 0 +summary: repair one +output: +happy output +`) + verifyRepairStatus(c, repair.DoneStatus) +} + +func (s *runScriptSuite) TestRepairBasicRunUnhappy(c *C) { + script := `#!/bin/sh +echo "unhappy output" +exit 1 +` + s.seqRepairs = []string{makeMockRepair(script)} + s.testScriptRun(c, script) + // verify + s.verifyRundir(c, []string{ + `^r0.retry$`, + `^r0.script$`, + `^work$`, + }) + s.verifyOutput(c, "r0.retry", `repair: canonical-1 +revision: 0 +summary: repair one +output: +unhappy output + +repair canonical-1 revision 0 failed: exit status 1`) + verifyRepairStatus(c, repair.RetryStatus) + + c.Check(s.errReport.repair, Equals, "canonical/1") + c.Check(s.errReport.errMsg, Equals, `repair canonical-1 revision 0 failed: exit status 1`) + c.Check(s.errReport.dupSig, Equals, `canonical/1 +repair canonical-1 revision 0 failed: exit status 1 +output: +repair: canonical-1 +revision: 0 +summary: repair one +output: +unhappy output +`) + c.Check(s.errReport.extra, DeepEquals, map[string]string{ + "Revision": "0", + "RepairID": "1", + "BrandID": "canonical", + "Status": "retry", + }) +} + +func (s *runScriptSuite) TestRepairBasicSkip(c *C) { + script := `#!/bin/sh +echo "other output" +echo "skip" >&$SNAP_REPAIR_STATUS_FD +exit 0 +` + s.seqRepairs = []string{makeMockRepair(script)} + s.testScriptRun(c, script) + // verify + s.verifyRundir(c, []string{ + `^r0.script$`, + `^r0.skip$`, + `^work$`, + }) + s.verifyOutput(c, "r0.skip", `repair: canonical-1 +revision: 0 +summary: repair one +output: +other output +`) + verifyRepairStatus(c, repair.SkipStatus) +} + +func (s *runScriptSuite) TestRepairBasicRunUnhappyThenHappy(c *C) { + script := `#!/bin/sh +if [ -f zzz-ran-once ]; then + echo "happy now" + echo "done" >&$SNAP_REPAIR_STATUS_FD + exit 0 +fi +echo "unhappy output" +touch zzz-ran-once +exit 1 +` + s.seqRepairs = []string{makeMockRepair(script)} + rpr := s.testScriptRun(c, script) + s.verifyRundir(c, []string{ + `^r0.retry$`, + `^r0.script$`, + `^work$`, + }) + s.verifyOutput(c, "r0.retry", `repair: canonical-1 +revision: 0 +summary: repair one +output: +unhappy output + +repair canonical-1 revision 0 failed: exit status 1`) + verifyRepairStatus(c, repair.RetryStatus) + + // run again, it will be happy this time + err := rpr.Run() + c.Assert(err, IsNil) + + s.verifyRundir(c, []string{ + `^r0.done$`, + `^r0.retry$`, + `^r0.script$`, + `^work$`, + }) + s.verifyOutput(c, "r0.done", `repair: canonical-1 +revision: 0 +summary: repair one +output: +happy now +`) + verifyRepairStatus(c, repair.DoneStatus) +} + +func (s *runScriptSuite) TestRepairHitsTimeout(c *C) { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + restore := repair.MockDefaultRepairTimeout(100 * time.Millisecond) + defer restore() + + script := `#!/bin/sh +echo "output before timeout" +sleep 100 +` + s.seqRepairs = []string{makeMockRepair(script)} + s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) + + rpr, err := s.runner.Next("canonical") + c.Assert(err, IsNil) + + err = rpr.Run() + c.Assert(err, IsNil) + + s.verifyRundir(c, []string{ + `^r0.retry$`, + `^r0.script$`, + `^work$`, + }) + s.verifyOutput(c, "r0.retry", `repair: canonical-1 +revision: 0 +summary: repair one +output: +output before timeout + +repair canonical-1 revision 0 failed: repair did not finish within 100ms`) + verifyRepairStatus(c, repair.RetryStatus) +} + +func (s *runScriptSuite) TestRepairHasCorrectPath(c *C) { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + script := `#!/bin/sh +echo PATH=$PATH +ls -l ${PATH##*:}/repair +` + s.seqRepairs = []string{makeMockRepair(script)} + s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) + + rpr, err := s.runner.Next("canonical") + c.Assert(err, IsNil) + + err = rpr.Run() + c.Assert(err, IsNil) + + c.Check(filepath.Join(s.runDir, "r0.retry"), testutil.FileMatches, fmt.Sprintf(`(?ms).*^PATH=.*:.*/run/snapd/repair/tools.*`)) + c.Check(filepath.Join(s.runDir, "r0.retry"), testutil.FileContains, `/repair -> /usr/lib/snapd/snap-repair`) + + // run again and ensure no error happens + err = rpr.Run() + c.Assert(err, IsNil) + +} diff --git a/cmd/snap-repair/staging.go b/cmd/snap-repair/staging.go new file mode 100644 index 00000000..629dd364 --- /dev/null +++ b/cmd/snap-repair/staging.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build withtestkeys withstagingkeys + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/osutil" +) + +const ( + encodedStagingRepairRootAccountKey = `type: account-key +authority-id: canonical +public-key-sha3-384: 0GgXgD-RtfU0HJFBmaaiUQaNUFATl1oOlzJ44Bi4MbQAwcu8ektQLGBkKQ4JuA_O +account-id: canonical +name: repair-root +since: 2017-07-07T00:00:00.0Z +body-length: 1406 +sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu + +AcbDTQRWhcGAASAA0D+AqdqBtgiaABSZ++NdDKcvFtpmEphDfpEAAo4MAWucmE5yVN9mHFRXvwU0 +ZkLQwPuiF5Vp5EP/kHyKgmmF9nKUBXnuZXfuH4vzH9ZuEfdxGc0On+XK4KwyPUzOXj2n1Rsxj0P5 +06wJ6QFghi6nORx3Hz4pxZH7SRANgZudwWE53+whbkJyU/psv4RXfxPnu3YGo0qPk/wGCfV8kkkH +UFmeqJJEk14EGI+Kv9WlIVAqctryqf9mSXkgnhp4lzdGpsRCmRcw7boVjdOieFCJv+gVs7i52Yxu +YyiA09XF8j85DSMl4s/TyN4bm9649beBCpTJNyb3Ep6lydGiZck8ChJRLDXGP2XZtRKXsBMNIfwP +5qnLtX1/Sm1znag0grGbd3AUqL6ySAr42x8QIxZfqzk5DvQbF3xOiu2xzxTt1eB3069MlnFw99ui +fLwlec7imbCiX3bryutCRhKgkJz4MbNsyHiW51k1l1IDbABey+gfVHpeBq/2aHK5qSy98dRBSYSj +Ki++j8zR1ODequWy+OrF4cu6IQ35eunHQ2mRsJIE0xFGAjG3vCPJwzVoNS5m5R0ffncIUYdKxt/R +W2mLo43qX0fquW5LvHyI14d3B3LYfKz05FmASJaE4A+/GQhM7kMCnmykro0MM6MU0sd2OruOZVVo +z6GQ37Hyo/TGToyCr7qD/+tSq3dKYGyezl/Y4I589eqnc1DaMHL2ssiXDsbpSRHpnNqMUq6UNg4M +NsUiDLaGsNJj1ft6A7jz+yoqJ74m3hlaQK1Rot8FXBkuJGoRKBahHbh/bfkGWChDvzY9ZpoUExY2 +rp7tNYEP/LEAI/RQd03sBnqd3V8YhggT2n6QCC4ikLKvUTE3RY0qn5aAa7KC1wVi+7SfdeVl3RBF +Jyb9GCYfRUv/bfFH/TCZ9WVN1v/GIMcjBFGJf7H3cz/deela53XSaecYHuvFRpVfzmx28UcR8UY4 +5WqHfxnVQlY6DPv+kjzMzIEJGwgSAFc0d4wlSwS/Y1T0ednFRUyjMAxEUvE8tOLibtXw4q/srIFt +OIgpd/xErcyi5Ddgt7EQoYo+rtVZ8x5EwR0+i7VAV+a3bnGSJW2LFEjt2RZUiMjohVZ4oOVuoDd2 +VQzMFv41flbyqjgHhtJSCIOKDg9uI2FHbQ5vrX9qBooS68YkBALwCq+P7nSxDxFuS0CgrzSH35FX +VneOl68U74pxRgdlPJ0HI92oilrbTH8Ft0m5SzNsy+9ZZZtIDFQW+lx/ApixyifARFnZ3C3Gdx59 +FlFNbE75+X28joGtul2mPjJ1eI1dCwiFCF3R/rwfRmw3Wpv76re+EzVR1MJVCcTgC1lUoCJpKl1J +n3PQLcR8J0iqswARAQAB + +AcLBXAQAAQoABgUCWYM7bQAKCRAHKljtl9kuLtCFD/4miBm0HyLE8GdboeUtWw+oOlH0AgabRqYi +a1TpEJeYQIjnwDuCCPYtJxL1Rc+5dSNnbY9L+34NuaSyYMJY/FMuSS5iaNomGnj7YiAOds1+1/6h +Z1bTm3ttZnphg5DxckYZLaKoYgRaOzAbiRM8l+2bDbXlq3KRxZ7o7D1V/xpPis8SWK57gQ7VppHI +fcw5jnzWokWSowaKShimjJNCXMaeGdGJBLU1wcJC/XRf3tXSZecwMfL9CN/G8b17HvIFN/Pe3oS9 +QxYMQ0p3J3PF3F19Iow0VHi78hPKtVmJb5igwzBlGYFW7zZ3R35nJ7Iv6VW58G2HDDGMdBfZp930 +FbLb3mj8Yw3S5fcMZ09vpT7PK0tjFoVJtDFBOkrjvxVMEPRa0IJNcfl/hgPdp1/IFXWpZhfvk8a8 +qgzffxN+Ro/J4Jt9QrHM4sNwiEOjVvHY4cQ9GOfns9UqocmxYPDxElBNraCFOCSudZgXiyF7zUYF +OnYqTDR4ChiZtmUqIiZr6rXgZTm1raGlqR7nsbDlkJtru7tzkgMRw8xFRolaQIKiyAwTewF7vLho +imwYTRuYRMzft1q5EeRWR4XwtlIuqsXg3FCGTNIG4HiAFKrrNV7AOvVjIUSgpOcWv2leSiRQjgpY +I9oD82ii+5rKvebnGIa0o+sWhYNFoviP/49DnDNJWA== +` +) + +func init() { + repairRootAccountKey, err := asserts.Decode([]byte(encodedStagingRepairRootAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted account-key: %v", err)) + } + if osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + trustedRepairRootKeys = append(trustedRepairRootKeys, repairRootAccountKey.(*asserts.AccountKey)) + } +} diff --git a/cmd/snap-repair/trace.go b/cmd/snap-repair/trace.go new file mode 100644 index 00000000..9724f2ef --- /dev/null +++ b/cmd/snap-repair/trace.go @@ -0,0 +1,176 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/snapcore/snapd/dirs" +) + +// newRepairTraces returns all repairTrace about the given "brand" and "seq" +// that can be found. brand, seq can be filepath.Glob expressions. +func newRepairTraces(brand, seq string) ([]*repairTrace, error) { + matches, err := filepath.Glob(filepath.Join(dirs.SnapRepairRunDir, brand, seq, "*")) + if err != nil { + return nil, err + } + + var repairTraces []*repairTrace + for _, match := range matches { + if trace := newRepairTraceFromPath(match); trace != nil { + repairTraces = append(repairTraces, trace) + } + } + + return repairTraces, nil +} + +// repairTrace holds information about a repair that was run. +type repairTrace struct { + path string +} + +// validRepairTraceName checks that the given name looks like a valid repair +// trace +var validRepairTraceName = regexp.MustCompile(`^r[0-9]+\.(done|skip|retry|running)$`) + +// newRepairTraceFromPath takes a repair log path like +// the path /var/lib/snapd/repair/run/my-brand/1/r2.done +// and contructs a repair log from that. +func newRepairTraceFromPath(path string) *repairTrace { + rt := &repairTrace{path: path} + if !validRepairTraceName.MatchString(filepath.Base(path)) { + return nil + } + return rt +} + +// Repair returns the repair human readable string in the form $brand-$id +func (rt *repairTrace) Repair() string { + seq := filepath.Base(filepath.Dir(rt.path)) + brand := filepath.Base(filepath.Dir(filepath.Dir(rt.path))) + + return fmt.Sprintf("%s-%s", brand, seq) +} + +// Revision returns the revision of the repair +func (rt *repairTrace) Revision() string { + rev, err := revFromFilepath(rt.path) + if err != nil { + // this can never happen because we check that path starts + // with the right prefix. However handle the case just in + // case. + return "-" + } + return rev +} + +// Summary returns the summary of the repair that was run +func (rt *repairTrace) Summary() string { + f, err := os.Open(rt.path) + if err != nil { + return "-" + } + defer f.Close() + + needle := "summary: " + scanner := bufio.NewScanner(f) + for scanner.Scan() { + s := scanner.Text() + if strings.HasPrefix(s, needle) { + return s[len(needle):] + } + } + + return "-" +} + +// Status returns the status of the given repair {done,skip,retry,running} +func (rt *repairTrace) Status() string { + return filepath.Ext(rt.path)[1:] +} + +func indentPrefix(level int) string { + return strings.Repeat(" ", level) +} + +// WriteScriptIndented outputs the script that produced this repair output +// to the given writer w with the indent level given by indent. +func (rt *repairTrace) WriteScriptIndented(w io.Writer, indent int) error { + scriptPath := rt.path[:strings.LastIndex(rt.path, ".")] + ".script" + f, err := os.Open(scriptPath) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + fmt.Fprintf(w, "%s%s\n", indentPrefix(indent), scanner.Text()) + } + if scanner.Err() != nil { + return scanner.Err() + } + return nil +} + +// WriteOutputIndented outputs the repair output to the given writer w +// with the indent level given by indent. +func (rt *repairTrace) WriteOutputIndented(w io.Writer, indent int) error { + f, err := os.Open(rt.path) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + // move forward in the log to where the actual script output starts + for scanner.Scan() { + if scanner.Text() == "output:" { + break + } + } + // write the script output to w + for scanner.Scan() { + fmt.Fprintf(w, "%s%s\n", indentPrefix(indent), scanner.Text()) + } + if scanner.Err() != nil { + return scanner.Err() + } + return nil +} + +// revFromFilepath is a helper that extracts the revision number from the +// filename of the repairTrace +func revFromFilepath(name string) (string, error) { + var rev int + if _, err := fmt.Sscanf(filepath.Base(name), "r%d.", &rev); err == nil { + return strconv.Itoa(rev), nil + } + return "", fmt.Errorf("cannot find revision in %q", name) +} diff --git a/cmd/snap-repair/trace_test.go b/cmd/snap-repair/trace_test.go new file mode 100644 index 00000000..2a55bf1e --- /dev/null +++ b/cmd/snap-repair/trace_test.go @@ -0,0 +1,66 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" +) + +func makeMockRepairState(c *C) { + // the canonical script dir content + basedir := filepath.Join(dirs.SnapRepairRunDir, "canonical/1") + err := os.MkdirAll(basedir, 0700) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r3.retry"), []byte("repair: canonical-1\nsummary: repair one\noutput:\nretry output"), 0600) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r3.script"), []byte("#!/bin/sh\necho retry output"), 0700) + c.Assert(err, IsNil) + + // my-brand + basedir = filepath.Join(dirs.SnapRepairRunDir, "my-brand/1") + err = os.MkdirAll(basedir, 0700) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r1.done"), []byte("repair: my-brand-1\nsummary: my-brand repair one\noutput:\ndone output"), 0600) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r1.script"), []byte("#!/bin/sh\necho done output"), 0700) + c.Assert(err, IsNil) + + basedir = filepath.Join(dirs.SnapRepairRunDir, "my-brand/2") + err = os.MkdirAll(basedir, 0700) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r2.skip"), []byte("repair: my-brand-2\nsummary: my-brand repair two\noutput:\nskip output"), 0600) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r2.script"), []byte("#!/bin/sh\necho skip output"), 0700) + c.Assert(err, IsNil) + + basedir = filepath.Join(dirs.SnapRepairRunDir, "my-brand/3") + err = os.MkdirAll(basedir, 0700) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r0.running"), []byte("repair: my-brand-3\nsummary: my-brand repair three\noutput:\nrunning output"), 0600) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r0.script"), []byte("#!/bin/sh\necho running output"), 0700) + c.Assert(err, IsNil) +} diff --git a/cmd/snap-repair/trusted.go b/cmd/snap-repair/trusted.go new file mode 100644 index 00000000..c7599b05 --- /dev/null +++ b/cmd/snap-repair/trusted.go @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/osutil" +) + +const ( + encodedRepairRootAccountKey = `type: account-key +authority-id: canonical +public-key-sha3-384: nttW6NfBXI_E-00u38W-KH6eiksfQNXuI7IiumoV49_zkbhM0sYTzSnFlwZC-W4t +account-id: canonical +name: repair-root +since: 2017-07-07T00:00:00.0Z +body-length: 1406 +sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk + +AcbDTQRWhcGAASAAtlCIEilQCQh9Ffjn9IxD+5FtWfdKdJHrQdtFPy/2Q1kOvC++ef/3bG1xMwao +tue9K0HCMtv3apHS1C32Y8JBMD8oykRpAd5H05+sgzZr3kCHIvgogKFsXfdd5+W5Q1+59Vy/81UH +tJCs99wBwboNh/pMCXBGI3jDRN1f7hOxcHUIW+KTaHCVZnrXXmCn6Oe6brR9qiUXgEB2I6rBT/Fe +cumdfvFN/zSsJ3Vvv9IbTfHYAZD82NrSqz4UZ3WJarIaxlykgLJaZN4bqSQYPYsc8lLlwjQeGloW ++r8dIypKOzPnUYurzWcNzcCnNCT1zhpY/IK2rFcZbN5/mP2/5PtjFlbX88aPGPbOTqYANmxfboCx +wo4D4aS7PD6gLC7XM8bgh8BACpmG3BnskL7F/9IHMl85SUHFIya2fDu7A7HqNUn7cpENGbHojj7G +J2s2965FSRuIvp69wEmknYD/kahjT1+Vy94D2rVB7mjtTruPueF2KTpo2jRXFM+ABq+T9ybjXD6f +UuSXu5xeg0Cv1sxOh4O4b45uaCXb8B74chEUW+cb3cV0NGE/QgBJUBeS68vUI8lqQFmPInci6Md4 +oiKFVbloL0ZmOGj73Xv2uAcexAK9bEiI+adVS2x9r4eFwtkST3XG0t/kw7eLgAVjtRcpmD6EuZ0Q +ulAJHEsl7Sazm8GRU4GtZWaCajVb4n5TS1lin2nqUXwqxRUA3smLqkQoYXQni9vmhez4dEP4MVvq +/0IdI50UGME5Fzc8hUhYzvbNS8g+VOeAK/qj3dzcdF9+n940AQI16Fcxi1/xFk8A4dw3AaDl4XnJ +piyStE+xi95ne5HJW8r/f/JQos8I6QR5w7pe2URbgUdVPgQLv3r/4dS/X3aP+oakrPR7JuAVdP62 +vsjF4jK8Bl69mcF434xpshnbnW/f7XHomPY4gp8y7kD2/DdEs5hvaTHIPp25DEYhqjt3gfmMuUXi +Mb5oy9KZp3ff8Squ+XNWSGQSyhX14xcQwM8QjNQnAisNg2hYwSM2n8q5IDWiwJQkFSriP5tMsa8E +DMGI3LXUZKRJll9dQBjs6VzApT4/Ee0ZvSni0d2cWm3wkqFQudRpqz3JSwQ7jal0W5e0UhNeHh/W +7nACD5hvcwF7UgUz0r8adlOy+nyfvWte65nbcRrIH7GS1xdgS0e9eW4znsplp7s/Z3gMhi8CN5TY +0nZW82TTl69Wvn13SGJye0yJSjiy4KS0iRE6BwAt7dGAMs5c62IlBsWEHLmCW1/lixWA9YXT9iji +G7DKSoofnsvqVP2wIQZxxt4xHMjUGXecyx8QX4BznwsV1vbzHOIG4a3Z9A1X1L3yh3ZbazFVeEE9 +7Dhz9hGYfd3PvwARAQAB + +AcLDXAQAAQoABgUCWbuO2gAKCRDUpVvql9g3IOPcIADZWObdYMKh2SblWXxUchnc3S4LDRtL8v+Q +HdnXO5+dJmsj5LWhdsB7lz373aaylFTwHpNDWcDdAu7ulP0vr7zJURo1jGOo7VojSEeuAAu3YhwL +2pR0p5Me0wuxl/pCX0x0nfDSeeTw11kproyN0GwJaErKEmyQyfOgVr2jN5sl1gBqQtKgG5gqZzC3 +oFH1HYGPl2kfAorxFw7MoPy4aRFaxUJfx4x6bEktgkkFT7AWGmawVwcpiiUbbpe9CPLEsn6yqJI9 +5XmQ3dJjp/6Y5D7x04LRH3Q5fajRcpdBrC0tDsT9UDbSRtIyo0KDNVHwQalLa2Sv51DV+Fy4eneM +Lgu+oCUOnBecXIWnX+k0uyDW8aLHYapx8etpW3pln/hMRd8JxYVYAqDn7G2AYeSGS/4lzCJzysW2 +2/4RhH9Ql8ea0nSWVTJr3pmXKlPSH/OOy9IADEDUuEdvyMcq3YOXA9E4L3g9vR31JH+++swcTQPz +rnGx0mE+TCQRWok/NZ1QNv4eNZlnLXdNS1DoV/kRqU04cblYYUoSO34mkjPEJ8ti+VzKh/PTA6/7 +1feFX276Zam/6b2rBLWCWYdblDM9oLAR4PfzntBZW4LzzOIb95IwiK4JoDoBr3x4+RxvxgVQLvHt +8990uQ0se9+8BtLVFtd07NbldHXRBlZkq22a8CeVYrU3YZEtuEBvlGDpkdegw/vcvgHUUK1f8dXJ +0+9oW2yQOLAguuPZ67GFSgdTnvR5dQYZZR2EbQJlPMOFA3loKeUxHSR9w7E3SFqXGqN1v6APDK0V +lpVFq+rYlprvbf4GB0oR8yQOGtlxf+Ag3Nnx+90nlmtTK4k7tQpLzuAXGGREDCtn0y/YvWvGt6kN +EV5Q/mAVe2/CtAUvfyX7V3xlzYCrJT9DBcCBMaUUekFrwvZi13WYJIn7YE2Qmam7ZsXdb991PoFv ++c6Pmeg6w3y7D+Vj4Yfi8IrjPrc6765DaaZxFyMia9GEQKHChZDkiEiAM6RfwlC5YXGzCroaZi0Y +Knf/UkUWoa/jKZgQNiqrZ9oGmbURLeXkkHzpcFitwjzWr6tNScCzNIqs/uxTxbFM8fJu1gSmauEY +TE1rn62SiuHNRKJqfLcCHucStK10knHkHTAJ3avS7rBz0Dy8UOa77bOjyei5n2rkyXztL2YjjGYh +8jEt00xcvwJGePBfH10gCgTFWdfhfcP9/muKgiOSErQlHPypnr4vqO0PU9XDp106FFWyyNPd95kC +l5IF9WMfl7YHpT0Ph7kBYwg9sKF/7oCVdbT5CoImxkE5DTkWB8xX6W/BhuMrp1rzTHFFGVd1ppb7 +EMUll4dd78OWonMlIgsMRuTSn93awb4X8xSJhRi9 +` +) + +func init() { + repairRootAccountKey, err := asserts.Decode([]byte(encodedRepairRootAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted account-key: %v", err)) + } + if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + trustedRepairRootKeys = append(trustedRepairRootKeys, repairRootAccountKey.(*asserts.AccountKey)) + } +} diff --git a/cmd/snap-seccomp/export_test.go b/cmd/snap-seccomp/export_test.go new file mode 100644 index 00000000..d2722dc1 --- /dev/null +++ b/cmd/snap-seccomp/export_test.go @@ -0,0 +1,49 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +var ( + Compile = compile + SeccompResolver = seccompResolver +) + +func MockArchUbuntuArchitecture(f func() string) (restore func()) { + realArchUbuntuArchitecture := archUbuntuArchitecture + archUbuntuArchitecture = f + return func() { + archUbuntuArchitecture = realArchUbuntuArchitecture + } +} + +func MockArchUbuntuKernelArchitecture(f func() string) (restore func()) { + realArchUbuntuKernelArchitecture := archUbuntuKernelArchitecture + archUbuntuKernelArchitecture = f + return func() { + archUbuntuKernelArchitecture = realArchUbuntuKernelArchitecture + } +} + +func MockErrnoOnDenial(i int16) (retore func()) { + origErrnoOnDenial := errnoOnDenial + errnoOnDenial = i + return func() { + errnoOnDenial = origErrnoOnDenial + } +} diff --git a/cmd/snap-seccomp/main.go b/cmd/snap-seccomp/main.go new file mode 100644 index 00000000..2d9158f4 --- /dev/null +++ b/cmd/snap-seccomp/main.go @@ -0,0 +1,785 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017-2018 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 + +//#cgo CFLAGS: -D_FILE_OFFSET_BITS=64 +//#cgo pkg-config: libseccomp +//#cgo LDFLAGS: +// +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +// //The XFS interface requires a 64 bit file system interface +// //but we don't want to leak this anywhere else if not globally +// //defined. +//#ifndef _FILE_OFFSET_BITS +//#define _FILE_OFFSET_BITS 64 +//#include +//#undef _FILE_OFFSET_BITS +//#else +//#include +//#endif +//#include +//#include +//#include +//#include +// +//#ifndef AF_IB +//#define AF_IB 27 +//#define PF_IB AF_IB +//#endif // AF_IB +// +//#ifndef AF_MPLS +//#define AF_MPLS 28 +//#define PF_MPLS AF_MPLS +//#endif // AF_MPLS +// +// // https://github.com/sctplab/usrsctp/blob/master/usrsctplib/usrsctp.h +//#ifndef AF_CONN +//#define AF_CONN 123 +//#define PF_CONN AF_CONN +//#endif // AF_CONN +// +//#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 +// +//#ifndef PR_SET_THP_DISABLE +//#define PR_SET_THP_DISABLE 41 +//#endif // PR_SET_THP_DISABLE +//#ifndef PR_GET_THP_DISABLE +//#define PR_GET_THP_DISABLE 42 +//#endif // PR_GET_THP_DISABLE +// +//#ifndef PR_MPX_ENABLE_MANAGEMENT +//#define PR_MPX_ENABLE_MANAGEMENT 43 +//#endif +// +//#ifndef PR_MPX_DISABLE_MANAGEMENT +//#define PR_MPX_DISABLE_MANAGEMENT 44 +//#endif +// +// //FIXME: ARCH_BAD is defined as ~0 in libseccomp internally, however +// // this leads to a build failure on 14.04. the important part +// // is that its an invalid id for libseccomp. +// +//#define ARCH_BAD 0x7FFFFFFF +//#ifndef SCMP_ARCH_AARCH64 +//#define SCMP_ARCH_AARCH64 ARCH_BAD +//#endif +// +//#ifndef SCMP_ARCH_PPC +//#define SCMP_ARCH_PPC ARCH_BAD +//#endif +// +//#ifndef SCMP_ARCH_PPC64LE +//#define SCMP_ARCH_PPC64LE ARCH_BAD +//#endif +// +//#ifndef SCMP_ARCH_PPC64 +//#define SCMP_ARCH_PPC64 ARCH_BAD +//#endif +// +//#ifndef SCMP_ARCH_S390X +//#define SCMP_ARCH_S390X ARCH_BAD +//#endif +// +//#ifndef SECCOMP_RET_LOG +//#define SECCOMP_RET_LOG 0x7ffc0000U +//#endif +// +//typedef struct seccomp_data kernel_seccomp_data; +// +//__u32 htot32(__u32 arch, __u32 val) +//{ +// if (arch & __AUDIT_ARCH_LE) +// return htole32(val); +// else +// return htobe32(val); +//} +// +//__u64 htot64(__u32 arch, __u64 val) +//{ +// if (arch & __AUDIT_ARCH_LE) +// return htole64(val); +// else +// return htobe64(val); +//} +// +// /* Define missing ptrace constants. They are available on some architectures +// only but the missing values are not reused on architectures that lack them. +// As such we can simply define the missing pair and have a simpler cross-arch +// code to support. */ +// +// #ifndef PTRACE_GETREGS +// #define PTRACE_GETREGS 12 +// #endif +// #ifndef PTRACE_SETREGS +// #define PTRACE_SETREGS 13 +// #endif +// #ifndef PTRACE_GETFPREGS +// #define PTRACE_GETFPREGS 14 +// #endif +// #ifndef PTRACE_SETFPREGS +// #define PTRACE_SETFPREGS 15 +// #endif +// #ifndef PTRACE_GETFPXREGS +// #define PTRACE_GETFPXREGS 18 +// #endif +// #ifndef PTRACE_SETFPXREGS +// #define PTRACE_SETFPXREGS 19 +// #endif +import "C" + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "os" + "regexp" + "strconv" + "strings" + "syscall" + + // FIXME: we want github.com/seccomp/libseccomp-golang but that + // will not work with trusty because libseccomp-golang checks + // for the seccomp version and errors if it find one < 2.2.0 + "github.com/mvo5/libseccomp-golang" + + "github.com/snapcore/snapd/arch" + "github.com/snapcore/snapd/osutil" +) + +// libseccomp maximum per ARG_COUNT_MAX in src/arch.h +const ScArgsMaxlength = 6 + +var seccompResolver = map[string]uint64{ + // man 2 socket - domain and man 5 apparmor.d. AF_ and PF_ are + // synonymous in the kernel and can be used interchangeably in + // policy (ie, if use AF_UNIX, don't need a corresponding PF_UNIX + // rule). See include/linux/socket.h + "AF_UNIX": syscall.AF_UNIX, + "PF_UNIX": C.PF_UNIX, + "AF_LOCAL": syscall.AF_LOCAL, + "PF_LOCAL": C.PF_LOCAL, + "AF_INET": syscall.AF_INET, + "PF_INET": C.PF_INET, + "AF_INET6": syscall.AF_INET6, + "PF_INET6": C.PF_INET6, + "AF_IPX": syscall.AF_IPX, + "PF_IPX": C.PF_IPX, + "AF_NETLINK": syscall.AF_NETLINK, + "PF_NETLINK": C.PF_NETLINK, + "AF_X25": syscall.AF_X25, + "PF_X25": C.PF_X25, + "AF_AX25": syscall.AF_AX25, + "PF_AX25": C.PF_AX25, + "AF_ATMPVC": syscall.AF_ATMPVC, + "PF_ATMPVC": C.PF_ATMPVC, + "AF_APPLETALK": syscall.AF_APPLETALK, + "PF_APPLETALK": C.PF_APPLETALK, + "AF_PACKET": syscall.AF_PACKET, + "PF_PACKET": C.PF_PACKET, + "AF_ALG": syscall.AF_ALG, + "PF_ALG": C.PF_ALG, + "AF_BRIDGE": syscall.AF_BRIDGE, + "PF_BRIDGE": C.PF_BRIDGE, + "AF_NETROM": syscall.AF_NETROM, + "PF_NETROM": C.PF_NETROM, + "AF_ROSE": syscall.AF_ROSE, + "PF_ROSE": C.PF_ROSE, + "AF_NETBEUI": syscall.AF_NETBEUI, + "PF_NETBEUI": C.PF_NETBEUI, + "AF_SECURITY": syscall.AF_SECURITY, + "PF_SECURITY": C.PF_SECURITY, + "AF_KEY": syscall.AF_KEY, + "PF_KEY": C.PF_KEY, + "AF_ASH": syscall.AF_ASH, + "PF_ASH": C.PF_ASH, + "AF_ECONET": syscall.AF_ECONET, + "PF_ECONET": C.PF_ECONET, + "AF_SNA": syscall.AF_SNA, + "PF_SNA": C.PF_SNA, + "AF_IRDA": syscall.AF_IRDA, + "PF_IRDA": C.PF_IRDA, + "AF_PPPOX": syscall.AF_PPPOX, + "PF_PPPOX": C.PF_PPPOX, + "AF_WANPIPE": syscall.AF_WANPIPE, + "PF_WANPIPE": C.PF_WANPIPE, + "AF_BLUETOOTH": syscall.AF_BLUETOOTH, + "PF_BLUETOOTH": C.PF_BLUETOOTH, + "AF_RDS": syscall.AF_RDS, + "PF_RDS": C.PF_RDS, + "AF_LLC": syscall.AF_LLC, + "PF_LLC": C.PF_LLC, + "AF_TIPC": syscall.AF_TIPC, + "PF_TIPC": C.PF_TIPC, + "AF_IUCV": syscall.AF_IUCV, + "PF_IUCV": C.PF_IUCV, + "AF_RXRPC": syscall.AF_RXRPC, + "PF_RXRPC": C.PF_RXRPC, + "AF_ISDN": syscall.AF_ISDN, + "PF_ISDN": C.PF_ISDN, + "AF_PHONET": syscall.AF_PHONET, + "PF_PHONET": C.PF_PHONET, + "AF_IEEE802154": syscall.AF_IEEE802154, + "PF_IEEE802154": C.PF_IEEE802154, + "AF_CAIF": syscall.AF_CAIF, + "PF_CAIF": C.AF_CAIF, + "AF_NFC": C.AF_NFC, + "PF_NFC": C.PF_NFC, + "AF_VSOCK": C.AF_VSOCK, + "PF_VSOCK": C.PF_VSOCK, + // may not be defined in socket.h yet + "AF_IB": C.AF_IB, // 27 + "PF_IB": C.PF_IB, + "AF_MPLS": C.AF_MPLS, // 28 + "PF_MPLS": C.PF_MPLS, + "AF_CAN": syscall.AF_CAN, + "PF_CAN": C.PF_CAN, + "AF_CONN": C.AF_CONN, // 123 + "PF_CONN": C.PF_CONN, + + // man 2 socket - type + "SOCK_STREAM": syscall.SOCK_STREAM, + "SOCK_DGRAM": syscall.SOCK_DGRAM, + "SOCK_SEQPACKET": syscall.SOCK_SEQPACKET, + "SOCK_RAW": syscall.SOCK_RAW, + "SOCK_RDM": syscall.SOCK_RDM, + "SOCK_PACKET": syscall.SOCK_PACKET, + + // man 2 prctl + "PR_CAP_AMBIENT": C.PR_CAP_AMBIENT, + "PR_CAP_AMBIENT_RAISE": C.PR_CAP_AMBIENT_RAISE, + "PR_CAP_AMBIENT_LOWER": C.PR_CAP_AMBIENT_LOWER, + "PR_CAP_AMBIENT_IS_SET": C.PR_CAP_AMBIENT_IS_SET, + "PR_CAP_AMBIENT_CLEAR_ALL": C.PR_CAP_AMBIENT_CLEAR_ALL, + "PR_CAPBSET_READ": C.PR_CAPBSET_READ, + "PR_CAPBSET_DROP": C.PR_CAPBSET_DROP, + "PR_SET_CHILD_SUBREAPER": C.PR_SET_CHILD_SUBREAPER, + "PR_GET_CHILD_SUBREAPER": C.PR_GET_CHILD_SUBREAPER, + "PR_SET_DUMPABLE": C.PR_SET_DUMPABLE, + "PR_GET_DUMPABLE": C.PR_GET_DUMPABLE, + "PR_SET_ENDIAN": C.PR_SET_ENDIAN, + "PR_GET_ENDIAN": C.PR_GET_ENDIAN, + "PR_SET_FPEMU": C.PR_SET_FPEMU, + "PR_GET_FPEMU": C.PR_GET_FPEMU, + "PR_SET_FPEXC": C.PR_SET_FPEXC, + "PR_GET_FPEXC": C.PR_GET_FPEXC, + "PR_SET_KEEPCAPS": C.PR_SET_KEEPCAPS, + "PR_GET_KEEPCAPS": C.PR_GET_KEEPCAPS, + "PR_MCE_KILL": C.PR_MCE_KILL, + "PR_MCE_KILL_GET": C.PR_MCE_KILL_GET, + "PR_SET_MM": C.PR_SET_MM, + "PR_SET_MM_START_CODE": C.PR_SET_MM_START_CODE, + "PR_SET_MM_END_CODE": C.PR_SET_MM_END_CODE, + "PR_SET_MM_START_DATA": C.PR_SET_MM_START_DATA, + "PR_SET_MM_END_DATA": C.PR_SET_MM_END_DATA, + "PR_SET_MM_START_STACK": C.PR_SET_MM_START_STACK, + "PR_SET_MM_START_BRK": C.PR_SET_MM_START_BRK, + "PR_SET_MM_BRK": C.PR_SET_MM_BRK, + "PR_SET_MM_ARG_START": C.PR_SET_MM_ARG_START, + "PR_SET_MM_ARG_END": C.PR_SET_MM_ARG_END, + "PR_SET_MM_ENV_START": C.PR_SET_MM_ENV_START, + "PR_SET_MM_ENV_END": C.PR_SET_MM_ENV_END, + "PR_SET_MM_AUXV": C.PR_SET_MM_AUXV, + "PR_SET_MM_EXE_FILE": C.PR_SET_MM_EXE_FILE, + "PR_MPX_ENABLE_MANAGEMENT": C.PR_MPX_ENABLE_MANAGEMENT, + "PR_MPX_DISABLE_MANAGEMENT": C.PR_MPX_DISABLE_MANAGEMENT, + "PR_SET_NAME": C.PR_SET_NAME, + "PR_GET_NAME": C.PR_GET_NAME, + "PR_SET_NO_NEW_PRIVS": C.PR_SET_NO_NEW_PRIVS, + "PR_GET_NO_NEW_PRIVS": C.PR_GET_NO_NEW_PRIVS, + "PR_SET_PDEATHSIG": C.PR_SET_PDEATHSIG, + "PR_GET_PDEATHSIG": C.PR_GET_PDEATHSIG, + "PR_SET_PTRACER": C.PR_SET_PTRACER, + "PR_SET_SECCOMP": C.PR_SET_SECCOMP, + "PR_GET_SECCOMP": C.PR_GET_SECCOMP, + "PR_SET_SECUREBITS": C.PR_SET_SECUREBITS, + "PR_GET_SECUREBITS": C.PR_GET_SECUREBITS, + "PR_SET_THP_DISABLE": C.PR_SET_THP_DISABLE, + "PR_TASK_PERF_EVENTS_DISABLE": C.PR_TASK_PERF_EVENTS_DISABLE, + "PR_TASK_PERF_EVENTS_ENABLE": C.PR_TASK_PERF_EVENTS_ENABLE, + "PR_GET_THP_DISABLE": C.PR_GET_THP_DISABLE, + "PR_GET_TID_ADDRESS": C.PR_GET_TID_ADDRESS, + "PR_SET_TIMERSLACK": C.PR_SET_TIMERSLACK, + "PR_GET_TIMERSLACK": C.PR_GET_TIMERSLACK, + "PR_SET_TIMING": C.PR_SET_TIMING, + "PR_GET_TIMING": C.PR_GET_TIMING, + "PR_SET_TSC": C.PR_SET_TSC, + "PR_GET_TSC": C.PR_GET_TSC, + "PR_SET_UNALIGN": C.PR_SET_UNALIGN, + "PR_GET_UNALIGN": C.PR_GET_UNALIGN, + + // man 2 getpriority + "PRIO_PROCESS": syscall.PRIO_PROCESS, + "PRIO_PGRP": syscall.PRIO_PGRP, + "PRIO_USER": syscall.PRIO_USER, + + // man 2 setns + "CLONE_NEWIPC": syscall.CLONE_NEWIPC, + "CLONE_NEWNET": syscall.CLONE_NEWNET, + "CLONE_NEWNS": syscall.CLONE_NEWNS, + "CLONE_NEWPID": syscall.CLONE_NEWPID, + "CLONE_NEWUSER": syscall.CLONE_NEWUSER, + "CLONE_NEWUTS": syscall.CLONE_NEWUTS, + + // man 4 tty_ioctl + "TIOCSTI": syscall.TIOCSTI, + + // man 2 quotactl (with what Linux supports) + "Q_SYNC": C.Q_SYNC, + "Q_QUOTAON": C.Q_QUOTAON, + "Q_QUOTAOFF": C.Q_QUOTAOFF, + "Q_GETFMT": C.Q_GETFMT, + "Q_GETINFO": C.Q_GETINFO, + "Q_SETINFO": C.Q_SETINFO, + "Q_GETQUOTA": C.Q_GETQUOTA, + "Q_SETQUOTA": C.Q_SETQUOTA, + "Q_XQUOTAON": C.Q_XQUOTAON, + "Q_XQUOTAOFF": C.Q_XQUOTAOFF, + "Q_XGETQUOTA": C.Q_XGETQUOTA, + "Q_XSETQLIM": C.Q_XSETQLIM, + "Q_XGETQSTAT": C.Q_XGETQSTAT, + "Q_XQUOTARM": C.Q_XQUOTARM, + + // man 2 mknod + "S_IFREG": syscall.S_IFREG, + "S_IFCHR": syscall.S_IFCHR, + "S_IFBLK": syscall.S_IFBLK, + "S_IFIFO": syscall.S_IFIFO, + "S_IFSOCK": syscall.S_IFSOCK, + + // man 7 netlink (uapi/linux/netlink.h) + "NETLINK_ROUTE": syscall.NETLINK_ROUTE, + "NETLINK_USERSOCK": syscall.NETLINK_USERSOCK, + "NETLINK_FIREWALL": syscall.NETLINK_FIREWALL, + "NETLINK_SOCK_DIAG": C.NETLINK_SOCK_DIAG, + "NETLINK_NFLOG": syscall.NETLINK_NFLOG, + "NETLINK_XFRM": syscall.NETLINK_XFRM, + "NETLINK_SELINUX": syscall.NETLINK_SELINUX, + "NETLINK_ISCSI": syscall.NETLINK_ISCSI, + "NETLINK_AUDIT": syscall.NETLINK_AUDIT, + "NETLINK_FIB_LOOKUP": syscall.NETLINK_FIB_LOOKUP, + "NETLINK_CONNECTOR": syscall.NETLINK_CONNECTOR, + "NETLINK_NETFILTER": syscall.NETLINK_NETFILTER, + "NETLINK_IP6_FW": syscall.NETLINK_IP6_FW, + "NETLINK_DNRTMSG": syscall.NETLINK_DNRTMSG, + "NETLINK_KOBJECT_UEVENT": syscall.NETLINK_KOBJECT_UEVENT, + "NETLINK_GENERIC": syscall.NETLINK_GENERIC, + "NETLINK_SCSITRANSPORT": syscall.NETLINK_SCSITRANSPORT, + "NETLINK_ECRYPTFS": syscall.NETLINK_ECRYPTFS, + "NETLINK_RDMA": C.NETLINK_RDMA, + "NETLINK_CRYPTO": C.NETLINK_CRYPTO, + "NETLINK_INET_DIAG": C.NETLINK_INET_DIAG, // synonymous with NETLINK_SOCK_DIAG + + // man 2 ptrace + "PTRACE_ATTACH": C.PTRACE_ATTACH, + "PTRACE_DETACH": C.PTRACE_DETACH, + "PTRACE_GETREGS": C.PTRACE_GETREGS, + "PTRACE_GETFPREGS": C.PTRACE_GETFPREGS, + "PTRACE_GETFPXREGS": C.PTRACE_GETFPXREGS, + "PTRACE_GETREGSET": C.PTRACE_GETREGSET, + "PTRACE_PEEKDATA": C.PTRACE_PEEKDATA, + // and have different spellings for PEEKUS{,E}R + "PTRACE_PEEKUSR": C.PTRACE_PEEKUSER, + "PTRACE_PEEKUSER": C.PTRACE_PEEKUSER, + "PTRACE_CONT": C.PTRACE_CONT, +} + +// UbuntuArchToScmpArch takes a dpkg architecture and converts it to +// the seccomp.ScmpArch as used in the libseccomp-golang library +func UbuntuArchToScmpArch(ubuntuArch string) seccomp.ScmpArch { + switch ubuntuArch { + case "amd64": + return seccomp.ArchAMD64 + case "arm64": + return seccomp.ArchARM64 + case "armhf": + return seccomp.ArchARM + case "i386": + return seccomp.ArchX86 + case "powerpc": + return seccomp.ArchPPC + case "ppc64": + return seccomp.ArchPPC64 + case "ppc64el": + return seccomp.ArchPPC64LE + case "s390x": + return seccomp.ArchS390X + } + panic(fmt.Sprintf("cannot map ubuntu arch %q to a seccomp arch", ubuntuArch)) +} + +// important for unit testing +type SeccompData C.kernel_seccomp_data + +func (sc *SeccompData) SetNr(nr seccomp.ScmpSyscall) { + sc.nr = C.int(C.htot32(C.__u32(sc.arch), C.__u32(nr))) +} +func (sc *SeccompData) SetArch(arch uint32) { + sc.arch = C.htot32(C.__u32(arch), C.__u32(arch)) +} +func (sc *SeccompData) SetArgs(args [6]uint64) { + for i := range args { + sc.args[i] = C.htot64(sc.arch, C.__u64(args[i])) + } +} + +func readNumber(token string) (uint64, error) { + if value, ok := seccompResolver[token]; ok { + return value, nil + } + // Negative numbers are not supported yet, but when they are, + // adjust this accordingly + return strconv.ParseUint(token, 10, 64) +} + +func parseLine(line string, secFilter *seccomp.ScmpFilter) error { + // ignore comments and empty lines + if strings.HasPrefix(line, "#") || line == "" { + return nil + } + + // regular line + tokens := strings.Fields(line) + if len(tokens[1:]) > ScArgsMaxlength { + return fmt.Errorf("too many arguments specified for syscall '%s' in line %q", tokens[0], line) + } + + // fish out syscall + secSyscall, err := seccomp.GetSyscallFromName(tokens[0]) + if err != nil { + // FIXME: use structed error in libseccomp-golang when + // https://github.com/seccomp/libseccomp-golang/pull/26 + // gets merged. For now, ignore + // unknown syscalls + return nil + } + + var conds []seccomp.ScmpCondition + for pos, arg := range tokens[1:] { + var cmpOp seccomp.ScmpCompareOp + var value uint64 + var err error + + if arg == "-" { // skip arg + continue + } + + if strings.HasPrefix(arg, ">=") { + cmpOp = seccomp.CompareGreaterEqual + value, err = readNumber(arg[2:]) + } else if strings.HasPrefix(arg, "<=") { + cmpOp = seccomp.CompareLessOrEqual + value, err = readNumber(arg[2:]) + } else if strings.HasPrefix(arg, "!") { + cmpOp = seccomp.CompareNotEqual + value, err = readNumber(arg[1:]) + } else if strings.HasPrefix(arg, "<") { + cmpOp = seccomp.CompareLess + value, err = readNumber(arg[1:]) + } else if strings.HasPrefix(arg, ">") { + cmpOp = seccomp.CompareGreater + value, err = readNumber(arg[1:]) + } else if strings.HasPrefix(arg, "|") { + cmpOp = seccomp.CompareMaskedEqual + value, err = readNumber(arg[1:]) + } else if strings.HasPrefix(arg, "u:") { + cmpOp = seccomp.CompareEqual + value, err = findUid(arg[2:]) + if err != nil { + return fmt.Errorf("cannot parse token %q (line %q): %v", arg, line, err) + } + } else if strings.HasPrefix(arg, "g:") { + cmpOp = seccomp.CompareEqual + value, err = findGid(arg[2:]) + if err != nil { + return fmt.Errorf("cannot parse token %q (line %q): %v", arg, line, err) + } + } else { + cmpOp = seccomp.CompareEqual + value, err = readNumber(arg) + } + if err != nil { + return fmt.Errorf("cannot parse token %q (line %q)", arg, line) + } + + var scmpCond seccomp.ScmpCondition + if cmpOp == seccomp.CompareMaskedEqual { + scmpCond, err = seccomp.MakeCondition(uint(pos), cmpOp, value, value) + } else { + scmpCond, err = seccomp.MakeCondition(uint(pos), cmpOp, value) + } + if err != nil { + return fmt.Errorf("cannot parse line %q: %s", line, err) + } + conds = append(conds, scmpCond) + } + + // Default to adding a precise match if possible. Otherwise + // let seccomp figure out the architecture specifics. + if err = secFilter.AddRuleConditionalExact(secSyscall, seccomp.ActAllow, conds); err != nil { + err = secFilter.AddRuleConditional(secSyscall, seccomp.ActAllow, conds) + } + + return err +} + +// used to mock in tests +var ( + archUbuntuArchitecture = arch.UbuntuArchitecture + archUbuntuKernelArchitecture = arch.UbuntuKernelArchitecture +) + +var ( + ubuntuArchitecture = archUbuntuArchitecture() + ubuntuKernelArchitecture = archUbuntuKernelArchitecture() +) + +// 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). +func addSecondaryArches(secFilter *seccomp.ScmpFilter) error { + // note that all architecture strings are in the dpkg + // architecture notation + var compatArch seccomp.ScmpArch + + // common case: kernel and userspace have the same arch. We + // add a compat architecture for some architectures that + // support it, e.g. on amd64 kernel and userland, we add + // compat i386 syscalls. + if ubuntuArchitecture == ubuntuKernelArchitecture { + switch archUbuntuArchitecture() { + case "amd64": + compatArch = seccomp.ArchX86 + case "arm64": + compatArch = seccomp.ArchARM + case "ppc64": + compatArch = seccomp.ArchPPC + } + } else { + // less common case: kernel and userspace have different archs + // so add a compat architecture that matches the kernel. E.g. + // an amd64 kernel with i386 userland needs the amd64 secondary + // arch added to support specialized snaps that might + // conditionally call 64bit code when the kernel supports it. + // Note that in this case snapd requests i386 (or arch 'all') + // snaps. While unusual from a traditional Linux distribution + // perspective, certain classes of embedded devices are known + // to use this configuration. + compatArch = UbuntuArchToScmpArch(archUbuntuKernelArchitecture()) + } + + if compatArch != seccomp.ArchInvalid { + return secFilter.AddArch(compatArch) + } + + return nil +} + +var errnoOnDenial int16 = C.EPERM + +func preprocess(content []byte) (unrestricted, complain bool) { + scanner := bufio.NewScanner(bytes.NewBuffer(content)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + switch line { + case "@unrestricted": + unrestricted = true + case "@complain": + complain = true + } + } + return unrestricted, complain +} + +func complainAction() seccomp.ScmpAction { + // XXX: Work around some distributions not having a new enough + // libseccomp-golang that declares ActLog. Instead, we'll guess at its + // value by adding one to ActAllow and then verify that the string + // representation is what we expect for ActLog. The value and string is + // defined in https://github.com/seccomp/libseccomp-golang/pull/29. + // + // Ultimately, the fix for this workaround is to be able to use the + // GetApi() function created in the PR above. It'll tell us if the + // kernel, libseccomp, and libseccomp-golang all support ActLog. + var actLog seccomp.ScmpAction = seccomp.ActAllow + 1 + + if actLog.String() == "Action: Log system call" { + return actLog + } + + // Because ActLog is functionally ActAllow with logging, if we don't + // support ActLog, fallback to ActLog. + return seccomp.ActAllow +} + +func compile(content []byte, out string) error { + var err error + var secFilter *seccomp.ScmpFilter + + unrestricted, complain := preprocess(content) + switch { + case unrestricted: + return osutil.AtomicWrite(out, bytes.NewBufferString("@unrestricted\n"), 0644, 0) + case complain: + var complainAct seccomp.ScmpAction = complainAction() + + secFilter, err = seccomp.NewFilter(complainAct) + if err != nil { + if complainAct != seccomp.ActAllow { + // ActLog is only supported in newer versions + // of the kernel, libseccomp, and + // libseccomp-golang. Attempt to fall back to + // ActAllow before erroring out. + complainAct = seccomp.ActAllow + secFilter, err = seccomp.NewFilter(complainAct) + } + } + + // Set unrestricted to 'true' to fallback to the pre-ActLog + // behavior of simply setting the allow filter without adding + // any rules. + if complainAct == seccomp.ActAllow { + unrestricted = true + } + default: + secFilter, err = seccomp.NewFilter(seccomp.ActErrno.SetReturnCode(errnoOnDenial)) + } + if err != nil { + return fmt.Errorf("cannot create seccomp filter: %s", err) + } + if err := addSecondaryArches(secFilter); err != nil { + return err + } + + if !unrestricted { + scanner := bufio.NewScanner(bytes.NewBuffer(content)) + for scanner.Scan() { + if err := parseLine(scanner.Text(), secFilter); err != nil { + return fmt.Errorf("cannot parse line: %s", err) + } + } + if scanner.Err(); err != nil { + return err + } + } + + if osutil.GetenvBool("SNAP_SECCOMP_DEBUG") { + secFilter.ExportPFC(os.Stdout) + } + + // write atomically + fout, err := osutil.NewAtomicFile(out, 0644, 0, osutil.NoChown, osutil.NoChown) + if err != nil { + return err + } + // Cancel once Committed is a NOP + defer fout.Cancel() + + if err := secFilter.ExportBPF(fout.File); err != nil { + return err + } + return fout.Commit() +} + +// Be very strict so usernames and groups specified in policy are widely +// compatible. From NAME_REGEX in /etc/adduser.conf +var userGroupNamePattern = regexp.MustCompile("^[a-z][-a-z0-9_]*$") + +// findUid returns the identifier of the given UNIX user name. +func findUid(username string) (uint64, error) { + if !userGroupNamePattern.MatchString(username) { + return 0, fmt.Errorf("%q must be a valid username", username) + } + return osutil.FindUid(username) +} + +// findGid returns the identifier of the given UNIX group name. +func findGid(group string) (uint64, error) { + if !userGroupNamePattern.MatchString(group) { + return 0, fmt.Errorf("%q must be a valid group name", group) + } + return osutil.FindGid(group) +} + +func showSeccompLibraryVersion() error { + major, minor, micro := seccomp.GetLibraryVersion() + fmt.Fprintf(os.Stdout, "%d.%d.%d\n", major, minor, micro) + return nil +} + +func main() { + var err error + var content []byte + + if len(os.Args) < 2 { + fmt.Printf("%s: need a command\n", os.Args[0]) + os.Exit(1) + } + + cmd := os.Args[1] + switch cmd { + case "compile": + if len(os.Args) < 4 { + fmt.Println("compile needs an input and output file") + os.Exit(1) + } + content, err = ioutil.ReadFile(os.Args[2]) + if err != nil { + break + } + err = compile(content, os.Args[3]) + case "library-version": + err = showSeccompLibraryVersion() + default: + err = fmt.Errorf("unsupported argument %q", cmd) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/snap-seccomp/main_ppc64le.go b/cmd/snap-seccomp/main_ppc64le.go new file mode 100644 index 00000000..fdfc2543 --- /dev/null +++ b/cmd/snap-seccomp/main_ppc64le.go @@ -0,0 +1,31 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +// +build ppc64le,go1.7,!go1.8 + +/* + * Copyright (C) 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 . + * + */ + +package main + +/* +#cgo LDFLAGS: -no-pie + +// we need "-no-pie" for ppc64le,go1.7 to work around build failure on +// ppc64el with go1.7, see +// https://forum.snapcraft.io/t/snapd-master-fails-on-zesty-ppc64el-with-r-ppc64-addr16-ha-for-symbol-out-of-range/ +*/ +import "C" diff --git a/cmd/snap-seccomp/main_test.go b/cmd/snap-seccomp/main_test.go new file mode 100644 index 00000000..9a8a28fb --- /dev/null +++ b/cmd/snap-seccomp/main_test.go @@ -0,0 +1,805 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "math/rand" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + + . "gopkg.in/check.v1" + + "github.com/mvo5/libseccomp-golang" + + "github.com/snapcore/snapd/arch" + main "github.com/snapcore/snapd/cmd/snap-seccomp" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type snapSeccompSuite struct { + seccompBpfLoader string + seccompSyscallRunner string + canCheckCompatArch bool +} + +var _ = Suite(&snapSeccompSuite{}) + +const ( + Deny = iota + Allow +) + +var seccompBpfLoaderContent = []byte(` +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define MAX_BPF_SIZE 32 * 1024 + +int sc_apply_seccomp_bpf(const char* profile_path) +{ + unsigned char bpf[MAX_BPF_SIZE + 1]; // account for EOF + FILE* fp; + fp = fopen(profile_path, "rb"); + if (fp == NULL) { + fprintf(stderr, "cannot read %s\n", profile_path); + return -1; + } + + // set 'size' to 1; to get bytes transferred + size_t num_read = fread(bpf, 1, sizeof(bpf), fp); + + if (ferror(fp) != 0) { + perror("fread()"); + return -1; + } else if (feof(fp) == 0) { + fprintf(stderr, "file too big\n"); + return -1; + } + fclose(fp); + + struct sock_fprog prog = { + .len = num_read / sizeof(struct sock_filter), + .filter = (struct sock_filter*)bpf, + }; + + // Set NNP to allow loading seccomp policy into the kernel without + // root + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + perror("prctl(PR_NO_NEW_PRIVS, 1, 0, 0, 0)"); + return -1; + } + + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) { + perror("prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...) failed"); + return -1; + } + return 0; +} + +int main(int argc, char* argv[]) +{ + int rc = 0; + if (argc < 2) { + fprintf(stderr, "Usage: %s [prog ...]\n", argv[0]); + return 1; + } + + rc = sc_apply_seccomp_bpf(argv[1]); + if (rc != 0) + return -rc; + + execv(argv[2], (char* const*)&argv[2]); + perror("execv failed"); + return 1; +} +`) + +var seccompSyscallRunnerContent = []byte(` +#define _GNU_SOURCE +#include +#include +#include +#include +int main(int argc, char** argv) +{ + int l[7], syscall_ret, ret = 0; + for (int i = 0; i < 7; i++) + l[i] = atoi(argv[i + 1]); + // There might be architecture-specific requirements. see "man syscall" + // for details. + syscall_ret = syscall(l[0], l[1], l[2], l[3], l[4], l[5], l[6]); + // 911 is our mocked errno + if (syscall_ret < 0 && errno == 911) { + ret = 10; + } + syscall(SYS_exit, ret, 0, 0, 0, 0, 0); + return 0; +} +`) + +func (s *snapSeccompSuite) SetUpSuite(c *C) { + main.MockErrnoOnDenial(911) + + // build seccomp-load helper + s.seccompBpfLoader = filepath.Join(c.MkDir(), "seccomp_bpf_loader") + err := ioutil.WriteFile(s.seccompBpfLoader+".c", seccompBpfLoaderContent, 0644) + c.Assert(err, IsNil) + cmd := exec.Command("gcc", "-Werror", "-Wall", s.seccompBpfLoader+".c", "-o", s.seccompBpfLoader) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + c.Assert(err, IsNil) + + // build syscall-runner helper + s.seccompSyscallRunner = filepath.Join(c.MkDir(), "seccomp_syscall_runner") + err = ioutil.WriteFile(s.seccompSyscallRunner+".c", seccompSyscallRunnerContent, 0644) + c.Assert(err, IsNil) + + cmd = exec.Command("gcc", "-std=c99", "-Werror", "-Wall", "-static", s.seccompSyscallRunner+".c", "-o", s.seccompSyscallRunner, "-Wl,-static", "-static-libgcc") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + c.Assert(err, IsNil) + + // Amazon Linux 2 is 64bit only and there is no multilib support + s.canCheckCompatArch = !release.DistroLike("amzn") + + // Build 32bit runner on amd64 to test non-native syscall handling. + // Ideally we would build for ppc64el->powerpc and arm64->armhf but + // it seems tricky to find the right gcc-multilib for this. + if arch.UbuntuArchitecture() == "amd64" && s.canCheckCompatArch { + cmd = exec.Command(cmd.Args[0], cmd.Args[1:]...) + cmd.Args = append(cmd.Args, "-m32") + for i, k := range cmd.Args { + if k == s.seccompSyscallRunner { + cmd.Args[i] = s.seccompSyscallRunner + ".m32" + } + } + if output, err := cmd.CombinedOutput(); err != nil { + fmt.Printf("cannot build multi-lib syscall runner: %v\n%s", err, output) + } + } +} + +// Runs the policy through the kernel: +// 1. runs main.Compile() +// 2. the program in seccompBpfLoaderContent with the output file as an +// argument +// 3. the program in seccompBpfLoaderContent loads the output file BPF into +// the kernel and executes the program in seccompBpfRunnerContent with the +// syscall and arguments specified by the test +// +// In this manner, in addition to verifying policy syntax we are able to +// unit test the resulting bpf in several ways. +// +// Full testing of applied policy is done elsewhere via spread tests. +// +// Note that we skip testing prctl(PR_SET_ENDIAN) - it causes havoc when +// it is run. We will also need to skip: fadvise64_64, +// ftruncate64, posix_fadvise, pread64, pwrite64, readahead, +// sync_file_range, and truncate64. +// Once we start using those. See `man syscall` +func (s *snapSeccompSuite) runBpf(c *C, seccompWhitelist, bpfInput string, expected int) { + // Common syscalls we need to allow for a minimal statically linked + // c program. + // + // If we compile a test program for each test we can get away with + // a even smaller set of syscalls: execve,exit essentially. But it + // means a much longer test run (30s vs 2s). Commit d288d89 contains + // the code for this. + common := ` +execve +uname +brk +arch_prctl +readlink +access +sysinfo +exit +# i386 +set_thread_area +# armhf +set_tls +# arm64 +readlinkat +faccessat +# i386 from amd64 +restart_syscall +` + bpfPath := filepath.Join(c.MkDir(), "bpf") + err := main.Compile([]byte(common+seccompWhitelist), bpfPath) + c.Assert(err, IsNil) + + // default syscall runner + syscallRunner := s.seccompSyscallRunner + + // syscallName;arch;arg1,arg2... + l := strings.Split(bpfInput, ";") + syscallName := l[0] + syscallArch := "native" + if len(l) > 1 { + syscallArch = l[1] + } + + syscallNr, err := seccomp.GetSyscallFromName(syscallName) + c.Assert(err, IsNil) + + // Check if we want to test non-native architecture + // handling. Doing this via the in-kernel tests is tricky as + // we need a kernel that can run the architecture and a + // compiler that can produce the required binaries. Currently + // we only test amd64 running i386 here. + if syscallArch != "native" { + syscallNr, err = seccomp.GetSyscallFromNameByArch(syscallName, main.UbuntuArchToScmpArch(syscallArch)) + c.Assert(err, IsNil) + + switch syscallArch { + case "amd64": + // default syscallRunner + case "i386": + syscallRunner = s.seccompSyscallRunner + ".m32" + default: + c.Errorf("unexpected non-native arch: %s", syscallArch) + } + } + switch { + case syscallNr == -101: + // "socket" + // see libseccomp: _s390x_sock_demux(), _x86_sock_demux() + // the -101 is translated to 359 (socket) + syscallNr = 359 + case syscallNr == -10165: + // "mknod" on arm64 is not available at all on arm64 + // only "mknodat" but libseccomp will not generate a + // "mknodat" whitelist, it geneates a whitelist with + // syscall -10165 (!?!) so we cannot test this. + c.Skip("skipping mknod tests on arm64") + case syscallNr < 0: + c.Errorf("failed to resolve %v: %v", l[0], syscallNr) + return + } + + var syscallRunnerArgs [7]string + syscallRunnerArgs[0] = strconv.FormatInt(int64(syscallNr), 10) + if len(l) > 2 { + args := strings.Split(l[2], ",") + for i := range args { + // init with random number argument + syscallArg := (uint64)(rand.Uint32()) + // override if the test specifies a specific number + if nr, err := strconv.ParseUint(args[i], 10, 64); err == nil { + syscallArg = nr + } else if nr, ok := main.SeccompResolver[args[i]]; ok { + syscallArg = nr + } + syscallRunnerArgs[i+1] = strconv.FormatUint(syscallArg, 10) + } + } + + cmd := exec.Command(s.seccompBpfLoader, bpfPath, syscallRunner, syscallRunnerArgs[0], syscallRunnerArgs[1], syscallRunnerArgs[2], syscallRunnerArgs[3], syscallRunnerArgs[4], syscallRunnerArgs[5], syscallRunnerArgs[6]) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + switch expected { + case Allow: + if err != nil { + c.Fatalf("unexpected error for %q (failed to run %q)", seccompWhitelist, err) + } + case Deny: + if err == nil { + c.Fatalf("unexpected success for %q %q (ran but should have failed)", seccompWhitelist, bpfInput) + } + default: + c.Fatalf("unknown expected result %v", expected) + } +} + +func (s *snapSeccompSuite) TestUnrestricted(c *C) { + inp := "@unrestricted\n" + outPath := filepath.Join(c.MkDir(), "bpf") + err := main.Compile([]byte(inp), outPath) + c.Assert(err, IsNil) + + c.Check(outPath, testutil.FileEquals, inp) +} + +// TestCompile iterates over a range of textual seccomp whitelist rules and +// mocked kernel syscall input. For each rule, the test consists of compiling +// the rule into a bpf program and then running that program on a virtual bpf +// machine and comparing the bpf machine output to the specified expected +// output and seccomp operation. Eg: +// {"", "", } +// +// Eg to test that the rule 'read >=2' is allowed with 'read(2)' and 'read(3)' +// and denied with 'read(1)' and 'read(0)', add the following tests: +// {"read >=2", "read;native;2", Allow}, +// {"read >=2", "read;native;3", Allow}, +// {"read >=2", "read;native;1", main.SeccompRetKill}, +// {"read >=2", "read;native;0", main.SeccompRetKill}, +func (s *snapSeccompSuite) TestCompile(c *C) { + + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // special + {"@complain", "execve", Allow}, + + // trivial allow + {"read", "read", Allow}, + {"read\nwrite\nexecve\n", "write", Allow}, + + // trivial denial + {"read", "ioctl", Deny}, + + // test argument filtering syntax, we currently support: + // >=, <=, !, <, >, | + // modifiers. + + // reads >= 2 are ok + {"read >=2", "read;native;2", Allow}, + {"read >=2", "read;native;3", Allow}, + // but not reads < 2, those get killed + {"read >=2", "read;native;1", Deny}, + {"read >=2", "read;native;0", Deny}, + + // reads <= 2 are ok + {"read <=2", "read;native;0", Allow}, + {"read <=2", "read;native;1", Allow}, + {"read <=2", "read;native;2", Allow}, + // but not reads >2, those get killed + {"read <=2", "read;native;3", Deny}, + {"read <=2", "read;native;4", Deny}, + + // reads that are not 2 are ok + {"read !2", "read;native;1", Allow}, + {"read !2", "read;native;3", Allow}, + // but not 2, this gets killed + {"read !2", "read;native;2", Deny}, + + // reads > 2 are ok + {"read >2", "read;native;4", Allow}, + {"read >2", "read;native;3", Allow}, + // but not reads <= 2, those get killed + {"read >2", "read;native;2", Deny}, + {"read >2", "read;native;1", Deny}, + + // reads < 2 are ok + {"read <2", "read;native;0", Allow}, + {"read <2", "read;native;1", Allow}, + // but not reads >= 2, those get killed + {"read <2", "read;native;2", Deny}, + {"read <2", "read;native;3", Deny}, + + // FIXME: test maskedEqual better + {"read |1", "read;native;1", Allow}, + {"read |1", "read;native;2", Deny}, + + // exact match, reads == 2 are ok + {"read 2", "read;native;2", Allow}, + // but not those != 2 + {"read 2", "read;native;3", Deny}, + {"read 2", "read;native;1", Deny}, + + // test actual syscalls and their expected usage + {"ioctl - TIOCSTI", "ioctl;native;-,TIOCSTI", Allow}, + {"ioctl - TIOCSTI", "ioctl;native;-,99", Deny}, + {"ioctl - !TIOCSTI", "ioctl;native;-,TIOCSTI", Deny}, + + // test_bad_seccomp_filter_args_clone + {"setns - CLONE_NEWNET", "setns;native;-,99", Deny}, + {"setns - CLONE_NEWNET", "setns;native;-,CLONE_NEWNET", Allow}, + + // test_bad_seccomp_filter_args_mknod + {"mknod - |S_IFIFO", "mknod;native;-,S_IFIFO", Allow}, + {"mknod - |S_IFIFO", "mknod;native;-,99", Deny}, + + // test_bad_seccomp_filter_args_prctl + {"prctl PR_CAP_AMBIENT_RAISE", "prctl;native;PR_CAP_AMBIENT_RAISE", Allow}, + {"prctl PR_CAP_AMBIENT_RAISE", "prctl;native;99", Deny}, + + // test_bad_seccomp_filter_args_prio + {"setpriority PRIO_PROCESS 0 >=0", "setpriority;native;PRIO_PROCESS,0,19", Allow}, + {"setpriority PRIO_PROCESS 0 >=0", "setpriority;native;99", Deny}, + + // test_bad_seccomp_filter_args_quotactl + {"quotactl Q_GETQUOTA", "quotactl;native;Q_GETQUOTA", Allow}, + {"quotactl Q_GETQUOTA", "quotactl;native;99", Deny}, + + // test_bad_seccomp_filter_args_termios + {"ioctl - TIOCSTI", "ioctl;native;-,TIOCSTI", Allow}, + {"ioctl - TIOCSTI", "ioctl;native;-,99", Deny}, + + // u:root g:root + {"fchown - u:root g:root", "fchown;native;-,0,0", Allow}, + {"fchown - u:root g:root", "fchown;native;-,99,0", Deny}, + {"chown - u:root g:root", "chown;native;-,0,0", Allow}, + {"chown - u:root g:root", "chown;native;-,99,0", Deny}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +// TestCompileSocket runs in a separate tests so that only this part +// can be skipped when "socketcall()" is used instead of "socket()". +// +// Some architectures (i386, s390x) use the "socketcall" syscall instead +// of "socket". This is the case on Ubuntu 14.04, 17.04, 17.10 +func (s *snapSeccompSuite) TestCompileSocket(c *C) { + if release.ReleaseInfo.ID == "ubuntu" && release.ReleaseInfo.VersionID == "14.04" { + c.Skip("14.04/i386 uses socketcall which cannot be tested here") + } + + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + + // test_bad_seccomp_filter_args_socket + {"socket AF_UNIX", "socket;native;AF_UNIX", Allow}, + {"socket AF_UNIX", "socket;native;99", Deny}, + {"socket - SOCK_STREAM", "socket;native;-,SOCK_STREAM", Allow}, + {"socket - SOCK_STREAM", "socket;native;-,99", Deny}, + {"socket AF_CONN", "socket;native;AF_CONN", Allow}, + {"socket AF_CONN", "socket;native;99", Deny}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } + +} + +func (s *snapSeccompSuite) TestCompileBadInput(c *C) { + for _, t := range []struct { + inp string + errMsg string + }{ + // test_bad_seccomp_filter_args_clone (various typos in input) + {"setns - CLONE_NEWNE", `cannot parse line: cannot parse token "CLONE_NEWNE" \(line "setns - CLONE_NEWNE"\)`}, + {"setns - CLONE_NEWNETT", `cannot parse line: cannot parse token "CLONE_NEWNETT" \(line "setns - CLONE_NEWNETT"\)`}, + {"setns - CL0NE_NEWNET", `cannot parse line: cannot parse token "CL0NE_NEWNET" \(line "setns - CL0NE_NEWNET"\)`}, + + // test_bad_seccomp_filter_args_mknod (various typos in input) + {"mknod - |S_IFIF", `cannot parse line: cannot parse token "S_IFIF" \(line "mknod - |S_IFIF"\)`}, + {"mknod - |S_IFIFOO", `cannot parse line: cannot parse token "S_IFIFOO" \(line "mknod - |S_IFIFOO"\)`}, + {"mknod - |S_!FIFO", `cannot parse line: cannot parse token "S_IFIFO" \(line "mknod - |S_!FIFO"\)`}, + + // test_bad_seccomp_filter_args_null + {"socket S\x00CK_STREAM", `cannot parse line: cannot parse token .*`}, + {"socket SOCK_STREAM\x00bad stuff", `cannot parse line: cannot parse token .*`}, + + // test_bad_seccomp_filter_args + {"setpriority bar", `cannot parse line: cannot parse token "bar" .*`}, + {"setpriority -1", `cannot parse line: cannot parse token "-1" .*`}, + {"setpriority 0 - -1 0", `cannot parse line: cannot parse token "-1" .*`}, + {"setpriority --10", `cannot parse line: cannot parse token "--10" .*`}, + {"setpriority 0:10", `cannot parse line: cannot parse token "0:10" .*`}, + {"setpriority 0-10", `cannot parse line: cannot parse token "0-10" .*`}, + {"setpriority 0,1", `cannot parse line: cannot parse token "0,1" .*`}, + {"setpriority 0x0", `cannot parse line: cannot parse token "0x0" .*`}, + {"setpriority a1", `cannot parse line: cannot parse token "a1" .*`}, + {"setpriority 1a", `cannot parse line: cannot parse token "1a" .*`}, + {"setpriority 1-", `cannot parse line: cannot parse token "1-" .*`}, + {"setpriority 1\\ 2", `cannot parse line: cannot parse token "1\\\\" .*`}, + {"setpriority 1\\n2", `cannot parse line: cannot parse token "1\\\\n2" .*`}, + {"setpriority 999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", `cannot parse line: cannot parse token "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999" .*`}, + {"mbind - - - - - - 7", `cannot parse line: too many arguments specified for syscall 'mbind' in line.*`}, + {"mbind 1 2 3 4 5 6 7", `cannot parse line: too many arguments specified for syscall 'mbind' in line.*`}, + // test_bad_seccomp_filter_args_prctl + {"prctl PR_GET_SECCOM", `cannot parse line: cannot parse token "PR_GET_SECCOM" .*`}, + {"prctl PR_GET_SECCOMPP", `cannot parse line: cannot parse token "PR_GET_SECCOMPP" .*`}, + {"prctl PR_GET_SECC0MP", `cannot parse line: cannot parse token "PR_GET_SECC0MP" .*`}, + {"prctl PR_CAP_AMBIENT_RAIS", `cannot parse line: cannot parse token "PR_CAP_AMBIENT_RAIS" .*`}, + {"prctl PR_CAP_AMBIENT_RAISEE", `cannot parse line: cannot parse token "PR_CAP_AMBIENT_RAISEE" .*`}, + // test_bad_seccomp_filter_args_prio + {"setpriority PRIO_PROCES 0 >=0", `cannot parse line: cannot parse token "PRIO_PROCES" .*`}, + {"setpriority PRIO_PROCESSS 0 >=0", `cannot parse line: cannot parse token "PRIO_PROCESSS" .*`}, + {"setpriority PRIO_PR0CESS 0 >=0", `cannot parse line: cannot parse token "PRIO_PR0CESS" .*`}, + // test_bad_seccomp_filter_args_quotactl + {"quotactl Q_GETQUOT", `cannot parse line: cannot parse token "Q_GETQUOT" .*`}, + {"quotactl Q_GETQUOTAA", `cannot parse line: cannot parse token "Q_GETQUOTAA" .*`}, + {"quotactl Q_GETQU0TA", `cannot parse line: cannot parse token "Q_GETQU0TA" .*`}, + // test_bad_seccomp_filter_args_socket + {"socket AF_UNI", `cannot parse line: cannot parse token "AF_UNI" .*`}, + {"socket AF_UNIXX", `cannot parse line: cannot parse token "AF_UNIXX" .*`}, + {"socket AF_UN!X", `cannot parse line: cannot parse token "AF_UN!X" .*`}, + {"socket - SOCK_STREA", `cannot parse line: cannot parse token "SOCK_STREA" .*`}, + {"socket - SOCK_STREAMM", `cannot parse line: cannot parse token "SOCK_STREAMM" .*`}, + {"socket - NETLINK_ROUT", `cannot parse line: cannot parse token "NETLINK_ROUT" .*`}, + {"socket - NETLINK_ROUTEE", `cannot parse line: cannot parse token "NETLINK_ROUTEE" .*`}, + {"socket - NETLINK_R0UTE", `cannot parse line: cannot parse token "NETLINK_R0UTE" .*`}, + // test_bad_seccomp_filter_args_termios + {"ioctl - TIOCST", `cannot parse line: cannot parse token "TIOCST" .*`}, + {"ioctl - TIOCSTII", `cannot parse line: cannot parse token "TIOCSTII" .*`}, + {"ioctl - TIOCST1", `cannot parse line: cannot parse token "TIOCST1" .*`}, + // ensure missing numbers are caught + {"setpriority >", `cannot parse line: cannot parse token ">" .*`}, + {"setpriority >=", `cannot parse line: cannot parse token ">=" .*`}, + {"setpriority <", `cannot parse line: cannot parse token "<" .*`}, + {"setpriority <=", `cannot parse line: cannot parse token "<=" .*`}, + {"setpriority |", `cannot parse line: cannot parse token "|" .*`}, + {"setpriority !", `cannot parse line: cannot parse token "!" .*`}, + + // u: + {"setuid :root", `cannot parse line: cannot parse token ":root" .*`}, + {"setuid u:", `cannot parse line: cannot parse token "u:" \(line "setuid u:"\): "" must be a valid username`}, + {"setuid u:0", `cannot parse line: cannot parse token "u:0" \(line "setuid u:0"\): "0" must be a valid username`}, + {"setuid u:b@d|npu+", `cannot parse line: cannot parse token "u:b@d|npu+" \(line "setuid u:b@d|npu+"\): "b@d|npu+" must be a valid username`}, + {"setuid u:snap.bad", `cannot parse line: cannot parse token "u:snap.bad" \(line "setuid u:snap.bad"\): "snap.bad" must be a valid username`}, + {"setuid U:root", `cannot parse line: cannot parse token "U:root" .*`}, + {"setuid u:nonexistent", `cannot parse line: cannot parse token "u:nonexistent" \(line "setuid u:nonexistent"\): user: unknown user nonexistent`}, + // g: + {"setgid g:", `cannot parse line: cannot parse token "g:" \(line "setgid g:"\): "" must be a valid group name`}, + {"setgid g:0", `cannot parse line: cannot parse token "g:0" \(line "setgid g:0"\): "0" must be a valid group name`}, + {"setgid g:b@d|npu+", `cannot parse line: cannot parse token "g:b@d|npu+" \(line "setgid g:b@d|npu+"\): "b@d|npu+" must be a valid group name`}, + {"setgid g:snap.bad", `cannot parse line: cannot parse token "g:snap.bad" \(line "setgid g:snap.bad"\): "snap.bad" must be a valid group name`}, + {"setgid G:root", `cannot parse line: cannot parse token "G:root" .*`}, + {"setgid g:nonexistent", `cannot parse line: cannot parse token "g:nonexistent" \(line "setgid g:nonexistent"\): group: unknown group nonexistent`}, + } { + outPath := filepath.Join(c.MkDir(), "bpf") + err := main.Compile([]byte(t.inp), outPath) + c.Check(err, ErrorMatches, t.errMsg, Commentf("%q errors in unexpected ways, got: %q expected %q", t.inp, err, t.errMsg)) + } +} + +// ported from test_restrictions_working_args_socket +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsSocket(c *C) { + if release.ReleaseInfo.ID == "ubuntu" && release.ReleaseInfo.VersionID == "14.04" { + c.Skip("14.04/i386 uses socketcall which cannot be tested here") + } + + for _, pre := range []string{"AF", "PF"} { + for _, i := range []string{"UNIX", "LOCAL", "INET", "INET6", "IPX", "NETLINK", "X25", "AX25", "ATMPVC", "APPLETALK", "PACKET", "ALG", "CAN", "BRIDGE", "NETROM", "ROSE", "NETBEUI", "SECURITY", "KEY", "ASH", "ECONET", "SNA", "IRDA", "PPPOX", "WANPIPE", "BLUETOOTH", "RDS", "LLC", "TIPC", "IUCV", "RXRPC", "ISDN", "PHONET", "IEEE802154", "CAIF", "NFC", "VSOCK", "MPLS", "IB"} { + seccompWhitelist := fmt.Sprintf("socket %s_%s", pre, i) + bpfInputGood := fmt.Sprintf("socket;native;%s_%s", pre, i) + bpfInputBad := "socket;native;99999" + s.runBpf(c, seccompWhitelist, bpfInputGood, Allow) + s.runBpf(c, seccompWhitelist, bpfInputBad, Deny) + + for _, j := range []string{"SOCK_STREAM", "SOCK_DGRAM", "SOCK_SEQPACKET", "SOCK_RAW", "SOCK_RDM", "SOCK_PACKET"} { + seccompWhitelist := fmt.Sprintf("socket %s_%s %s", pre, i, j) + bpfInputGood := fmt.Sprintf("socket;native;%s_%s,%s", pre, i, j) + bpfInputBad := fmt.Sprintf("socket;native;%s_%s,9999", pre, i) + s.runBpf(c, seccompWhitelist, bpfInputGood, Allow) + s.runBpf(c, seccompWhitelist, bpfInputBad, Deny) + } + } + } + + for _, i := range []string{"NETLINK_ROUTE", "NETLINK_USERSOCK", "NETLINK_FIREWALL", "NETLINK_SOCK_DIAG", "NETLINK_NFLOG", "NETLINK_XFRM", "NETLINK_SELINUX", "NETLINK_ISCSI", "NETLINK_AUDIT", "NETLINK_FIB_LOOKUP", "NETLINK_CONNECTOR", "NETLINK_NETFILTER", "NETLINK_IP6_FW", "NETLINK_DNRTMSG", "NETLINK_KOBJECT_UEVENT", "NETLINK_GENERIC", "NETLINK_SCSITRANSPORT", "NETLINK_ECRYPTFS", "NETLINK_RDMA", "NETLINK_CRYPTO", "NETLINK_INET_DIAG"} { + for _, j := range []string{"AF_NETLINK", "PF_NETLINK"} { + seccompWhitelist := fmt.Sprintf("socket %s - %s", j, i) + bpfInputGood := fmt.Sprintf("socket;native;%s,0,%s", j, i) + bpfInputBad := fmt.Sprintf("socket;native;%s,0,99", j) + s.runBpf(c, seccompWhitelist, bpfInputGood, Allow) + s.runBpf(c, seccompWhitelist, bpfInputBad, Deny) + } + } +} + +// ported from test_restrictions_working_args_quotactl +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsQuotactl(c *C) { + for _, arg := range []string{"Q_QUOTAON", "Q_QUOTAOFF", "Q_GETQUOTA", "Q_SETQUOTA", "Q_GETINFO", "Q_SETINFO", "Q_GETFMT", "Q_SYNC", "Q_XQUOTAON", "Q_XQUOTAOFF", "Q_XGETQUOTA", "Q_XSETQLIM", "Q_XGETQSTAT", "Q_XQUOTARM"} { + // good input + seccompWhitelist := fmt.Sprintf("quotactl %s", arg) + bpfInputGood := fmt.Sprintf("quotactl;native;%s", arg) + s.runBpf(c, seccompWhitelist, bpfInputGood, Allow) + // bad input + for _, bad := range []string{"quotactl;native;99999", "read;native;"} { + s.runBpf(c, seccompWhitelist, bad, Deny) + } + } +} + +// ported from test_restrictions_working_args_prctl +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsPrctl(c *C) { + for _, arg := range []string{"PR_CAP_AMBIENT", "PR_CAP_AMBIENT_RAISE", "PR_CAP_AMBIENT_LOWER", "PR_CAP_AMBIENT_IS_SET", "PR_CAP_AMBIENT_CLEAR_ALL", "PR_CAPBSET_READ", "PR_CAPBSET_DROP", "PR_SET_CHILD_SUBREAPER", "PR_GET_CHILD_SUBREAPER", "PR_SET_DUMPABLE", "PR_GET_DUMPABLE", "PR_GET_ENDIAN", "PR_SET_FPEMU", "PR_GET_FPEMU", "PR_SET_FPEXC", "PR_GET_FPEXC", "PR_SET_KEEPCAPS", "PR_GET_KEEPCAPS", "PR_MCE_KILL", "PR_MCE_KILL_GET", "PR_SET_MM", "PR_SET_MM_START_CODE", "PR_SET_MM_END_CODE", "PR_SET_MM_START_DATA", "PR_SET_MM_END_DATA", "PR_SET_MM_START_STACK", "PR_SET_MM_START_BRK", "PR_SET_MM_BRK", "PR_SET_MM_ARG_START", "PR_SET_MM_ARG_END", "PR_SET_MM_ENV_START", "PR_SET_MM_ENV_END", "PR_SET_MM_AUXV", "PR_SET_MM_EXE_FILE", "PR_MPX_ENABLE_MANAGEMENT", "PR_MPX_DISABLE_MANAGEMENT", "PR_SET_NAME", "PR_GET_NAME", "PR_SET_NO_NEW_PRIVS", "PR_GET_NO_NEW_PRIVS", "PR_SET_PDEATHSIG", "PR_GET_PDEATHSIG", "PR_SET_PTRACER", "PR_SET_SECCOMP", "PR_GET_SECCOMP", "PR_SET_SECUREBITS", "PR_GET_SECUREBITS", "PR_SET_THP_DISABLE", "PR_TASK_PERF_EVENTS_DISABLE", "PR_TASK_PERF_EVENTS_ENABLE", "PR_GET_THP_DISABLE", "PR_GET_TID_ADDRESS", "PR_SET_TIMERSLACK", "PR_GET_TIMERSLACK", "PR_SET_TIMING", "PR_GET_TIMING", "PR_SET_TSC", "PR_GET_TSC", "PR_SET_UNALIGN", "PR_GET_UNALIGN"} { + // good input + seccompWhitelist := fmt.Sprintf("prctl %s", arg) + bpfInputGood := fmt.Sprintf("prctl;native;%s", arg) + s.runBpf(c, seccompWhitelist, bpfInputGood, Allow) + // bad input + for _, bad := range []string{"prctl;native;99999", "setpriority;native;"} { + s.runBpf(c, seccompWhitelist, bad, Deny) + } + + if arg == "PR_CAP_AMBIENT" { + for _, j := range []string{"PR_CAP_AMBIENT_RAISE", "PR_CAP_AMBIENT_LOWER", "PR_CAP_AMBIENT_IS_SET", "PR_CAP_AMBIENT_CLEAR_ALL"} { + seccompWhitelist := fmt.Sprintf("prctl %s %s", arg, j) + bpfInputGood := fmt.Sprintf("prctl;native;%s,%s", arg, j) + s.runBpf(c, seccompWhitelist, bpfInputGood, Allow) + for _, bad := range []string{ + fmt.Sprintf("prctl;native;%s,99999", arg), + "setpriority;native;", + } { + s.runBpf(c, seccompWhitelist, bad, Deny) + } + } + } + } +} + +// ported from test_restrictions_working_args_clone +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsClone(c *C) { + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // good input + {"setns - CLONE_NEWIPC", "setns;native;-,CLONE_NEWIPC", Allow}, + {"setns - CLONE_NEWNET", "setns;native;-,CLONE_NEWNET", Allow}, + {"setns - CLONE_NEWNS", "setns;native;-,CLONE_NEWNS", Allow}, + {"setns - CLONE_NEWPID", "setns;native;-,CLONE_NEWPID", Allow}, + {"setns - CLONE_NEWUSER", "setns;native;-,CLONE_NEWUSER", Allow}, + {"setns - CLONE_NEWUTS", "setns;native;-,CLONE_NEWUTS", Allow}, + // bad input + {"setns - CLONE_NEWIPC", "setns;native;-,99", Deny}, + {"setns - CLONE_NEWNET", "setns;native;-,99", Deny}, + {"setns - CLONE_NEWNS", "setns;native;-,99", Deny}, + {"setns - CLONE_NEWPID", "setns;native;-,99", Deny}, + {"setns - CLONE_NEWUSER", "setns;native;-,99", Deny}, + {"setns - CLONE_NEWUTS", "setns;native;-,99", Deny}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +// ported from test_restrictions_working_args_mknod +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsMknod(c *C) { + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // good input + {"mknod - S_IFREG", "mknod;native;-,S_IFREG", Allow}, + {"mknod - S_IFCHR", "mknod;native;-,S_IFCHR", Allow}, + {"mknod - S_IFBLK", "mknod;native;-,S_IFBLK", Allow}, + {"mknod - S_IFIFO", "mknod;native;-,S_IFIFO", Allow}, + {"mknod - S_IFSOCK", "mknod;native;-,S_IFSOCK", Allow}, + // bad input + {"mknod - S_IFREG", "mknod;native;-,999", Deny}, + {"mknod - S_IFCHR", "mknod;native;-,999", Deny}, + {"mknod - S_IFBLK", "mknod;native;-,999", Deny}, + {"mknod - S_IFIFO", "mknod;native;-,999", Deny}, + {"mknod - S_IFSOCK", "mknod;native;-,999", Deny}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +// ported from test_restrictions_working_args_prio +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsPrio(c *C) { + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // good input + {"setpriority PRIO_PROCESS", "setpriority;native;PRIO_PROCESS", Allow}, + {"setpriority PRIO_PGRP", "setpriority;native;PRIO_PGRP", Allow}, + {"setpriority PRIO_USER", "setpriority;native;PRIO_USER", Allow}, + // bad input + {"setpriority PRIO_PROCESS", "setpriority;native;99", Deny}, + {"setpriority PRIO_PGRP", "setpriority;native;99", Deny}, + {"setpriority PRIO_USER", "setpriority;native;99", Deny}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +// ported from test_restrictions_working_args_termios +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsTermios(c *C) { + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // good input + {"ioctl - TIOCSTI", "ioctl;native;-,TIOCSTI", Allow}, + // bad input + {"ioctl - TIOCSTI", "quotactl;native;-,99", Deny}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsUidGid(c *C) { + // while 'root' user usually has uid 0, 'daemon' user uid may vary + // across distributions, best lookup the uid directly + daemonUid, err := osutil.FindUid("daemon") + + if err != nil { + c.Skip("daemon user not available, perhaps we are in a buildroot jail") + } + + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // good input. 'root' is guaranteed to be '0' and 'daemon' uid + // was determined at runtime + {"setuid u:root", "setuid;native;0", Allow}, + {"setuid u:daemon", fmt.Sprintf("setuid;native;%v", daemonUid), Allow}, + {"setgid g:root", "setgid;native;0", Allow}, + {"setgid g:daemon", fmt.Sprintf("setgid;native;%v", daemonUid), Allow}, + // bad input + {"setuid u:root", "setuid;native;99", Deny}, + {"setuid u:daemon", "setuid;native;99", Deny}, + {"setgid g:root", "setgid;native;99", Deny}, + {"setgid g:daemon", "setgid;native;99", Deny}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +func (s *snapSeccompSuite) TestCompatArchWorks(c *C) { + if !s.canCheckCompatArch { + c.Skip("multi-lib syscall runner not supported by this host") + } + for _, t := range []struct { + arch string + seccompWhitelist string + bpfInput string + expected int + }{ + // on amd64 we add compat i386 + {"amd64", "read", "read;i386", Allow}, + {"amd64", "read", "read;amd64", Allow}, + } { + // It is tricky to mock the architecture here because + // seccomp is always adding the native arch to the seccomp + // filter and it will silently discard arches that have + // an endian mismatch: + // https://github.com/seccomp/libseccomp/issues/86 + // + // This means we can not just + // main.MockArchUbuntuArchitecture(t.arch) + // here because on endian mismatch the arch will *not* be + // added + if arch.UbuntuArchitecture() == t.arch { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } + } +} diff --git a/cmd/snap-update-ns/bootstrap.c b/cmd/snap-update-ns/bootstrap.c new file mode 100644 index 00000000..70a33cfd --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.c @@ -0,0 +1,499 @@ +/* + * Copyright (C) 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 . + * + */ + +// IMPORTANT: all the code in this file may be run with elevated privileges +// when invoking snap-update-ns from the setuid snap-confine. +// +// This file is a preprocessor for snap-update-ns' main() function. It will +// perform input validation and clear the environment so that snap-update-ns' +// go code runs with safe inputs when called by the setuid() snap-confine. + +#include "bootstrap.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// bootstrap_errno contains a copy of errno if a system call fails. +int bootstrap_errno = 0; +// bootstrap_msg contains a static string if something fails. +const char *bootstrap_msg = NULL; + +// setns_into_snap switches mount namespace into that of a given snap. +static int setns_into_snap(const char *snap_name) +{ + // Construct the name of the .mnt file to open. + char buf[PATH_MAX] = { + 0, + }; + int n = snprintf(buf, sizeof buf, "/run/snapd/ns/%s.mnt", snap_name); + if (n >= sizeof buf || n < 0) { + bootstrap_errno = 0; + bootstrap_msg = "cannot format mount namespace file name"; + return -1; + } + // Open the mount namespace file. + int fd = open(buf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if (fd < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot open mount namespace file"; + return -1; + } + // Switch to the mount namespace of the given snap. + int err = setns(fd, CLONE_NEWNS); + if (err < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot switch mount namespace"; + }; + + close(fd); + return err; +} + +// switch_to_privileged_user drops to the real user ID while retaining +// CAP_SYS_ADMIN, for operations such as mount(). +static int switch_to_privileged_user() +{ + uid_t real_uid; + gid_t real_gid; + + real_uid = getuid(); + if (real_uid == 0) { + // We're running as root: no need to switch IDs + return 0; + } + real_gid = getgid(); + + // _LINUX_CAPABILITY_VERSION_3 valid for kernel >= 2.6.26. See + // https://github.com/torvalds/linux/blob/master/kernel/capability.c + struct __user_cap_header_struct hdr = + { _LINUX_CAPABILITY_VERSION_3, 0 }; + struct __user_cap_data_struct data[2] = { {0} }; + + data[0].effective = (CAP_TO_MASK(CAP_SYS_ADMIN) | + CAP_TO_MASK(CAP_SETUID) | CAP_TO_MASK(CAP_SETGID)); + data[0].permitted = data[0].effective; + data[0].inheritable = 0; + data[1].effective = 0; + data[1].permitted = 0; + data[1].inheritable = 0; + + if (capset(&hdr, data) != 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot set permitted capabilities mask"; + return -1; + } + + if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) != 0) { + bootstrap_errno = errno; + bootstrap_msg = + "cannot tell kernel to keep capabilities over setuid"; + return -1; + } + + if (setgroups(1, &real_gid) != 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot drop supplementary groups"; + return -1; + } + + if (setgid(real_gid) != 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot switch to real group ID"; + return -1; + } + + if (setuid(real_uid) != 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot switch to real user ID"; + return -1; + } + // After changing uid, our effective capabilities were dropped. + // Reacquire CAP_SYS_ADMIN, and discard CAP_SETUID/CAP_SETGID. + data[0].effective = CAP_TO_MASK(CAP_SYS_ADMIN); + data[0].permitted = data[0].effective; + if (capset(&hdr, data) != 0) { + bootstrap_errno = errno; + bootstrap_msg = + "cannot enable capabilities after switching to real user"; + return -1; + } + + return 0; +} + +// TODO: reuse the code from snap-confine, if possible. +static int skip_lowercase_letters(const char **p) +{ + int skipped = 0; + const char *c; + for (c = *p; *c >= 'a' && *c <= 'z'; ++c) { + skipped += 1; + } + *p = (*p) + skipped; + return skipped; +} + +// TODO: reuse the code from snap-confine, if possible. +static int skip_digits(const char **p) +{ + int skipped = 0; + const char *c; + for (c = *p; *c >= '0' && *c <= '9'; ++c) { + skipped += 1; + } + *p = (*p) + skipped; + return skipped; +} + +// TODO: reuse the code from snap-confine, if possible. +static int skip_one_char(const char **p, char c) +{ + if (**p == c) { + *p += 1; + return 1; + } + return 0; +} + +// validate_snap_name performs full validation of the given name. +int validate_snap_name(const char *snap_name) +{ + // NOTE: This function should be synchronized with the two other + // implementations: sc_snap_name_validate and snap.ValidateName. + + // Ensure that name is not NULL + if (snap_name == NULL) { + bootstrap_msg = "snap name cannot be NULL"; + return -1; + } + // This is a regexp-free routine hand-codes the following pattern: + // + // "^([a-z0-9]+-?)*[a-z](-?[a-z0-9])*$" + // + // The only motivation for not using regular expressions is so that we + // don't run untrusted input against a potentially complex regular + // expression engine. + const char *p = snap_name; + if (skip_one_char(&p, '-')) { + bootstrap_msg = "snap name cannot start with a dash"; + return -1; + } + bool got_letter = false; + int n = 0, m; + for (; *p != '\0';) { + if ((m = skip_lowercase_letters(&p)) > 0) { + n += m; + got_letter = true; + continue; + } + if ((m = skip_digits(&p)) > 0) { + n += m; + continue; + } + if (skip_one_char(&p, '-') > 0) { + n++; + if (*p == '\0') { + bootstrap_msg = + "snap name cannot end with a dash"; + return -1; + } + if (skip_one_char(&p, '-') > 0) { + bootstrap_msg = + "snap name cannot contain two consecutive dashes"; + return -1; + } + continue; + } + bootstrap_msg = + "snap name must use lower case letters, digits or dashes"; + return -1; + } + if (!got_letter) { + bootstrap_msg = "snap name must contain at least one letter"; + return -1; + } + if (n < 2) { + bootstrap_msg = "snap name must be longer than 1 character"; + return -1; + } + if (n > 40) { + bootstrap_msg = "snap name must be shorter than 40 characters"; + return -1; + } + + bootstrap_msg = NULL; + return 0; +} + +static int instance_key_validate(const char *instance_key) +{ + // NOTE: see snap.ValidateInstanceName for reference of a valid instance key + // format + + // Ensure that name is not NULL + if (instance_key == NULL) { + bootstrap_msg = "instance key cannot be NULL"; + return -1; + } + // This is a regexp-free routine hand-coding the following pattern: + // + // "^[a-z]{1,10}$" + // + // The only motivation for not using regular expressions is so that we don't + // run untrusted input against a potentially complex regular expression + // engine. + int i = 0; + for (i = 0; instance_key[i] != '\0'; i++) { + if (islower(instance_key[i]) || isdigit(instance_key[i])) { + continue; + } + bootstrap_msg = + "instance key must use lower case letters or digits"; + return -1; + } + + if (i == 0) { + bootstrap_msg = + "instance key must contain at least one letter or digit"; + return -1; + } else if (i > 10) { + bootstrap_msg = + "instance key must be shorter than 10 characters"; + return -1; + } + return 0; +} + +// validate_instance_name performs full validation of the given snap instance name. +int validate_instance_name(const char *instance_name) +{ + // NOTE: This function should be synchronized with the two other + // implementations: sc_instance_name_validate and snap.ValidateInstanceName. + + if (instance_name == NULL) { + bootstrap_msg = "snap instance name cannot be NULL"; + return -1; + } + // 40 char snap_name + '_' + 10 char instance_key + 1 extra overflow + 1 + // NULL + char s[53] = { 0 }; + strncpy(s, instance_name, sizeof(s) - 1); + + char *t = s; + const char *snap_name = strsep(&t, "_"); + const char *instance_key = strsep(&t, "_"); + const char *third_separator = strsep(&t, "_"); + if (third_separator != NULL) { + bootstrap_msg = + "snap instance name can contain only one underscore"; + return -1; + } + + if (validate_snap_name(snap_name) < 0) { + return -1; + } + // When the instance_name is a normal snap name, instance_key will be + // NULL, so only validate instance_key when we found one. + if (instance_key != NULL && instance_key_validate(instance_key) < 0) { + return -1; + } + + return 0; +} + +// parse the -u argument, returns -1 on failure or 0 on success. +static int parse_arg_u(int argc, char * const *argv, int *optind, unsigned long *uid_out) +{ + if (*optind + 1 == argc || argv[*optind + 1] == NULL) { + bootstrap_msg = "-u requires an argument"; + bootstrap_errno = 0; + return -1; + } + const char *uid_text = argv[*optind + 1]; + errno = 0; + char *uid_text_end = NULL; + unsigned long parsed_uid = strtoul(uid_text, &uid_text_end, 10); + if ( + /* Reject overflow in parsed representation */ + (parsed_uid == ULONG_MAX && errno != 0) + /* Reject leading whitespace allowed by strtoul. */ + || (isspace(*uid_text)) + /* Reject empty string. */ + || (*uid_text == '\0') + /* Reject partially parsed strings. */ + || (*uid_text != '\0' && uid_text_end != NULL + && *uid_text_end != '\0')) { + bootstrap_msg = "cannot parse user id"; + bootstrap_errno = errno; + return -1; + } + if ((long)parsed_uid < 0) { + bootstrap_msg = "user id cannot be negative"; + bootstrap_errno = 0; + return -1; + } + if (uid_out != NULL) { + *uid_out = parsed_uid; + } + *optind += 1; // Account for the argument to -u. + return 0; +} + +// process_arguments parses given a command line +// argc and argv are defined as for the main() function +void process_arguments(int argc, char *const *argv, const char **snap_name_out, + bool * should_setns_out, bool * process_user_fstab, unsigned long * uid_out) +{ + // Find the name of the called program. If it is ending with ".test" then do nothing. + // NOTE: This lets us use cgo/go to write tests without running the bulk + // of the code automatically. + // + if (argv == NULL || argc < 1) { + bootstrap_errno = 0; + bootstrap_msg = "argv0 is corrupted"; + return; + } + const char *argv0 = argv[0]; + const char *argv0_suffix_maybe = strstr(argv0, ".test"); + if (argv0_suffix_maybe != NULL + && argv0_suffix_maybe[strlen(".test")] == '\0') { + bootstrap_errno = 0; + bootstrap_msg = "bootstrap is not enabled while testing"; + return; + } + + bool should_setns = true; + bool user_fstab = false; + const char *snap_name = NULL; + + // Sanity check the command line arguments. The go parts will + // scan this too. + int i; + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (arg[0] == '-') { + /* We have an option */ + if (!strcmp(arg, "--from-snap-confine")) { + // When we are running under "--from-snap-confine" + // option skip the setns call as snap-confine has + // already placed us in the right namespace. + should_setns = false; + } else if (!strcmp(arg, "--user-mounts")) { + user_fstab = true; + // Processing the user-fstab file implies we're being + // called from snap-confine. + should_setns = false; + } else if (!strcmp(arg, "-u")) { + if (parse_arg_u(argc, argv, &i, uid_out)) { + return; + } + // Providing an user identifier implies we are performing an + // update of a specific user mount namespace and that we are + // invoked from snapd and we should setns ourselves. When + // invoked from snap-confine we are only called with + // --from-snap-confine and with --user-mounts. + should_setns = true; + user_fstab = true; + } else { + bootstrap_errno = 0; + bootstrap_msg = "unsupported option"; + return; + } + } else { + // We expect a single positional argument: the snap name + if (snap_name != NULL) { + bootstrap_errno = 0; + bootstrap_msg = "too many positional arguments"; + return; + } + snap_name = arg; + } + } + + // If there's no snap name given, just bail out. + if (snap_name == NULL) { + bootstrap_errno = 0; + bootstrap_msg = "snap name not provided"; + return; + } + // Ensure that the snap instance name is valid so that we don't blindly setns into + // something that is controlled by a potential attacker. + if (validate_instance_name(snap_name) < 0) { + bootstrap_errno = 0; + // bootstap_msg is set by validate_instance_name; + return; + } + // We have a valid snap name now so let's store it. + if (snap_name_out != NULL) { + *snap_name_out = snap_name; + } + if (should_setns_out != NULL) { + *should_setns_out = should_setns; + } + if (process_user_fstab != NULL) { + *process_user_fstab = user_fstab; + } + bootstrap_errno = 0; + bootstrap_msg = NULL; +} + +// bootstrap prepares snap-update-ns to work in the namespace of the snap given +// on command line. +void bootstrap(int argc, char **argv, char **envp) +{ + // We may have been started via a setuid-root snap-confine. In order to + // prevent environment-based attacks we start by erasing all environment + // variables. + char *snapd_debug = getenv("SNAPD_DEBUG"); + if (clearenv() != 0) { + bootstrap_errno = 0; + bootstrap_msg = "bootstrap could not clear the environment"; + return; + } + if (snapd_debug != NULL) { + setenv("SNAPD_DEBUG", snapd_debug, 0); + } + // Analyze the read process cmdline to find the snap name and decide if we + // should use setns to jump into the mount namespace of a particular snap. + // This is spread out for easier testability. + const char *snap_name = NULL; + bool should_setns = false; + bool process_user_fstab = false; + unsigned long uid = 0; + process_arguments(argc, argv, &snap_name, &should_setns, + &process_user_fstab, &uid); + if (process_user_fstab) { + switch_to_privileged_user(); + // switch_to_privileged_user sets bootstrap_{errno,msg} + } else if (snap_name != NULL && should_setns) { + setns_into_snap(snap_name); + // setns_into_snap sets bootstrap_{errno,msg} + } +} diff --git a/cmd/snap-update-ns/bootstrap.go b/cmd/snap-update-ns/bootstrap.go new file mode 100644 index 00000000..bb844aaa --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.go @@ -0,0 +1,134 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +// Use a pre-main helper to switch the mount namespace. This is required as +// golang creates threads at will and setns(..., CLONE_NEWNS) fails if any +// threads apart from the main thread exist. + +/* + +#include +#include "bootstrap.h" + +// The bootstrap function is called by the loader before passing +// control to main. We are using `preinit_array` rather than +// `init_array` because the Go linker adds its own initialisation +// function to `init_array`, and having ours run second would defeat +// the purpose of the C bootstrap code. +// +// The `used` attribute ensures that the compiler doesn't oprimise out +// the variable on the mistaken belief that it isn't used. +__attribute__((section(".preinit_array"), used)) static typeof(&bootstrap) init = &bootstrap; + +// NOTE: do not add anything before the following `import "C"' +*/ +import "C" + +import ( + "errors" + "fmt" + "syscall" + "unsafe" +) + +var ( + // ErrNoNamespace is returned when a snap namespace does not exist. + ErrNoNamespace = errors.New("cannot update mount namespace that was not created yet") +) + +// IMPORTANT: all the code in this section may be run with elevated privileges +// when invoking snap-update-ns from the setuid snap-confine. + +// BootstrapError returns error (if any) encountered in pre-main C code. +func BootstrapError() error { + if C.bootstrap_msg == nil { + return nil + } + errno := syscall.Errno(C.bootstrap_errno) + // Translate EINVAL from setns or ENOENT from open into a dedicated error. + if errno == syscall.EINVAL || errno == syscall.ENOENT { + return ErrNoNamespace + } + if errno != 0 { + return fmt.Errorf("%s: %s", C.GoString(C.bootstrap_msg), errno) + } + return fmt.Errorf("%s", C.GoString(C.bootstrap_msg)) +} + +// This function is here to make clearing the boostrap errors accessible +// from the tests. +func clearBootstrapError() { + C.bootstrap_msg = nil + C.bootstrap_errno = 0 +} + +// END IMPORTANT + +func makeArgv(args []string) []*C.char { + // Create argv array with terminating NULL element + argv := make([]*C.char, len(args)+1) + for i, arg := range args { + argv[i] = C.CString(arg) + } + return argv +} + +func freeArgv(argv []*C.char) { + for _, arg := range argv { + C.free(unsafe.Pointer(arg)) + } +} + +// validateInstanceName checks if snap instance name is valid. +// This also sets bootstrap_msg on failure. +// +// This function is here only to make the C.validate_instance_name +// code testable from go. +func validateInstanceName(instanceName string) int { + cStr := C.CString(instanceName) + defer C.free(unsafe.Pointer(cStr)) + return int(C.validate_instance_name(cStr)) +} + +// processArguments parses commnad line arguments. +// The argument cmdline is a string with embedded +// NUL bytes, separating particular arguments. +// +// This function is here only to make the C.validate_instance_name +// code testable from go. +func processArguments(args []string) (snapName string, shouldSetNs bool, processUserFstab bool, uid uint) { + argv := makeArgv(args) + defer freeArgv(argv) + + var snapNameOut *C.char + var shouldSetNsOut C.bool + var processUserFstabOut C.bool + var uidOut C.ulong + C.process_arguments(C.int(len(args)), &argv[0], &snapNameOut, &shouldSetNsOut, &processUserFstabOut, &uidOut) + if snapNameOut != nil { + snapName = C.GoString(snapNameOut) + } + shouldSetNs = bool(shouldSetNsOut) + processUserFstab = bool(processUserFstabOut) + uid = uint(uidOut) + + return snapName, shouldSetNs, processUserFstab, uid +} diff --git a/cmd/snap-update-ns/bootstrap.h b/cmd/snap-update-ns/bootstrap.h new file mode 100644 index 00000000..37701948 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 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 SNAPD_CMD_SNAP_UPDATE_NS_H +#define SNAPD_CMD_SNAP_UPDATE_NS_H + +#define _GNU_SOURCE + +#include +#include + +extern int bootstrap_errno; +extern const char *bootstrap_msg; + +void bootstrap(int argc, char **argv, char **envp); +void process_arguments(int argc, char *const *argv, const char **snap_name_out, + bool * should_setns_out, bool * process_user_fstab, unsigned long * uid_out); +int validate_instance_name(const char *instance_name); + +#endif diff --git a/cmd/snap-update-ns/bootstrap_ppc64le.go b/cmd/snap-update-ns/bootstrap_ppc64le.go new file mode 100644 index 00000000..fdfc2543 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap_ppc64le.go @@ -0,0 +1,31 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +// +build ppc64le,go1.7,!go1.8 + +/* + * Copyright (C) 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 . + * + */ + +package main + +/* +#cgo LDFLAGS: -no-pie + +// we need "-no-pie" for ppc64le,go1.7 to work around build failure on +// ppc64el with go1.7, see +// https://forum.snapcraft.io/t/snapd-master-fails-on-zesty-ppc64el-with-r-ppc64-addr16-ha-for-symbol-out-of-range/ +*/ +import "C" diff --git a/cmd/snap-update-ns/bootstrap_test.go b/cmd/snap-update-ns/bootstrap_test.go new file mode 100644 index 00000000..92eda827 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap_test.go @@ -0,0 +1,159 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" +) + +type bootstrapSuite struct{} + +var _ = Suite(&bootstrapSuite{}) + +// Check that ValidateSnapName rejects "/" and "..". +func (s *bootstrapSuite) TestValidateInstanceName(c *C) { + validNames := []string{ + "aa", + "aa_a", + "hello-world", + "a123456789012345678901234567890123456789", + "a123456789012345678901234567890123456789_0123456789", + "hello-world_foo", + "foo_0123456789", + "foo_1234abcd", + "a123456789012345678901234567890123456789", + "a123456789012345678901234567890123456789_0123456789", + } + for _, name := range validNames { + c.Check(update.ValidateInstanceName(name), Equals, 0, Commentf("name %q should be valid but is not", name)) + } + + invalidNames := []string{ + "", + "a", + "a_a", + "a123456789012345678901234567890123456789_01234567890", + "hello/world", + "hello..world", + "INVALID", + "-invalid", + "hello-world_", + "_foo", + "foo_01234567890", + "foo_123_456", + "foo__456", + "foo_", + "hello-world_foo_foo", + "foo01234567890012345678900123456789001234567890", + "foo01234567890012345678900123456789001234567890_foo", + "a123456789012345678901234567890123456789_0123456789_", + } + for _, name := range invalidNames { + c.Check(update.ValidateInstanceName(name), Equals, -1, Commentf("name %q should be invalid but is valid", name)) + } + +} + +// Test various cases of command line handling. +func (s *bootstrapSuite) TestProcessArguments(c *C) { + cases := []struct { + cmdline []string + snapName string + shouldSetNs bool + userFstab bool + uid uint + errPattern string + }{ + // Corrupted buffer is dealt with. + {[]string{}, "", false, false, 0, "argv0 is corrupted"}, + // When testing real bootstrap is identified and disabled. + {[]string{"argv0.test"}, "", false, false, 0, "bootstrap is not enabled while testing"}, + // Snap name is mandatory. + {[]string{"argv0"}, "", false, false, 0, "snap name not provided"}, + // Snap name is parsed correctly. + {[]string{"argv0", "snapname"}, "snapname", true, false, 0, ""}, + {[]string{"argv0", "snapname_instance"}, "snapname_instance", true, false, 0, ""}, + // Onlye one snap name is allowed. + {[]string{"argv0", "snapone", "snaptwo"}, "", false, false, 0, "too many positional arguments"}, + // Snap name is validated correctly. + {[]string{"argv0", ""}, "", false, false, 0, "snap name must contain at least one letter"}, + {[]string{"argv0", "in--valid"}, "", false, false, 0, "snap name cannot contain two consecutive dashes"}, + {[]string{"argv0", "invalid-"}, "", false, false, 0, "snap name cannot end with a dash"}, + {[]string{"argv0", "@invalid"}, "", false, false, 0, "snap name must use lower case letters, digits or dashes"}, + {[]string{"argv0", "INVALID"}, "", false, false, 0, "snap name must use lower case letters, digits or dashes"}, + {[]string{"argv0", "foo_01234567890"}, "", false, false, 0, "instance key must be shorter than 10 characters"}, + {[]string{"argv0", "foo_0123456_2"}, "", false, false, 0, "snap instance name can contain only one underscore"}, + // The option --from-snap-confine disables setns. + {[]string{"argv0", "--from-snap-confine", "snapname"}, "snapname", false, false, 0, ""}, + {[]string{"argv0", "snapname", "--from-snap-confine"}, "snapname", false, false, 0, ""}, + // The option --user-mounts switches to the real uid + {[]string{"argv0", "--user-mounts", "snapname"}, "snapname", false, true, 0, ""}, + // Unknown options are reported. + {[]string{"argv0", "-invalid"}, "", false, false, 0, "unsupported option"}, + {[]string{"argv0", "--option"}, "", false, false, 0, "unsupported option"}, + {[]string{"argv0", "--from-snap-confine", "-invalid", "snapname"}, "", false, false, 0, "unsupported option"}, + // The -u option can be used to specify the user id. + {[]string{"argv0", "snapname", "-u", "1234"}, "snapname", true, true, 1234, ""}, + {[]string{"argv0", "-u", "1234", "snapname"}, "snapname", true, true, 1234, ""}, + /* Empty user id is rejected. */ + {[]string{"argv0", "-u", "", "snapname"}, "", false, false, 0, "cannot parse user id"}, + /* Partially parsed values are rejected. */ + {[]string{"argv0", "-u", "1foo", "snapname"}, "", false, false, 0, "cannot parse user id"}, + /* Hexadecimal values are rejected. */ + {[]string{"argv0", "-u", "0x16", "snapname"}, "", false, false, 0, "cannot parse user id"}, + {[]string{"argv0", "-u", " 0x16", "snapname"}, "", false, false, 0, "cannot parse user id"}, + {[]string{"argv0", "-u", "0x16 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, + {[]string{"argv0", "-u", " 0x16 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, + /* Octal-looking values are parsed as decimal. */ + {[]string{"argv0", "-u", "042", "snapname"}, "snapname", true, true, 42, ""}, + /* Spaces around octal values is rejected. */ + {[]string{"argv0", "-u", " 042", "snapname"}, "", false, false, 0, "cannot parse user id"}, + {[]string{"argv0", "-u", "042 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, + {[]string{"argv0", "-u", " 042 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, + /* Space around the value is rejected. */ + {[]string{"argv0", "-u", "42 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, + {[]string{"argv0", "-u", " 42", "snapname"}, "", false, false, 0, "cannot parse user id"}, + {[]string{"argv0", "-u", " 42 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, + {[]string{"argv0", "-u", "\n42 ", "snapname"}, "", false, false, 0, "cannot parse user id"}, + {[]string{"argv0", "-u", "42\t", "snapname"}, "", false, false, 0, "cannot parse user id"}, + /* Negative values are rejected. */ + {[]string{"argv0", "-u", "-1", "snapname"}, "", false, false, 0, "user id cannot be negative"}, + /* The option -u requires an argument. */ + {[]string{"argv0", "snapname", "-u"}, "", false, false, 0, "-u requires an argument"}, + } + for _, tc := range cases { + update.ClearBootstrapError() + snapName, shouldSetNs, userFstab, uid := update.ProcessArguments(tc.cmdline) + err := update.BootstrapError() + comment := Commentf("failed with cmdline %q, expected error pattern %q, actual error %q", + tc.cmdline, tc.errPattern, err) + if tc.errPattern != "" { + c.Assert(err, ErrorMatches, tc.errPattern, comment) + } else { + c.Assert(err, IsNil, comment) + } + c.Check(snapName, Equals, tc.snapName, comment) + c.Check(shouldSetNs, Equals, tc.shouldSetNs, comment) + c.Check(userFstab, Equals, tc.userFstab, comment) + c.Check(uid, Equals, tc.uid, comment) + } +} diff --git a/cmd/snap-update-ns/change.go b/cmd/snap-update-ns/change.go new file mode 100644 index 00000000..dadd509a --- /dev/null +++ b/cmd/snap-update-ns/change.go @@ -0,0 +1,492 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "syscall" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" +) + +// Action represents a mount action (mount, remount, unmount, etc). +type Action string + +const ( + // Keep indicates that a given mount entry should be kept as-is. + Keep Action = "keep" + // Mount represents an action that results in mounting something somewhere. + Mount Action = "mount" + // Unmount represents an action that results in unmounting something from somewhere. + Unmount Action = "unmount" + // Remount when needed +) + +var ( + // ErrIgnoredMissingMount is returned when a mount entry has + // been marked with x-snapd.ignore-missing, and the mount + // source or target do not exist. + ErrIgnoredMissingMount = errors.New("mount source or target are missing") +) + +// Change describes a change to the mount table (action and the entry to act on). +type Change struct { + Entry osutil.MountEntry + Action Action +} + +// String formats mount change to a human-readable line. +func (c Change) String() string { + return fmt.Sprintf("%s (%s)", c.Action, c.Entry) +} + +// changePerform is Change.Perform that can be mocked for testing. +var changePerform func(*Change, *Assumptions) ([]*Change, error) + +// mimicRequired provides information if an error warrants a writable mimic. +// +// The returned path is the location where a mimic should be constructed. +func mimicRequired(err error) (needsMimic bool, path string) { + switch err.(type) { + case *ReadOnlyFsError: + rofsErr := err.(*ReadOnlyFsError) + return true, rofsErr.Path + case *TrespassingError: + tErr := err.(*TrespassingError) + return true, tErr.ViolatedPath + } + return false, "" +} + +func (c *Change) createPath(path string, pokeHoles bool, as *Assumptions) ([]*Change, error) { + // If we've been asked to create a missing path, and the mount + // entry uses the ignore-missing option, return an error. + if c.Entry.XSnapdIgnoreMissing() { + return nil, ErrIgnoredMissingMount + } + + var err error + var changes []*Change + + // In case we need to create something, some constants. + const ( + mode = 0755 + uid = 0 + gid = 0 + ) + + // If the element doesn't exist we can attempt to create it. We will + // create the parent directory and then the final element relative to it. + // The traversed space may be writable so we just try to create things + // first. + kind := c.Entry.XSnapdKind() + + // TODO: re-factor this, if possible, with inspection and preemptive + // creation after the current release ships. This should be possible but + // will affect tests heavily (churn, not safe before release). + rs := as.RestrictionsFor(path) + switch kind { + case "": + err = MkdirAll(path, mode, uid, gid, rs) + case "file": + err = MkfileAll(path, mode, uid, gid, rs) + case "symlink": + err = MksymlinkAll(path, mode, uid, gid, c.Entry.XSnapdSymlink(), rs) + } + if needsMimic, mimicPath := mimicRequired(err); needsMimic && pokeHoles { + // If the error can be recovered by using a writable mimic + // then construct one and try again. + changes, err = createWritableMimic(mimicPath, path, as) + if err != nil { + err = fmt.Errorf("cannot create writable mimic over %q: %s", mimicPath, err) + } else { + // Try once again. Note that we care *just* about the error. We have already + // performed the hole poking and thus additional changes must be nil. + _, err = c.createPath(path, false, as) + } + } + return changes, err +} + +func (c *Change) ensureTarget(as *Assumptions) ([]*Change, error) { + var changes []*Change + + kind := c.Entry.XSnapdKind() + path := c.Entry.Dir + + // We use lstat to ensure that we don't follow a symlink in case one was + // set up by the snap. Note that at the time this is run, all the snap's + // processes are frozen but if the path is a directory controlled by the + // user (typically in /home) then we may still race with user processes + // that change it. + fi, err := osLstat(path) + + if err == nil { + // If the element already exists we just need to ensure it is of + // the correct type. The desired type depends on the kind of entry + // we are working with. + switch kind { + case "": + if !fi.Mode().IsDir() { + err = fmt.Errorf("cannot use %q as mount point: not a directory", path) + } + case "file": + if !fi.Mode().IsRegular() { + err = fmt.Errorf("cannot use %q as mount point: not a regular file", path) + } + case "symlink": + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + // Create path verifies the symlink or fails if it is not what we wanted. + _, err = c.createPath(path, false, as) + } else { + err = fmt.Errorf("cannot create symlink in %q: existing file in the way", path) + } + } + } else if os.IsNotExist(err) { + changes, err = c.createPath(path, true, as) + } else { + // If we cannot inspect the element let's just bail out. + err = fmt.Errorf("cannot inspect %q: %v", path, err) + } + return changes, err +} + +func (c *Change) ensureSource(as *Assumptions) ([]*Change, error) { + var changes []*Change + + // We only have to do ensure bind mount source exists. + // This also rules out symlinks. + flags, _ := osutil.MountOptsToCommonFlags(c.Entry.Options) + if flags&syscall.MS_BIND == 0 { + return nil, nil + } + + kind := c.Entry.XSnapdKind() + path := c.Entry.Name + fi, err := osLstat(path) + + if err == nil { + // If the element already exists we just need to ensure it is of + // the correct type. The desired type depends on the kind of entry + // we are working with. + switch kind { + case "": + if !fi.Mode().IsDir() { + err = fmt.Errorf("cannot use %q as bind-mount source: not a directory", path) + } + case "file": + if !fi.Mode().IsRegular() { + err = fmt.Errorf("cannot use %q as bind-mount source: not a regular file", path) + } + } + } else if os.IsNotExist(err) { + // NOTE: This createPath is using pokeHoles, to make read-only places + // writable, but only for layouts and not for other (typically content + // sharing) mount entries. + // + // This is done because the changes made with pokeHoles=true are only + // visible in this current mount namespace and are not generally + // visible from other snaps because they inhabit different namespaces. + // + // In other words, changes made here are only observable by the single + // snap they apply to. As such they are useless for content sharing but + // very much useful to layouts. + pokeHoles := c.Entry.XSnapdOrigin() == "layout" + changes, err = c.createPath(path, pokeHoles, as) + } else { + // If we cannot inspect the element let's just bail out. + err = fmt.Errorf("cannot inspect %q: %v", path, err) + } + + return changes, err +} + +// changePerformImpl is the real implementation of Change.Perform +func changePerformImpl(c *Change, as *Assumptions) (changes []*Change, err error) { + if c.Action == Mount { + var changesSource, changesTarget []*Change + // We may be asked to bind mount a file, bind mount a directory, mount + // a filesystem over a directory, or create a symlink (which is abusing + // the "mount" concept slightly). That actual operation is performed in + // c.lowLevelPerform. Here we just set the stage to make that possible. + // + // As a result of this ensure call we may need to make the medium writable + // and that's why we may return more changes as a result of performing this + // one. + changesTarget, err = c.ensureTarget(as) + // NOTE: we are collecting changes even if things fail. This is so that + // upper layers can perform undo correctly. + changes = append(changes, changesTarget...) + if err != nil { + return changes, err + } + + // At this time we can be sure that the target element (for files and + // directories) exists and is of the right type or that it (for + // symlinks) doesn't exist but the parent directory does. + // This property holds as long as we don't interact with locations that + // are under the control of regular (non-snap) processes that are not + // suspended and may be racing with us. + changesSource, err = c.ensureSource(as) + // NOTE: we are collecting changes even if things fail. This is so that + // upper layers can perform undo correctly. + changes = append(changes, changesSource...) + if err != nil { + return changes, err + } + } + + // Perform the underlying mount / unmount / unlink call. + err = c.lowLevelPerform(as) + return changes, err +} + +func init() { + changePerform = changePerformImpl +} + +// Perform executes the desired mount or unmount change using system calls. +// Filesystems that depend on helper programs or multiple independent calls to +// the kernel (--make-shared, for example) are unsupported. +// +// Perform may synthesize *additional* changes that were necessary to perform +// this change (such as mounted tmpfs or overlayfs). +func (c *Change) Perform(as *Assumptions) ([]*Change, error) { + return changePerform(c, as) +} + +// lowLevelPerform is simple bridge from Change to mount / unmount syscall. +func (c *Change) lowLevelPerform(as *Assumptions) error { + var err error + switch c.Action { + case Mount: + kind := c.Entry.XSnapdKind() + switch kind { + case "symlink": + // symlinks are handled in createInode directly, nothing to do here. + case "", "file": + flags, unparsed := osutil.MountOptsToCommonFlags(c.Entry.Options) + // Use Secure.BindMount for bind mounts + if flags&syscall.MS_BIND == syscall.MS_BIND { + err = BindMount(c.Entry.Name, c.Entry.Dir, uint(flags)) + } else { + err = sysMount(c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flags), strings.Join(unparsed, ",")) + } + logger.Debugf("mount %q %q %q %d %q (error: %v)", c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flags), strings.Join(unparsed, ","), err) + if err == nil { + as.AddChange(c) + } + } + return err + case Unmount: + kind := c.Entry.XSnapdKind() + switch kind { + case "symlink": + err = osRemove(c.Entry.Dir) + logger.Debugf("remove %q (error: %v)", c.Entry.Dir, err) + case "", "file": + // Detach the mount point instead of unmounting it if requested. + flags := umountNoFollow + if c.Entry.XSnapdDetach() { + flags |= syscall.MNT_DETACH + } + + // Perform the raw unmount operation. + err = sysUnmount(c.Entry.Dir, flags) + if err == nil { + as.AddChange(c) + } + logger.Debugf("umount %q (error: %v)", c.Entry.Dir, err) + if err != nil { + return err + } + + // Open a path of the file we are considering the removal of. + path := c.Entry.Dir + var fd int + fd, err = OpenPath(path) + if err != nil { + return err + } + defer sysClose(fd) + + // Don't attempt to remove anything from squashfs. + var statfsBuf syscall.Statfs_t + err = sysFstatfs(fd, &statfsBuf) + if err != nil { + return err + } + if statfsBuf.Type == SquashfsMagic { + return nil + } + + if kind == "file" { + // Don't attempt to remove non-empty files since they cannot be + // the placeholders we created. + var statBuf syscall.Stat_t + err = sysFstat(fd, &statBuf) + if err != nil { + return err + } + if statBuf.Size != 0 { + return nil + } + } + + // Remove the file or directory while using the full path. There's + // no way to avoid a race here since there's no way to unlink a + // file solely by file descriptor. + err = osRemove(path) + // Unpack the low-level error that osRemove wraps into PathError. + if packed, ok := err.(*os.PathError); ok { + err = packed.Err + } + // If we were removing a directory but it was not empty then just + // ignore the error. This is the equivalent of the non-empty file + // check we do above. See rmdir(2) for explanation why we accept + // more than one errno value. + if kind == "" && (err == syscall.ENOTEMPTY || err == syscall.EEXIST) { + return nil + } + } + return err + case Keep: + return nil + } + return fmt.Errorf("cannot process mount change: unknown action: %q", c.Action) +} + +// NeededChanges computes the changes required to change current to desired mount entries. +// +// The current and desired profiles is a fstab like list of mount entries. The +// lists are processed and a "diff" of mount changes is produced. The mount +// changes, when applied in order, transform the current profile into the +// desired profile. +func NeededChanges(currentProfile, desiredProfile *osutil.MountProfile) []*Change { + // Copy both profiles as we will want to mutate them. + current := make([]osutil.MountEntry, len(currentProfile.Entries)) + copy(current, currentProfile.Entries) + desired := make([]osutil.MountEntry, len(desiredProfile.Entries)) + copy(desired, desiredProfile.Entries) + + // Clean the directory part of both profiles. This is done so that we can + // easily test if a given directory is a subdirectory with + // strings.HasPrefix coupled with an extra slash character. + for i := range current { + current[i].Dir = filepath.Clean(current[i].Dir) + } + for i := range desired { + desired[i].Dir = filepath.Clean(desired[i].Dir) + } + + // Sort both lists by directory name with implicit trailing slash. + sort.Sort(byOriginAndMagicDir(current)) + sort.Sort(byOriginAndMagicDir(desired)) + + // Construct a desired directory map. + desiredMap := make(map[string]*osutil.MountEntry) + for i := range desired { + desiredMap[desired[i].Dir] = &desired[i] + } + + // Indexed by mount point path. + reuse := make(map[string]bool) + // Indexed by entry ID + desiredIDs := make(map[string]bool) + var skipDir string + + // Collect the IDs of desired changes. + // We need that below to keep implicit changes from the current profile. + for i := range desired { + desiredIDs[desired[i].XSnapdEntryID()] = true + } + + // Compute reusable entries: those which are equal in current and desired and which + // are not prefixed by another entry that changed. + for i := range current { + dir := current[i].Dir + if skipDir != "" && strings.HasPrefix(dir, skipDir) { + logger.Debugf("skipping entry %q", current[i]) + continue + } + skipDir = "" // reset skip prefix as it no longer applies + + // Reuse synthetic entries if their needed-by entry is desired. + // Synthetic entries cannot exist on their own and always couple to a + // non-synthetic entry. + + // NOTE: Synthetic changes have a special purpose. + // + // They are a "shadow" of mount events that occurred to allow one of + // the desired mount entries to be possible. The changes have only one + // goal: tell snap-update-ns how those mount events can be undone in + // case they are no longer needed. The actual changes may have been + // different and may have involved steps not represented as synthetic + // mount entires as long as those synthetic entries can be undone to + // reverse the effect. In reality each non-tmpfs synthetic entry was + // constructed using a temporary bind mount that contained the original + // mount entries of a directory that was hidden with a tmpfs, but this + // fact was lost. + if current[i].XSnapdSynthetic() && desiredIDs[current[i].XSnapdNeededBy()] { + logger.Debugf("reusing synthetic entry %q", current[i]) + reuse[dir] = true + continue + } + + // Reuse entries that are desired and identical in the current profile. + if entry, ok := desiredMap[dir]; ok && current[i].Equal(entry) { + logger.Debugf("reusing unchanged entry %q", current[i]) + reuse[dir] = true + continue + } + + skipDir = strings.TrimSuffix(dir, "/") + "/" + } + + logger.Debugf("desiredIDs: %v", desiredIDs) + logger.Debugf("reuse: %v", reuse) + + // We are now ready to compute the necessary mount changes. + var changes []*Change + + // Unmount entries not reused in reverse to handle children before their parent. + for i := len(current) - 1; i >= 0; i-- { + if reuse[current[i].Dir] { + changes = append(changes, &Change{Action: Keep, Entry: current[i]}) + } else { + changes = append(changes, &Change{Action: Unmount, Entry: current[i]}) + } + } + + // Mount desired entries not reused. + for i := range desired { + if !reuse[desired[i].Dir] { + changes = append(changes, &Change{Action: Mount, Entry: desired[i]}) + } + } + + return changes +} diff --git a/cmd/snap-update-ns/change_test.go b/cmd/snap-update-ns/change_test.go new file mode 100644 index 00000000..76952c09 --- /dev/null +++ b/cmd/snap-update-ns/change_test.go @@ -0,0 +1,2355 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "errors" + "os" + "strings" + "syscall" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/testutil" +) + +type changeSuite struct { + testutil.BaseTest + sys *testutil.SyscallRecorder + as *update.Assumptions +} + +var ( + errTesting = errors.New("testing") +) + +var _ = Suite(&changeSuite{}) + +func (s *changeSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + // Mock and record system interactions. + s.sys = &testutil.SyscallRecorder{} + s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) + s.as = &update.Assumptions{} +} + +func (s *changeSuite) TearDownTest(c *C) { + s.BaseTest.TearDownTest(c) + s.sys.CheckForStrayDescriptors(c) +} + +func (s *changeSuite) TestFakeFileInfo(c *C) { + c.Assert(testutil.FileInfoDir.IsDir(), Equals, true) + c.Assert(testutil.FileInfoFile.IsDir(), Equals, false) + c.Assert(testutil.FileInfoSymlink.IsDir(), Equals, false) +} + +func (s *changeSuite) TestString(c *C) { + change := update.Change{ + Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda1"}, + Action: update.Mount, + } + c.Assert(change.String(), Equals, "mount (/dev/sda1 /a/b none defaults 0 0)") +} + +// When there are no profiles we don't do anything. +func (s *changeSuite) TestNeededChangesNoProfiles(c *C) { + current := &osutil.MountProfile{} + desired := &osutil.MountProfile{} + changes := update.NeededChanges(current, desired) + c.Assert(changes, IsNil) +} + +// When the profiles are the same we don't do anything. +func (s *changeSuite) TestNeededChangesNoChange(c *C) { + current := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Keep}, + }) +} + +// When the content interface is connected we should mount the new entry. +func (s *changeSuite) TestNeededChangesTrivialMount(c *C) { + current := &osutil.MountProfile{} + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: desired.Entries[0], Action: update.Mount}, + }) +} + +// When the content interface is disconnected we should unmount the mounted entry. +func (s *changeSuite) TestNeededChangesTrivialUnmount(c *C) { + current := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} + desired := &osutil.MountProfile{} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: current.Entries[0], Action: update.Unmount}, + }) +} + +// When umounting we unmount children before parents. +func (s *changeSuite) TestNeededChangesUnmountOrder(c *C) { + current := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/common/stuff/extra"}, + {Dir: "/common/stuff"}, + }} + desired := &osutil.MountProfile{} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Unmount}, + {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Unmount}, + }) +} + +// When mounting we mount the parents before the children. +func (s *changeSuite) TestNeededChangesMountOrder(c *C) { + current := &osutil.MountProfile{} + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/common/stuff/extra"}, + {Dir: "/common/stuff"}, + }} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Mount}, + {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, + }) +} + +// When parent changes we don't reuse its children +func (s *changeSuite) TestNeededChangesChangedParentSameChild(c *C) { + current := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/common/stuff", Name: "/dev/sda1"}, + {Dir: "/common/stuff/extra"}, + {Dir: "/common/unrelated"}, + }} + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/common/stuff", Name: "/dev/sda2"}, + {Dir: "/common/stuff/extra"}, + {Dir: "/common/unrelated"}, + }} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Keep}, + {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Unmount}, + {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Unmount}, + {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda2"}, Action: update.Mount}, + {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, + }) +} + +// When child changes we don't touch the unchanged parent +func (s *changeSuite) TestNeededChangesSameParentChangedChild(c *C) { + current := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/common/stuff"}, + {Dir: "/common/stuff/extra", Name: "/dev/sda1"}, + {Dir: "/common/unrelated"}, + }} + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/common/stuff"}, + {Dir: "/common/stuff/extra", Name: "/dev/sda2"}, + {Dir: "/common/unrelated"}, + }} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Keep}, + {Entry: osutil.MountEntry{Dir: "/common/stuff/extra", Name: "/dev/sda1"}, Action: update.Unmount}, + {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Keep}, + {Entry: osutil.MountEntry{Dir: "/common/stuff/extra", Name: "/dev/sda2"}, Action: update.Mount}, + }) +} + +// Unused bind mount farms are unmounted. +func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUnused(c *C) { + current := &osutil.MountProfile{Entries: []osutil.MountEntry{{ + // The tmpfs that lets us write into immutable squashfs. We mock + // x-snapd.needed-by to the last entry in the current profile (the bind + // mount). Mark it synthetic since it is a helper mount that is needed + // to facilitate the following mounts. + Name: "tmpfs", + Dir: "/snap/name/42/subdir", + Type: "tmpfs", + Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, + }, { + // A bind mount to preserve a directory hidden by the tmpfs (the mount + // point is created elsewhere). We mock x-snapd.needed-by to the + // location of the bind mount below that is no longer desired. + Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", + Dir: "/snap/name/42/subdir/existing", + Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, + }, { + // A bind mount to put some content from another snap. The bind mount + // is nothing special but the fact that it is possible is the reason + // the two entries above exist. The mount point (created) is created + // elsewhere. + Name: "/snap/other/123/libs", + Dir: "/snap/name/42/subdir/created", + Options: []string{"bind", "ro"}, + }}} + + desired := &osutil.MountProfile{} + + changes := update.NeededChanges(current, desired) + + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{ + Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", + Dir: "/snap/name/42/subdir/existing", + Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, + }, Action: update.Unmount}, + {Entry: osutil.MountEntry{ + Name: "/snap/other/123/libs", + Dir: "/snap/name/42/subdir/created", + Options: []string{"bind", "ro"}, + }, Action: update.Unmount}, + {Entry: osutil.MountEntry{ + Name: "tmpfs", + Dir: "/snap/name/42/subdir", + Type: "tmpfs", + Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, + }, Action: update.Unmount}, + }) +} + +func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUsed(c *C) { + // NOTE: the current profile is the same as in the test + // TestNeededChangesTmpfsBindMountFarmUnused written above. + current := &osutil.MountProfile{Entries: []osutil.MountEntry{{ + Name: "tmpfs", + Dir: "/snap/name/42/subdir", + Type: "tmpfs", + Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, + }, { + Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", + Dir: "/snap/name/42/subdir/existing", + Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, + }, { + Name: "/snap/other/123/libs", + Dir: "/snap/name/42/subdir/created", + Options: []string{"bind", "ro"}, + }}} + + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{ + // This is the only entry that we explicitly want but in order to + // support it we need to keep the remaining implicit entries. + Name: "/snap/other/123/libs", + Dir: "/snap/name/42/subdir/created", + Options: []string{"bind", "ro"}, + }}} + + changes := update.NeededChanges(current, desired) + + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{ + Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", + Dir: "/snap/name/42/subdir/existing", + Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, + }, Action: update.Keep}, + {Entry: osutil.MountEntry{ + Name: "/snap/other/123/libs", + Dir: "/snap/name/42/subdir/created", + Options: []string{"bind", "ro"}, + }, Action: update.Keep}, + {Entry: osutil.MountEntry{ + Name: "tmpfs", + Dir: "/snap/name/42/subdir", + Type: "tmpfs", + Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, + }, Action: update.Keep}, + }) +} + +// cur = ['/a/b', '/a/b-1', '/a/b-1/3', '/a/b/c'] +// des = ['/a/b', '/a/b-1', '/a/b/c' +// +// We are smart about comparing entries as directories. Here even though "/a/b" +// is a prefix of "/a/b-1" it is correctly reused. +func (s *changeSuite) TestNeededChangesSmartEntryComparison(c *C) { + current := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/a/b", Name: "/dev/sda1"}, + {Dir: "/a/b-1"}, + {Dir: "/a/b-1/3"}, + {Dir: "/a/b/c"}, + }} + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/a/b", Name: "/dev/sda2"}, + {Dir: "/a/b-1"}, + {Dir: "/a/b/c"}, + }} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{Dir: "/a/b/c"}, Action: update.Unmount}, + {Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda1"}, Action: update.Unmount}, + {Entry: osutil.MountEntry{Dir: "/a/b-1/3"}, Action: update.Unmount}, + {Entry: osutil.MountEntry{Dir: "/a/b-1"}, Action: update.Keep}, + + {Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda2"}, Action: update.Mount}, + {Entry: osutil.MountEntry{Dir: "/a/b/c"}, Action: update.Mount}, + }) +} + +// Parallel instance changes are executed first +func (s *changeSuite) TestNeededChangesParallelInstancesManyComeFirst(c *C) { + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/common/stuff", Name: "/dev/sda1"}, + {Dir: "/common/stuff/extra"}, + {Dir: "/common/unrelated"}, + {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, + {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, + }} + changes := update.NeededChanges(&osutil.MountProfile{}, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Mount}, + {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, + {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount}, + }) +} + +// Parallel instance changes are kept if already present +func (s *changeSuite) TestNeededChangesParallelInstancesKeep(c *C) { + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/common/stuff", Name: "/dev/sda1"}, + {Dir: "/common/unrelated"}, + {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, + {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, + }} + current := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, + {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, + }} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, + {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, + {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Mount}, + {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount}, + }) +} + +// Parallel instance with mounts inside +func (s *changeSuite) TestNeededChangesParallelInstancesInsideMount(c *C) { + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/foo/bar/baz"}, + {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, + {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, + }} + current := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/foo/bar/zed"}, + {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, + {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, + }} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{Dir: "/foo/bar/zed"}, Action: update.Unmount}, + {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, + {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, + {Entry: osutil.MountEntry{Dir: "/foo/bar/baz"}, Action: update.Mount}, + }) +} + +func mustReadProfile(profileStr string) *osutil.MountProfile { + profile, err := osutil.ReadMountProfile(strings.NewReader(profileStr)) + if err != nil { + panic(err) + } + return profile +} + +func (s *changeSuite) TestRuntimeUsingSymlinks(c *C) { + // We start with a runtime shared from one snap to another and then exposed + // to /opt with a symbolic link. This is the initial state of the + // application in version v1. + initial := mustReadProfile("") + desired_v1 := mustReadProfile( + "none /opt/runtime none x-snapd.kind=symlink,x-snapd.symlink=/snap/app/x1/runtime,x-snapd.origin=layout 0 0\n" + + "/snap/runtime/x1/opt/runtime /snap/app/x1/runtime none bind,ro 0 0\n") + // The changes we compute are trivial, simply perform each operation in order. + changes := update.NeededChanges(initial, desired_v1) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: desired_v1.Entries[0], Action: update.Mount}, + {Entry: desired_v1.Entries[1], Action: update.Mount}, + }) + // After performing both changes we have a new synthesized entry. We get an + // extra writable mimic over /opt so that we can add our symlink. The + // content sharing into $SNAP is applied as expected since the snap ships + // the required mount point. + current_v1 := mustReadProfile( + "/snap/runtime/x1/opt/runtime /snap/app/x1/runtime none bind,ro 0 0\n" + + "none /opt/runtime none x-snapd.kind=symlink,x-snapd.symlink=/snap/app/x1/runtime,x-snapd.origin=layout 0 0\n" + + "tmpfs /opt tmpfs x-snapd.synthetic,x-snapd.needed-by=/opt/runtime,mode=0755,uid=0,gid=0 0") + + // We now proceed to replace app v1 with v2 which uses a bind mount instead + // of a symlink. First, let's start with the updated desired profile: + desired_v2 := mustReadProfile( + "/snap/app/x2/runtime /opt/runtime none rbind,rw,x-snapd.origin=layout 0 0\n" + + "/snap/runtime/x1/opt/runtime /snap/app/x2/runtime none bind,ro 0 0\n") + + // Let's see what the update algorithm thinks. + changes = update.NeededChanges(current_v1, desired_v2) + c.Assert(changes, DeepEquals, []*update.Change{ + // We are dropping the content interface bind mount because app changed revision + {Entry: current_v1.Entries[0], Action: update.Unmount}, + // We are also dropping the symlink we had in /opt/runtime + {Entry: current_v1.Entries[1], Action: update.Unmount}, + // But, we are keeping the /opt tmpfs because we still want /opt/runtime to exist (neat!) + {Entry: current_v1.Entries[2], Action: update.Keep}, + // We are adding a new bind mount for /opt/runtime + {Entry: desired_v2.Entries[0], Action: update.Mount}, + // We also adding the updated path of the content interface (for revision x2) + {Entry: desired_v2.Entries[1], Action: update.Mount}, + }) + + // After performing all those changes this is the profile we observe. + current_v2 := mustReadProfile( + "tmpfs /opt tmpfs x-snapd.synthetic,x-snapd.needed-by=/opt/runtime,mode=0755,uid=0,gid=0 0 0\n" + + "/snap/app/x2/runtime /opt/runtime none rbind,rw,x-snapd.origin=layout 0 0\n" + + "/snap/runtime/x1/opt/runtime /snap/app/x2/runtime none bind,ro 0 0\n") + + // So far so good. To trigger the issue we now revert or refresh to v1 + // again. Let's see what happens here. The desired profiles are already + // known so let's see what the algorithm thinks now. + changes = update.NeededChanges(current_v2, desired_v1) + c.Assert(changes, DeepEquals, []*update.Change{ + // We are, again, dropping the content interface bind mount because app changed revision + {Entry: current_v2.Entries[2], Action: update.Unmount}, + // We are also dropping the bind mount from /opt/runtime since we want a symlink instead + {Entry: current_v2.Entries[1], Action: update.Unmount}, + // Again, we reuse the tmpfs. + {Entry: current_v2.Entries[0], Action: update.Keep}, + // We are providing a symlink /opt/runtime -> to $SNAP/runtime. + {Entry: desired_v1.Entries[0], Action: update.Mount}, + // We are bind mounting the runtime from another snap into $SNAP/runtime + {Entry: desired_v1.Entries[1], Action: update.Mount}, + }) + + // The problem is that the tmpfs contains leftovers from the things we + // created and those prevent the execution of this mount profile. +} + +// ######################################## +// Topic: mounting & unmounting filesystems +// ######################################## + +// Change.Perform returns errors from os.Lstat (apart from ErrNotExist) +func (s *changeSuite) TestPerformFilesystemMountLstatError(c *C) { + s.sys.InsertFault(`lstat "/target"`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, E: errTesting}, + }) +} + +// Change.Perform wants to mount a filesystem. +func (s *changeSuite) TestPerformFilesystemMount(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `mount "device" "/target" "type" 0 ""`}, + }) +} + +// Change.Perform wants to mount a filesystem but it fails. +func (s *changeSuite) TestPerformFilesystemMountWithError(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + s.sys.InsertFault(`mount "device" "/target" "type" 0 ""`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, Equals, errTesting) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `mount "device" "/target" "type" 0 ""`, E: errTesting}, + }) +} + +// Change.Perform wants to mount a filesystem but the mount point isn't there. +func (s *changeSuite) TestPerformFilesystemMountWithoutMountPoint(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "target" 0755`}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `mount "device" "/target" "type" 0 ""`}, + }) +} + +// Change.Perform wants to create a filesystem but the mount point isn't there and cannot be created. +func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointWithErrors(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "target" 0755`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot create directory "/target": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "target" 0755`, E: errTesting}, + {C: `close 3`}, + }) +} + +// Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only. +func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBase(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try + s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS, nil) // works on 2nd try + s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) + s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. + s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) + + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, DeepEquals, []*update.Change{ + {Action: update.Mount, Entry: osutil.MountEntry{ + Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", + Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}}, + }, + }) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + // sniff mount target + {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, + + // /rofs/target is missing, create it + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, + {C: `close 4`}, + + // error, read only filesystem, create a mimic + {C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, + {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, + {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "tmp" 0755`}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `mkdirat 4 ".snap" 0755`}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `fchown 5 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `mkdirat 5 "rofs" 0755`}, + {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 5`}, + {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, + + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, + {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, + {C: `fstat 7 `, R: syscall.Stat_t{}}, + {C: `close 6`}, + {C: `close 5`}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, + {C: `close 7`}, + {C: `close 4`}, + + {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, + {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, + {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, + + // Perform clean up after the unmount operation. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 5`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, + {C: `remove "/tmp/.snap/rofs"`}, + {C: `close 6`}, + + // mimic ready, re-try initial mkdir + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "target" 0755`}, + {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 4`}, + + // mount the filesystem + {C: `mount "device" "/rofs/target" "type" 0 ""`}, + }) +} + +// Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only and mimic fails during planning. +func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBaseErrorWhilePlanning(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS) + s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) + s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) + s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) + s.sys.InsertFault(`readdir "/rofs"`, errTesting) // make the writable mimic fail + + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot create writable mimic over "/rofs": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + // sniff mount target + {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, + + // /rofs/target is missing, create it + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, + {C: `close 4`}, + + // error, read only filesystem, create a mimic + {C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, + {C: `readdir "/rofs"`, E: errTesting}, + // cannot create mimic, that's it + }) +} + +// Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only and mimic fails during execution. +func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBaseErrorWhileExecuting(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS) + s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) + s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) + s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) + s.sys.InsertFault(`mkdirat 4 ".snap" 0755`, errTesting) // make the writable mimic fail + + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot create writable mimic over "/rofs": cannot create directory "/tmp/.snap": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + // sniff mount target + {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, + + // /rofs/target is missing, create it + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, + {C: `close 4`}, + + // error, read only filesystem, create a mimic + {C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, + {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, + {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "tmp" 0755`}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `mkdirat 4 ".snap" 0755`, E: errTesting}, + {C: `close 4`}, + {C: `close 3`}, + // cannot create mimic, that's it + }) +} + +// Change.Perform wants to mount a filesystem but there's a symlink in mount point. +func (s *changeSuite) TestPerformFilesystemMountWithSymlinkInMountPoint(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoSymlink}, + }) +} + +// Change.Perform wants to mount a filesystem but there's a file in mount point. +func (s *changeSuite) TestPerformFilesystemMountWithFileInMountPoint(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoFile}, + }) +} + +// Change.Perform wants to unmount a filesystem. +func (s *changeSuite) TestPerformFilesystemUnmount(c *C) { + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{}) + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `unmount "/target" UMOUNT_NOFOLLOW`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{}}, + {C: `remove "/target"`}, + {C: `close 4`}, + }) + c.Assert(synth, HasLen, 0) +} + +// Change.Perform wants to detach a bind mount. +func (s *changeSuite) TestPerformFilesystemDetch(c *C) { + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{}) + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/something", Dir: "/target", Options: []string{"x-snapd.detach"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `unmount "/target" UMOUNT_NOFOLLOW|MNT_DETACH`}, + + // Perform clean up after the unmount operation. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{}}, + {C: `remove "/target"`}, + {C: `close 4`}, + }) + c.Assert(synth, HasLen, 0) +} + +// Change.Perform wants to unmount a filesystem but it fails. +func (s *changeSuite) TestPerformFilesystemUnmountError(c *C) { + s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting) + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + synth, err := chg.Perform(s.as) + c.Assert(err, Equals, errTesting) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting}, + }) + c.Assert(synth, HasLen, 0) +} + +// Change.Perform passes non-flag options to the kernel. +func (s *changeSuite) TestPerformFilesystemMountWithOptions(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"ro", "funky"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `mount "device" "/target" "type" MS_RDONLY "funky"`}, + }) +} + +// Change.Perform doesn't pass snapd-specific options to the kernel. +func (s *changeSuite) TestPerformFilesystemMountWithSnapdSpecificOptions(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"ro", "x-snapd.funky"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `mount "device" "/target" "type" MS_RDONLY ""`}, + }) +} + +// ############################################### +// Topic: bind-mounting and unmounting directories +// ############################################### + +// Change.Perform wants to bind mount a directory but the target cannot be stat'ed. +func (s *changeSuite) TestPerformDirectoryBindMountTargetLstatError(c *C) { + s.sys.InsertFault(`lstat "/target"`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, E: errTesting}, + }) +} + +// Change.Perform wants to bind mount a directory but the source cannot be stat'ed. +func (s *changeSuite) TestPerformDirectoryBindMountSourceLstatError(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + s.sys.InsertFault(`lstat "/source"`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot inspect "/source": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `lstat "/source"`, E: errTesting}, + }) +} + +// Change.Perform wants to bind mount a directory. +func (s *changeSuite) TestPerformDirectoryBindMount(c *C) { + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `lstat "/source"`, R: testutil.FileInfoDir}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, + {C: `close 5`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to bind mount a directory but it fails. +func (s *changeSuite) TestPerformDirectoryBindMountWithError(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + s.sys.InsertFault(`mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, Equals, errTesting) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `lstat "/source"`, R: testutil.FileInfoDir}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, E: errTesting}, + {C: `close 5`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to bind mount a directory but the mount point isn't there. +func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPoint(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) + s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "target" 0755`}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `lstat "/source"`, R: testutil.FileInfoDir}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, + {C: `close 5`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to bind mount a directory but the mount source isn't there. +func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSource(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `lstat "/source"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "source" 0755`}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, + {C: `close 5`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to create a directory bind mount but the mount point isn't there and cannot be created. +func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPointWithErrors(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "target" 0755`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot create directory "/target": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "target" 0755`, E: errTesting}, + {C: `close 3`}, + }) +} + +// Change.Perform wants to create a directory bind mount but the mount source isn't there and cannot be created. +func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceWithErrors(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "source" 0755`, errTesting) + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot create directory "/source": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `lstat "/source"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "source" 0755`, E: errTesting}, + {C: `close 3`}, + }) +} + +// Change.Perform wants to bind mount a directory but the mount point isn't there and the parent is read-only. +func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPointAndReadOnlyBase(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try + s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS, nil) // works on 2nd try + s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) + s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. + s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) + + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/rofs/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, DeepEquals, []*update.Change{ + {Action: update.Mount, Entry: osutil.MountEntry{ + Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", + Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}}, + }, + }) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + // sniff mount target + {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, + + // /rofs/target is missing, create it + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, + {C: `close 4`}, + + // error, read only filesystem, create a mimic + {C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, + {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, + {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "tmp" 0755`}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `mkdirat 4 ".snap" 0755`}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `fchown 5 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `mkdirat 5 "rofs" 0755`}, + {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 5`}, + {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, + + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, + {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, + {C: `fstat 7 `, R: syscall.Stat_t{}}, + {C: `close 6`}, + {C: `close 5`}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, + {C: `close 7`}, + {C: `close 4`}, + + {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, + {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, + {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, + + // Perform clean up after the unmount operation. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 5`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, + {C: `remove "/tmp/.snap/rofs"`}, + {C: `close 6`}, + + // mimic ready, re-try initial mkdir + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "target" 0755`}, + {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 4`}, + + // sniff mount source + {C: `lstat "/source"`, R: testutil.FileInfoDir}, + + // mount the filesystem + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 5`}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/6" "" MS_BIND ""`}, + {C: `close 6`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to bind mount a directory but the mount source isn't there and the parent is read-only. +func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceAndReadOnlyBase(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) + s.sys.InsertFault(`lstat "/rofs/source"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`mkdirat 4 "source" 0755`, syscall.EROFS) + + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/rofs/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `lstat "/rofs/source"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "source" 0755`, E: syscall.EROFS}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to bind mount a directory but the mount source isn't there and the parent is read-only but this is for a layout. +func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceAndReadOnlyBaseForLayout(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + s.sys.InsertFault(`lstat "/rofs/source"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try + s.sys.InsertFault(`mkdirat 4 "source" 0755`, syscall.EROFS, nil) // works on 2nd try + s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. + s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) + s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) + + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/rofs/source", Dir: "/target", Options: []string{"bind", "x-snapd.origin=layout"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Check(synth, DeepEquals, []*update.Change{ + {Action: update.Mount, Entry: osutil.MountEntry{ + Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", + Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/source", "mode=0755", "uid=0", "gid=0"}}, + }, + }) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + // sniff mount target and source + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `lstat "/rofs/source"`, E: syscall.ENOENT}, + + // /rofs/source is missing, create it + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "source" 0755`, E: syscall.EROFS}, + {C: `close 4`}, + + // error /rofs is a read-only filesystem, create a mimic + {C: `lstat "/rofs" `, R: syscall.Stat_t{Mode: 0755}}, + {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, + {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "tmp" 0755`}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `mkdirat 4 ".snap" 0755`}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `fchown 5 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `mkdirat 5 "rofs" 0755`}, + {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 5`}, + {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, + {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, + {C: `fstat 7 `, R: syscall.Stat_t{}}, + {C: `close 6`}, + {C: `close 5`}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, + {C: `close 7`}, + {C: `close 4`}, + {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, + {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, + {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, + + // Perform clean up after the unmount operation. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 5`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, + {C: `remove "/tmp/.snap/rofs"`}, + {C: `close 6`}, + + // /rofs/source was missing (we checked earlier), create it + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "source" 0755`}, + {C: `openat 4 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 4`}, + + // bind mount /rofs/source -> /target + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 4`}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/5" "/proc/self/fd/4" "" MS_BIND ""`}, + {C: `close 4`}, + {C: `close 5`}, + }) +} + +// Change.Perform wants to bind mount a directory but there's a symlink in mount point. +func (s *changeSuite) TestPerformDirectoryBindMountWithSymlinkInMountPoint(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoSymlink}, + }) +} + +// Change.Perform wants to bind mount a directory but there's a file in mount mount. +func (s *changeSuite) TestPerformDirectoryBindMountWithFileInMountPoint(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoFile}, + }) +} + +// Change.Perform wants to bind mount a directory but there's a symlink in source. +func (s *changeSuite) TestPerformDirectoryBindMountWithSymlinkInMountSource(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoSymlink) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a directory`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `lstat "/source"`, R: testutil.FileInfoSymlink}, + }) +} + +// Change.Perform wants to bind mount a directory but there's a file in source. +func (s *changeSuite) TestPerformDirectoryBindMountWithFileInMountSource(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a directory`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `lstat "/source"`, R: testutil.FileInfoFile}, + }) +} + +// Change.Perform wants to unmount a directory bind mount. +func (s *changeSuite) TestPerformDirectoryBindUnmount(c *C) { + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{}) + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `unmount "/target" UMOUNT_NOFOLLOW`}, + + // Perform clean up after the unmount operation. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{}}, + {C: `remove "/target"`}, + {C: `close 4`}, + }) + c.Assert(synth, HasLen, 0) +} + +// Change.Perform wants to unmount a directory bind mount but it fails. +func (s *changeSuite) TestPerformDirectoryBindUnmountError(c *C) { + s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting) + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, Equals, errTesting) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting}, + }) + c.Assert(synth, HasLen, 0) +} + +// ######################################### +// Topic: bind-mounting and unmounting files +// ######################################### + +// Change.Perform wants to bind mount a file but the target cannot be stat'ed. +func (s *changeSuite) TestPerformFileBindMountTargetLstatError(c *C) { + s.sys.InsertFault(`lstat "/target"`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, E: errTesting}, + }) +} + +// Change.Perform wants to bind mount a file but the source cannot be stat'ed. +func (s *changeSuite) TestPerformFileBindMountSourceLstatError(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) + s.sys.InsertFault(`lstat "/source"`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot inspect "/source": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoFile}, + {C: `lstat "/source"`, E: errTesting}, + }) +} + +// Change.Perform wants to bind mount a file. +func (s *changeSuite) TestPerformFileBindMount(c *C) { + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoFile}, + {C: `lstat "/source"`, R: testutil.FileInfoFile}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, + {C: `close 5`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to bind mount a file but it fails. +func (s *changeSuite) TestPerformFileBindMountWithError(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + s.sys.InsertFault(`mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, Equals, errTesting) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoFile}, + {C: `lstat "/source"`, R: testutil.FileInfoFile}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, E: errTesting}, + {C: `close 5`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to bind mount a file but the mount point isn't there. +func (s *changeSuite) TestPerformFileBindMountWithoutMountPoint(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) + s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `lstat "/source"`, R: testutil.FileInfoFile}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, + {C: `close 5`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to create a directory bind mount but the mount point isn't there and cannot be created. +func (s *changeSuite) TestPerformFileBindMountWithoutMountPointWithErrors(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) + s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot open file "/target": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting}, + {C: `close 3`}, + }) +} + +// Change.Perform wants to bind mount a file but the mount source isn't there. +func (s *changeSuite) TestPerformFileBindMountWithoutMountSource(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoFile}, + {C: `lstat "/source"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, + {C: `close 5`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to create a file bind mount but the mount source isn't there and cannot be created. +func (s *changeSuite) TestPerformFileBindMountWithoutMountSourceWithErrors(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) + s.sys.InsertFault(`openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting) + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot open file "/source": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoFile}, + {C: `lstat "/source"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting}, + {C: `close 3`}, + }) +} + +// Change.Perform wants to bind mount a file but the mount point isn't there and the parent is read-only. +func (s *changeSuite) TestPerformFileBindMountWithoutMountPointAndReadOnlyBase(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EROFS, nil) // works on 2nd try + s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) + s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. + s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) + + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/rofs/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, DeepEquals, []*update.Change{ + {Action: update.Mount, Entry: osutil.MountEntry{ + Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", + Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}}, + }, + }) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + // sniff mount target + {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, + + // /rofs/target is missing, create it + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EROFS}, + {C: `close 4`}, + + // error, read only filesystem, create a mimic + {C: `lstat "/rofs" `, R: syscall.Stat_t{Mode: 0755}}, + {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, + {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "tmp" 0755`}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `mkdirat 4 ".snap" 0755`}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `fchown 5 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `mkdirat 5 "rofs" 0755`}, + {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 5`}, + {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, + + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, + {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, + {C: `fstat 7 `, R: syscall.Stat_t{}}, + {C: `close 6`}, + {C: `close 5`}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, + {C: `close 7`}, + {C: `close 4`}, + + {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, + {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, + {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, + + // Perform clean up after the unmount operation. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 5`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, + {C: `remove "/tmp/.snap/rofs"`}, + {C: `close 6`}, + + // mimic ready, re-try initial mkdir + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 4`}, + + // sniff mount source + {C: `lstat "/source"`, R: testutil.FileInfoFile}, + + // mount the filesystem + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 5`}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/6" "" MS_BIND ""`}, + {C: `close 6`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to bind mount a file but there's a symlink in mount point. +func (s *changeSuite) TestPerformFileBindMountWithSymlinkInMountPoint(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a regular file`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoSymlink}, + }) +} + +// Change.Perform wants to bind mount a file but there's a directory in mount point. +func (s *changeSuite) TestPerformBindMountFileWithDirectoryInMountPoint(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a regular file`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + }) +} + +// Change.Perform wants to bind mount a file but there's a symlink in source. +func (s *changeSuite) TestPerformFileBindMountWithSymlinkInMountSource(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoSymlink) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a regular file`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoFile}, + {C: `lstat "/source"`, R: testutil.FileInfoSymlink}, + }) +} + +// Change.Perform wants to bind mount a file but there's a directory in source. +func (s *changeSuite) TestPerformFileBindMountWithDirectoryInMountSource(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) + s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a regular file`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoFile}, + {C: `lstat "/source"`, R: testutil.FileInfoDir}, + }) +} + +// Change.Perform wants to unmount a file bind mount made on empty squashfs placeholder. +func (s *changeSuite) TestPerformFileBindUnmountOnSquashfs(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.SquashfsMagic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 0}) + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `unmount "/target" UMOUNT_NOFOLLOW`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, + {C: `close 4`}, + }) + c.Assert(synth, HasLen, 0) +} + +// Change.Perform wants to unmount a file bind mount made on non-empty ext4 placeholder. +func (s *changeSuite) TestPerformFileBindUnmountOnExt4NonEmpty(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 1}) + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `unmount "/target" UMOUNT_NOFOLLOW`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{Size: 1}}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, + {C: `fstat 4 `, R: syscall.Stat_t{Size: 1}}, + {C: `close 4`}, + }) + c.Assert(synth, HasLen, 0) +} + +// Change.Perform wants to unmount a file bind mount made on empty tmpfs placeholder. +func (s *changeSuite) TestPerformFileBindUnmountOnTmpfsEmpty(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 0}) + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `unmount "/target" UMOUNT_NOFOLLOW`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{Size: 0}}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, + {C: `fstat 4 `, R: syscall.Stat_t{Size: 0}}, + {C: `remove "/target"`}, + {C: `close 4`}, + }) + c.Assert(synth, HasLen, 0) +} + +// Change.Perform wants to unmount a file bind mount made on empty tmpfs placeholder but it is busy!. +func (s *changeSuite) TestPerformFileBindUnmountOnTmpfsEmptyButBusy(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 0}) + s.sys.InsertFault(`remove "/target"`, syscall.EBUSY) + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, "device or resource busy") + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `unmount "/target" UMOUNT_NOFOLLOW`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{Size: 0}}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, + {C: `fstat 4 `, R: syscall.Stat_t{Size: 0}}, + {C: `remove "/target"`, E: syscall.EBUSY}, + {C: `close 4`}, + }) + c.Assert(synth, HasLen, 0) +} + +// Change.Perform wants to unmount a file bind mount but it fails. +func (s *changeSuite) TestPerformFileBindUnmountError(c *C) { + s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting) + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, Equals, errTesting) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting}, + }) + c.Assert(synth, HasLen, 0) +} + +// ############################################################# +// Topic: handling mounts with the x-snapd.ignore-missing option +// ############################################################# + +func (s *changeSuite) TestPerformMountWithIgnoredMissingMountSource(c *C) { + s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.ignore-missing"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, Equals, update.ErrIgnoredMissingMount) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, R: testutil.FileInfoDir}, + {C: `lstat "/source"`, E: syscall.ENOENT}, + }) +} + +func (s *changeSuite) TestPerformMountWithIgnoredMissingMountPoint(c *C) { + s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.ignore-missing"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, Equals, update.ErrIgnoredMissingMount) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/target"`, E: syscall.ENOENT}, + }) +} + +// ######################## +// Topic: creating symlinks +// ######################## + +// Change.Perform wants to create a symlink but name cannot be stat'ed. +func (s *changeSuite) TestPerformCreateSymlinkNameLstatError(c *C) { + s.sys.InsertFault(`lstat "/name"`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot inspect "/name": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/name"`, E: errTesting}, + }) +} + +// Change.Perform wants to create a symlink. +func (s *changeSuite) TestPerformCreateSymlink(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/name"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `symlinkat "/oldname" 3 "name"`}, + {C: `close 3`}, + }) +} + +// Change.Perform wants to create a symlink but it fails. +func (s *changeSuite) TestPerformCreateSymlinkWithError(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT) + s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot create symlink "/name": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/name"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `symlinkat "/oldname" 3 "name"`, E: errTesting}, + {C: `close 3`}, + }) +} + +// Change.Perform wants to create a symlink but the target is empty. +func (s *changeSuite) TestPerformCreateSymlinkWithNoTargetError(c *C) { + s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink="}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot create symlink with empty target: "/name"`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/name"`, E: syscall.ENOENT}, + }) +} + +// Change.Perform wants to create a symlink but the base directory isn't there. +func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDir(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/base/name"`, syscall.ENOENT) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/base/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/base/name"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "base" 0755`}, + {C: `openat 3 "base" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `close 3`}, + {C: `symlinkat "/oldname" 4 "name"`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to create a symlink but the base directory isn't there and cannot be created. +func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDirWithErrors(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/base/name"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "base" 0755`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/base/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot create directory "/base": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/base/name"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "base" 0755`, E: errTesting}, + {C: `close 3`}, + }) +} + +// Change.Perform wants to create a symlink but the base directory isn't there and the parent is read-only. +func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDirAndReadOnlyBase(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertFault(`lstat "/rofs/name"`, syscall.ENOENT) + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`symlinkat "/oldname" 4 "name"`, syscall.EROFS, nil) // works on 2nd try + s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) + s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. + s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) + + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/rofs/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, DeepEquals, []*update.Change{ + {Action: update.Mount, Entry: osutil.MountEntry{ + Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", + Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/name", "mode=0755", "uid=0", "gid=0"}}, + }, + }) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + // sniff symlink name + {C: `lstat "/rofs/name"`, E: syscall.ENOENT}, + + // create base name (/rofs) + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + // create symlink + {C: `symlinkat "/oldname" 4 "name"`, E: syscall.EROFS}, + {C: `close 4`}, + + // error, read only filesystem, create a mimic + {C: `lstat "/rofs" `, R: syscall.Stat_t{Mode: 0755}}, + {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, + {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "tmp" 0755`}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 0 0`}, + {C: `mkdirat 4 ".snap" 0755`}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `fchown 5 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `mkdirat 5 "rofs" 0755`}, + {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 5`}, + {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, + + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 3`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, + {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, + {C: `fstat 7 `, R: syscall.Stat_t{}}, + {C: `close 6`}, + {C: `close 5`}, + {C: `close 3`}, + {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, + {C: `close 7`}, + {C: `close 4`}, + + {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, + {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, + {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, + + // Perform clean up after the unmount operation. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 5`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, + {C: `remove "/tmp/.snap/rofs"`}, + {C: `close 6`}, + + // mimic ready, re-try initial base mkdir + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + // create symlink + {C: `symlinkat "/oldname" 4 "name"`}, + {C: `close 4`}, + }) +} + +// Change.Perform wants to create a symlink but there's a file in the way. +func (s *changeSuite) TestPerformCreateSymlinkWithFileInTheWay(c *C) { + s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoFile) + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot create symlink in "/name": existing file in the way`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/name"`, R: testutil.FileInfoFile}, + }) +} + +// Change.Perform wants to create a symlink but a correct symlink is already present. +func (s *changeSuite) TestPerformCreateSymlinkWithGoodSymlinkPresent(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoSymlink) + s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, syscall.EEXIST) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Mode: syscall.S_IFLNK}) + s.sys.InsertReadlinkatResult(`readlinkat 4 "" `, "/oldname") + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/name"`, R: testutil.FileInfoSymlink}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `symlinkat "/oldname" 3 "name"`, E: syscall.EEXIST}, + {C: `openat 3 "name" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{Mode: syscall.S_IFLNK}}, + {C: `readlinkat 4 "" `, R: "/oldname"}, + {C: `close 4`}, + {C: `close 3`}, + }) +} + +// Change.Perform wants to create a symlink but a incorrect symlink is already present. +func (s *changeSuite) TestPerformCreateSymlinkWithBadSymlinkPresent(c *C) { + defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. + s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoSymlink) + s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, syscall.EEXIST) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Mode: syscall.S_IFLNK}) + s.sys.InsertReadlinkatResult(`readlinkat 4 "" `, "/evil") + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot create symbolic link "/name": existing symbolic link in the way`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `lstat "/name"`, R: testutil.FileInfoSymlink}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `symlinkat "/oldname" 3 "name"`, E: syscall.EEXIST}, + {C: `openat 3 "name" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{Mode: syscall.S_IFLNK}}, + {C: `readlinkat 4 "" `, R: "/evil"}, + {C: `close 4`}, + {C: `close 3`}, + }) +} + +func (s *changeSuite) TestPerformRemoveSymlink(c *C) { + chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `remove "/name"`}, + }) +} + +// Change.Perform wants to create a symlink in /etc and the write is made private. +func (s *changeSuite) TestPerformCreateSymlinkWithAvoidedTrespassing(c *C) { + defer s.as.MockUnrestrictedPaths("/tmp/")() // Allow writing to /tmp + + s.sys.InsertFault(`lstat "/etc/demo.conf"`, syscall.ENOENT) + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) + s.sys.InsertFstatfsResult(`fstatfs 4 `, + // On 1st call ext4, on 2nd call tmpfs + syscall.Statfs_t{Type: update.Ext4Magic}, + syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertSysLstatResult(`lstat "/etc" `, syscall.Stat_t{Mode: 0755}) + otherConf := testutil.FakeFileInfo("other.conf", 0755) + s.sys.InsertReadDirResult(`readdir "/etc"`, []os.FileInfo{otherConf}) + s.sys.InsertFault(`lstat "/tmp/.snap/etc"`, syscall.ENOENT) + s.sys.InsertFault(`lstat "/tmp/.snap/etc/other.conf"`, syscall.ENOENT) + s.sys.InsertOsLstatResult(`lstat "/etc"`, testutil.FileInfoDir) + s.sys.InsertOsLstatResult(`lstat "/etc/other.conf"`, otherConf) + s.sys.InsertFault(`mkdirat 3 "tmp" 0755`, syscall.EEXIST) + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{Mode: syscall.S_IFREG}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Mode: syscall.S_IFDIR}) + s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{Mode: syscall.S_IFDIR}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{}) + + // This is the change we want to perform: + // put a layout symlink at /etc/demo.conf -> /oldname + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/etc/demo.conf", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} + synth, err := chg.Perform(s.as) + c.Check(err, IsNil) + c.Check(synth, HasLen, 2) + // We have created some synthetic change (made /etc a new tmpfs and re-populate it) + c.Assert(synth[0], DeepEquals, &update.Change{ + Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/etc", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/etc/demo.conf", "mode=0755", "uid=0", "gid=0"}}, + Action: "mount"}) + c.Assert(synth[1], DeepEquals, &update.Change{ + Entry: osutil.MountEntry{Name: "/etc/other.conf", Dir: "/etc/other.conf", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/etc/demo.conf"}}, + Action: "mount"}) + + // And this is exactly how we made that happen: + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + // Attempt to construct a symlink /etc/demo.conf -> /oldname. + // This stops as soon as we notice that /etc is an ext4 filesystem. + // To avoid writing to it directly we need a writable mimic. + {C: `lstat "/etc/demo.conf"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, + {C: `fstat 3 `, R: syscall.Stat_t{}}, + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, + {C: `fstat 4 `, R: syscall.Stat_t{Mode: 0x4000}}, + {C: `close 4`}, + + // Create a writable mimic over /etc, scan the contents of /etc first. + // For convenience we pretend that /etc is empty. The mimic + // replicates /etc in /tmp/.snap/etc for subsequent re-construction. + {C: `lstat "/etc" `, R: syscall.Stat_t{Mode: 0755}}, + {C: `readdir "/etc"`, R: []os.FileInfo{otherConf}}, + {C: `lstat "/tmp/.snap/etc"`, E: syscall.ENOENT}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "tmp" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `mkdirat 4 ".snap" 0755`}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `fchown 5 0 0`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `mkdirat 5 "etc" 0755`}, + {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 5`}, + + // Prepare a secure bind mount operation /etc -> /tmp/.snap/etc + {C: `lstat "/etc"`, R: testutil.FileInfoDir}, + + // Open an O_PATH descriptor to /etc. We need this as a source of a + // secure bind mount operation. We also ensure that the descriptor + // refers to a directory. + // NOTE: we keep fd 4 open for subsequent use. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: syscall.Stat_t{Mode: syscall.S_IFDIR}}, + {C: `close 3`}, + + // Open an O_PATH descriptor to /tmp/.snap/etc. We need this as a + // target of a secure bind mount operation. We also ensure that the + // descriptor refers to a directory. + // NOTE: we keep fd 7 open for subsequent use. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, + {C: `openat 6 "etc" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, + {C: `fstat 7 `, R: syscall.Stat_t{Mode: syscall.S_IFDIR}}, + {C: `close 6`}, + {C: `close 5`}, + {C: `close 3`}, + + // Perform the secure bind mount operation /etc -> /tmp/.snap/etc + // and release the two associated file descriptors. + {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, + {C: `close 7`}, + {C: `close 4`}, + + // Mount a tmpfs over /etc, re-constructing the original mode and + // ownership. Bind mount each original file over and detach the copy + // of /etc we had in /tmp/.snap/etc. + + {C: `lstat "/etc"`, R: testutil.FileInfoDir}, + {C: `mount "tmpfs" "/etc" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, + // Here we restore the contents of /etc: here it's just one file - other.conf + {C: `lstat "/etc/other.conf"`, R: otherConf}, + {C: `lstat "/tmp/.snap/etc/other.conf"`, E: syscall.ENOENT}, + + // Create /tmp/.snap/etc/other.conf as an empty file. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "tmp" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `mkdirat 4 ".snap" 0755`}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `fchown 5 0 0`}, + {C: `mkdirat 5 "etc" 0755`}, + {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 6}, + {C: `fchown 6 0 0`}, + {C: `close 5`}, + {C: `close 4`}, + {C: `close 3`}, + // NOTE: This is without O_DIRECTORY and with O_CREAT|O_EXCL, + // we are creating an empty file for the subsequent bind mount. + {C: `openat 6 "other.conf" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 6`}, + + // Open O_PATH to /tmp/.snap/etc/other.conf + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, + {C: `openat 6 "other.conf" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, + {C: `fstat 7 `, R: syscall.Stat_t{Mode: syscall.S_IFDIR}}, + {C: `close 6`}, + {C: `close 5`}, + {C: `close 4`}, + {C: `close 3`}, + + // Open O_PATH to /etc/other.conf + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "other.conf" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{Mode: syscall.S_IFREG}}, + {C: `close 4`}, + {C: `close 3`}, + + // Restore the /etc/other.conf file with a secure bind mount. + {C: `mount "/proc/self/fd/7" "/proc/self/fd/5" "" MS_BIND ""`}, + {C: `close 5`}, + {C: `close 7`}, + + // We're done restoring now. + {C: `unmount "/tmp/.snap/etc" UMOUNT_NOFOLLOW|MNT_DETACH`}, + + // Perform clean up after the unmount operation. + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, + {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 5`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `fstatfs 6 `, R: syscall.Statfs_t{}}, + {C: `remove "/tmp/.snap/etc"`}, + {C: `close 6`}, + + // The mimic is now complete and subsequent writes to /etc are private + // to the mount namespace of the process. + + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, + {C: `fstat 3 `, R: syscall.Stat_t{}}, + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, + {C: `fstat 4 `, R: syscall.Stat_t{Mode: 0x4000}}, + {C: `symlinkat "/oldname" 4 "demo.conf"`}, + {C: `close 4`}, + }) +} + +// ########### +// Topic: misc +// ########### + +// Change.Perform handles unknown actions. +func (s *changeSuite) TestPerformUnknownAction(c *C) { + chg := &update.Change{Action: update.Action(42)} + synth, err := chg.Perform(s.as) + c.Assert(err, ErrorMatches, `cannot process mount change: unknown action: .*`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), HasLen, 0) +} + +// Change.Perform wants to keep a mount entry unchanged. +func (s *changeSuite) TestPerformKeep(c *C) { + chg := &update.Change{Action: update.Keep} + synth, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.RCalls(), HasLen, 0) +} + +// ############################################ +// Topic: change history tracked in Assumptions +// ############################################ + +func (s *changeSuite) TestPerformedChangesAreTracked(c *C) { + s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) + c.Assert(s.as.PastChanges(), HasLen, 0) + + chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + _, err := chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(s.as.PastChanges(), DeepEquals, []*update.Change{ + {Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}, + }) + + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{}) + chg = &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} + _, err = chg.Perform(s.as) + c.Assert(err, IsNil) + c.Assert(s.as.PastChanges(), DeepEquals, []*update.Change{ + // past changes stack in order. + {Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}, + {Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}, + }) +} diff --git a/cmd/snap-update-ns/export_test.go b/cmd/snap-update-ns/export_test.go new file mode 100644 index 00000000..74865fc6 --- /dev/null +++ b/cmd/snap-update-ns/export_test.go @@ -0,0 +1,183 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "os" + "syscall" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/osutil/sys" +) + +var ( + // change + ValidateInstanceName = validateInstanceName + ProcessArguments = processArguments + // freezer + FreezeSnapProcesses = freezeSnapProcesses + ThawSnapProcesses = thawSnapProcesses + // utils + PlanWritableMimic = planWritableMimic + ExecWritableMimic = execWritableMimic + + // main + ComputeAndSaveChanges = computeAndSaveChanges + ApplyUserFstab = applyUserFstab + + // bootstrap + ClearBootstrapError = clearBootstrapError + + // trespassing + IsReadOnly = isReadOnly + IsPrivateTmpfsCreatedBySnapd = isPrivateTmpfsCreatedBySnapd +) + +// SystemCalls encapsulates various system interactions performed by this module. +type SystemCalls interface { + OsLstat(name string) (os.FileInfo, error) + SysLstat(name string, buf *syscall.Stat_t) error + ReadDir(dirname string) ([]os.FileInfo, error) + Symlinkat(oldname string, dirfd int, newname string) error + Readlinkat(dirfd int, path string, buf []byte) (int, error) + Remove(name string) error + + Close(fd int) error + Fchdir(fd int) error + Fchown(fd int, uid sys.UserID, gid sys.GroupID) error + Mkdirat(dirfd int, path string, mode uint32) error + Mount(source string, target string, fstype string, flags uintptr, data string) (err error) + Open(path string, flags int, mode uint32) (fd int, err error) + Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) + Unmount(target string, flags int) error + Fstat(fd int, buf *syscall.Stat_t) error + Fstatfs(fd int, buf *syscall.Statfs_t) error +} + +// MockSystemCalls replaces real system calls with those of the argument. +func MockSystemCalls(sc SystemCalls) (restore func()) { + // save + oldOsLstat := osLstat + oldRemove := osRemove + oldIoutilReadDir := ioutilReadDir + + oldSysClose := sysClose + oldSysFchown := sysFchown + oldSysMkdirat := sysMkdirat + oldSysMount := sysMount + oldSysOpen := sysOpen + oldSysOpenat := sysOpenat + oldSysUnmount := sysUnmount + oldSysSymlinkat := sysSymlinkat + oldReadlinkat := sysReadlinkat + oldFstat := sysFstat + oldFstatfs := sysFstatfs + oldSysFchdir := sysFchdir + oldSysLstat := sysLstat + + // override + osLstat = sc.OsLstat + osRemove = sc.Remove + ioutilReadDir = sc.ReadDir + + sysClose = sc.Close + sysFchown = sc.Fchown + sysMkdirat = sc.Mkdirat + sysMount = sc.Mount + sysOpen = sc.Open + sysOpenat = sc.Openat + sysUnmount = sc.Unmount + sysSymlinkat = sc.Symlinkat + sysReadlinkat = sc.Readlinkat + sysFstat = sc.Fstat + sysFstatfs = sc.Fstatfs + sysFchdir = sc.Fchdir + sysLstat = sc.SysLstat + + return func() { + // restore + osLstat = oldOsLstat + osRemove = oldRemove + ioutilReadDir = oldIoutilReadDir + + sysClose = oldSysClose + sysFchown = oldSysFchown + sysMkdirat = oldSysMkdirat + sysMount = oldSysMount + sysOpen = oldSysOpen + sysOpenat = oldSysOpenat + sysUnmount = oldSysUnmount + sysSymlinkat = oldSysSymlinkat + sysReadlinkat = oldReadlinkat + sysFstat = oldFstat + sysFstatfs = oldFstatfs + sysFchdir = oldSysFchdir + sysLstat = oldSysLstat + } +} + +func MockFreezerCgroupDir(c *C) (restore func()) { + old := freezerCgroupDir + freezerCgroupDir = c.MkDir() + return func() { + freezerCgroupDir = old + } +} + +func FreezerCgroupDir() string { + return freezerCgroupDir +} + +func MockChangePerform(f func(chg *Change, as *Assumptions) ([]*Change, error)) func() { + origChangePerform := changePerform + changePerform = f + return func() { + changePerform = origChangePerform + } +} + +func MockReadDir(fn func(string) ([]os.FileInfo, error)) (restore func()) { + old := ioutilReadDir + ioutilReadDir = fn + return func() { + ioutilReadDir = old + } +} + +func MockReadlink(fn func(string) (string, error)) (restore func()) { + old := osReadlink + osReadlink = fn + return func() { + osReadlink = old + } +} + +func (as *Assumptions) IsRestricted(path string) bool { + return as.isRestricted(path) +} + +func (as *Assumptions) PastChanges() []*Change { + return as.pastChanges +} + +func (as *Assumptions) CanWriteToDirectory(dirFd int, dirName string) (bool, error) { + return as.canWriteToDirectory(dirFd, dirName) +} diff --git a/cmd/snap-update-ns/freezer.go b/cmd/snap-update-ns/freezer.go new file mode 100644 index 00000000..3d09d5dd --- /dev/null +++ b/cmd/snap-update-ns/freezer.go @@ -0,0 +1,74 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" +) + +var freezerCgroupDir = "/sys/fs/cgroup/freezer" + +// freezeSnapProcesses freezes all the processes originating from the given snap. +// Processes are frozen regardless of which particular snap application they +// originate from. +func freezeSnapProcesses(snapName string) error { + fname := filepath.Join(freezerCgroupDir, fmt.Sprintf("snap.%s", snapName), "freezer.state") + if err := ioutil.WriteFile(fname, []byte("FROZEN"), 0644); err != nil && os.IsNotExist(err) { + // When there's no freezer cgroup we don't have to freeze anything. + // This can happen when no process belonging to a given snap has been + // started yet. + return nil + } else if err != nil { + return fmt.Errorf("cannot freeze processes of snap %q, %v", snapName, err) + } + for i := 0; i < 30; i++ { + data, err := ioutil.ReadFile(fname) + if err != nil { + return fmt.Errorf("cannot determine the freeze state of processes of snap %q, %v", snapName, err) + } + // If the cgroup is still freezing then wait a moment and try again. + if bytes.Equal(data, []byte("FREEZING")) { + time.Sleep(100 * time.Millisecond) + continue + } + return nil + } + // If we got here then we timed out after seeing FREEZING for too long. + thawSnapProcesses(snapName) // ignore the error, this is best-effort. + return fmt.Errorf("cannot finish freezing processes of snap %q", snapName) +} + +func thawSnapProcesses(snapName string) error { + fname := filepath.Join(freezerCgroupDir, fmt.Sprintf("snap.%s", snapName), "freezer.state") + if err := ioutil.WriteFile(fname, []byte("THAWED"), 0644); err != nil && os.IsNotExist(err) { + // When there's no freezer cgroup we don't have to thaw anything. + // This can happen when no process belonging to a given snap has been + // started yet. + return nil + } else if err != nil { + return fmt.Errorf("cannot thaw processes of snap %q", snapName) + } + return nil +} diff --git a/cmd/snap-update-ns/freezer_test.go b/cmd/snap-update-ns/freezer_test.go new file mode 100644 index 00000000..89813ffb --- /dev/null +++ b/cmd/snap-update-ns/freezer_test.go @@ -0,0 +1,91 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "fmt" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/testutil" +) + +type freezerSuite struct{} + +var _ = Suite(&freezerSuite{}) + +func (s *freezerSuite) TestFreezeSnapProcesses(c *C) { + restore := update.MockFreezerCgroupDir(c) + defer restore() + + n := "foo" // snap name + p := filepath.Join(update.FreezerCgroupDir(), fmt.Sprintf("snap.%s", n)) // snap freezer cgroup + f := filepath.Join(p, "freezer.state") // freezer.state file of the cgroup + + // When the freezer cgroup filesystem doesn't exist we do nothing at all. + c.Assert(update.FreezeSnapProcesses(n), IsNil) + _, err := os.Stat(f) + c.Assert(os.IsNotExist(err), Equals, true) + + // When the freezer cgroup filesystem exists but the particular cgroup + // doesn't exist we don nothing at all. + c.Assert(os.MkdirAll(update.FreezerCgroupDir(), 0755), IsNil) + c.Assert(update.FreezeSnapProcesses(n), IsNil) + _, err = os.Stat(f) + c.Assert(os.IsNotExist(err), Equals, true) + + // When the cgroup exists we write FROZEN the freezer.state file. + c.Assert(os.MkdirAll(p, 0755), IsNil) + c.Assert(update.FreezeSnapProcesses(n), IsNil) + _, err = os.Stat(f) + c.Assert(err, IsNil) + c.Assert(f, testutil.FileEquals, `FROZEN`) +} + +func (s *freezerSuite) TestThawSnapProcesses(c *C) { + restore := update.MockFreezerCgroupDir(c) + defer restore() + + n := "foo" // snap name + p := filepath.Join(update.FreezerCgroupDir(), fmt.Sprintf("snap.%s", n)) // snap freezer cgroup + f := filepath.Join(p, "freezer.state") // freezer.state file of the cgroup + + // When the freezer cgroup filesystem doesn't exist we do nothing at all. + c.Assert(update.ThawSnapProcesses(n), IsNil) + _, err := os.Stat(f) + c.Assert(os.IsNotExist(err), Equals, true) + + // When the freezer cgroup filesystem exists but the particular cgroup + // doesn't exist we don nothing at all. + c.Assert(os.MkdirAll(update.FreezerCgroupDir(), 0755), IsNil) + c.Assert(update.ThawSnapProcesses(n), IsNil) + _, err = os.Stat(f) + c.Assert(os.IsNotExist(err), Equals, true) + + // When the cgroup exists we write THAWED the freezer.state file. + c.Assert(os.MkdirAll(p, 0755), IsNil) + c.Assert(update.ThawSnapProcesses(n), IsNil) + _, err = os.Stat(f) + c.Assert(err, IsNil) + c.Assert(f, testutil.FileEquals, `THAWED`) +} diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go new file mode 100644 index 00000000..c7baa7c8 --- /dev/null +++ b/cmd/snap-update-ns/main.go @@ -0,0 +1,291 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +var opts struct { + FromSnapConfine bool `long:"from-snap-confine"` + UserMounts bool `long:"user-mounts"` + Positionals struct { + SnapName string `positional-arg-name:"SNAP_NAME" required:"yes"` + } `positional-args:"true"` +} + +// IMPORTANT: all the code in main() until bootstrap is finished may be run +// with elevated privileges when invoking snap-update-ns from the setuid +// snap-confine. + +func main() { + logger.SimpleSetup() + if err := run(); err != nil { + fmt.Printf("cannot update snap namespace: %s\n", err) + os.Exit(1) + } + // END IMPORTANT +} + +func parseArgs(args []string) error { + parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) + _, err := parser.ParseArgs(args) + return err +} + +// IMPORTANT: all the code in run() until BootStrapError() is finished may +// be run with elevated privileges when invoking snap-update-ns from +// the setuid snap-confine. + +func run() error { + // There is some C code that runs before main() is started. + // That code always runs and sets an error condition if it fails. + // Here we just check for the error. + if err := BootstrapError(); err != nil { + // If there is no mount namespace to transition to let's just quit + // instantly without any errors as there is nothing to do anymore. + if err == ErrNoNamespace { + logger.Debugf("no preserved mount namespace, nothing to update") + return nil + } + return err + } + // END IMPORTANT + + if err := parseArgs(os.Args[1:]); err != nil { + return err + } + + if opts.UserMounts { + return applyUserFstab(opts.Positionals.SnapName) + } + return applyFstab(opts.Positionals.SnapName, opts.FromSnapConfine) +} + +func applyFstab(instanceName string, fromSnapConfine bool) error { + // Lock the mount namespace so that any concurrently attempted invocations + // of snap-confine are synchronized and will see consistent state. + lock, err := mount.OpenLock(instanceName) + if err != nil { + return fmt.Errorf("cannot open lock file for mount namespace of snap %q: %s", instanceName, err) + } + defer func() { + logger.Debugf("unlocking mount namespace of snap %q", instanceName) + lock.Close() + }() + + logger.Debugf("locking mount namespace of snap %q", instanceName) + if fromSnapConfine { + // When --from-snap-confine is passed then we just ensure that the + // namespace is locked. This is used by snap-confine to use + // snap-update-ns to apply mount profiles. + if err := lock.TryLock(); err != osutil.ErrAlreadyLocked { + return fmt.Errorf("mount namespace of snap %q is not locked but --from-snap-confine was used", instanceName) + } + } else { + if err := lock.Lock(); err != nil { + return fmt.Errorf("cannot lock mount namespace of snap %q: %s", instanceName, err) + } + } + + // Freeze the mount namespace and unfreeze it later. This lets us perform + // modifications without snap processes attempting to construct + // symlinks or perform other malicious activity (such as attempting to + // introduce a symlink that would cause us to mount something other + // than what we expected). + logger.Debugf("freezing processes of snap %q", instanceName) + if err := freezeSnapProcesses(instanceName); err != nil { + return err + } + defer func() { + logger.Debugf("thawing processes of snap %q", instanceName) + thawSnapProcesses(instanceName) + }() + + // Allow creating directories related to this snap name. + // + // Note that we allow /var/snap instead of /var/snap/$SNAP_NAME because + // content interface connections can readily create missing mount points on + // both sides of the interface connection. + // + // We scope /snap/$SNAP_NAME because only one side of the connection can be + // created, as snaps are read-only, the mimic construction will kick-in and + // create the missing directory but this directory is only visible from the + // snap that we are operating on (either plug or slot side, the point is, + // the mount point is not universally visible). + // + // /snap/$SNAP_NAME needs to be there as the code that creates such mount + // points must traverse writable host filesystem that contains /snap/*/ and + // normally such access is off-limits. This approach allows /snap/foo + // without allowing /snap/bin, for example. + // + // /snap/$SNAP_INSTANCE_NAME and /snap/$SNAP_NAME are added to allow + // remapping for parallel installs only when the snap has an instance key + // + // TODO: Handle /home/*/snap/* when we do per-user mount namespaces and + // allow defining layout items that refer to SNAP_USER_DATA and + // SNAP_USER_COMMON. + as := &Assumptions{} + as.AddUnrestrictedPaths("/tmp", "/var/snap", "/snap/"+instanceName) + if snapName := snap.InstanceSnap(instanceName); snapName != instanceName { + as.AddUnrestrictedPaths("/snap/" + snapName) + } + return computeAndSaveChanges(instanceName, as) +} + +func computeAndSaveChanges(snapName string, as *Assumptions) error { + // Read the desired and current mount profiles. Note that missing files + // count as empty profiles so that we can gracefully handle a mount + // interface connection/disconnection. + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + desired, err := osutil.LoadMountProfile(desiredProfilePath) + if err != nil { + return fmt.Errorf("cannot load desired mount profile of snap %q: %s", snapName, err) + } + debugShowProfile(desired, "desired mount profile") + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + currentBefore, err := osutil.LoadMountProfile(currentProfilePath) + if err != nil { + return fmt.Errorf("cannot load current mount profile of snap %q: %s", snapName, err) + } + debugShowProfile(currentBefore, "current mount profile (before applying changes)") + // Synthesize mount changes that were applied before for the purpose of the tmpfs detector. + for _, entry := range currentBefore.Entries { + as.AddChange(&Change{Action: Mount, Entry: entry}) + } + + currentAfter, err := applyProfile(snapName, currentBefore, desired, as) + if err != nil { + return err + } + + logger.Debugf("saving current mount profile of snap %q", snapName) + if err := currentAfter.Save(currentProfilePath); err != nil { + return fmt.Errorf("cannot save current mount profile of snap %q: %s", snapName, err) + } + return nil +} + +func applyProfile(snapName string, currentBefore, desired *osutil.MountProfile, as *Assumptions) (*osutil.MountProfile, error) { + // Compute the needed changes and perform each change if + // needed, collecting those that we managed to perform or that + // were performed already. + changesNeeded := NeededChanges(currentBefore, desired) + debugShowChanges(changesNeeded, "mount changes needed") + + logger.Debugf("performing mount changes:") + var changesMade []*Change + for _, change := range changesNeeded { + logger.Debugf("\t * %s", change) + synthesised, err := changePerform(change, as) + changesMade = append(changesMade, synthesised...) + if len(synthesised) > 0 { + logger.Debugf("\tsynthesised additional mount changes:") + for _, synth := range synthesised { + logger.Debugf(" * \t\t%s", synth) + } + } + if err != nil { + // We may have done something even if Perform itself has + // failed. We need to collect synthesized changes and + // store them. + origin := change.Entry.XSnapdOrigin() + if origin == "layout" || origin == "overname" { + return nil, err + } else if err != ErrIgnoredMissingMount { + logger.Noticef("cannot change mount namespace of snap %q according to change %s: %s", snapName, change, err) + } + continue + } + + changesMade = append(changesMade, change) + } + + // Compute the new current profile so that it contains only changes that were made + // and save it back for next runs. + var currentAfter osutil.MountProfile + for _, change := range changesMade { + if change.Action == Mount || change.Action == Keep { + currentAfter.Entries = append(currentAfter.Entries, change.Entry) + } + } + debugShowProfile(¤tAfter, "current mount profile (after applying changes)") + return ¤tAfter, nil +} + +func debugShowProfile(profile *osutil.MountProfile, header string) { + if len(profile.Entries) > 0 { + logger.Debugf("%s:", header) + for _, entry := range profile.Entries { + logger.Debugf("\t%s", entry) + } + } else { + logger.Debugf("%s: (none)", header) + } +} + +func debugShowChanges(changes []*Change, header string) { + if len(changes) > 0 { + logger.Debugf("%s:", header) + for _, change := range changes { + logger.Debugf("\t%s", change) + } + } else { + logger.Debugf("%s: (none)", header) + } +} + +func applyUserFstab(snapName string) error { + desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) + desired, err := osutil.LoadMountProfile(desiredProfilePath) + if err != nil { + return fmt.Errorf("cannot load desired user mount profile of snap %q: %s", snapName, err) + } + + // Replace XDG_RUNTIME_DIR in mount profile + xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) + for i := range desired.Entries { + if strings.HasPrefix(desired.Entries[i].Name, "$XDG_RUNTIME_DIR/") { + desired.Entries[i].Name = strings.Replace(desired.Entries[i].Name, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) + } + if strings.HasPrefix(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR/") { + desired.Entries[i].Dir = strings.Replace(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) + } + } + + debugShowProfile(desired, "desired mount profile") + + // TODO: configure the secure helper and inform it about directories that + // can be created without trespassing. + as := &Assumptions{} + _, err = applyProfile(snapName, &osutil.MountProfile{}, desired, as) + return err +} diff --git a/cmd/snap-update-ns/main_test.go b/cmd/snap-update-ns/main_test.go new file mode 100644 index 00000000..82628a3c --- /dev/null +++ b/cmd/snap-update-ns/main_test.go @@ -0,0 +1,387 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/testutil" +) + +func Test(t *testing.T) { TestingT(t) } + +type mainSuite struct { + testutil.BaseTest + as *update.Assumptions + log *bytes.Buffer +} + +var _ = Suite(&mainSuite{}) + +func (s *mainSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.as = &update.Assumptions{} + buf, restore := logger.MockLogger() + s.BaseTest.AddCleanup(restore) + s.log = buf +} + +func (s *mainSuite) TestComputeAndSaveChanges(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + return nil, nil + }) + defer restore() + + snapName := "foo" + desiredProfileContent := `/var/lib/snapd/hostfs/usr/share/fonts /usr/share/fonts none bind,ro 0 0 +/var/lib/snapd/hostfs/usr/local/share/fonts /usr/local/share/fonts none bind,ro 0 0` + + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + err := os.MkdirAll(filepath.Dir(desiredProfilePath), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644) + c.Assert(err, IsNil) + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + err = os.MkdirAll(filepath.Dir(currentProfilePath), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(currentProfilePath, nil, 0644) + c.Assert(err, IsNil) + + err = update.ComputeAndSaveChanges(snapName, s.as) + c.Assert(err, IsNil) + + c.Check(currentProfilePath, testutil.FileEquals, `/var/lib/snapd/hostfs/usr/local/share/fonts /usr/local/share/fonts none bind,ro 0 0 +/var/lib/snapd/hostfs/usr/share/fonts /usr/share/fonts none bind,ro 0 0 +`) +} + +func (s *mainSuite) TestAddingSyntheticChanges(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + // The snap `mysnap` wishes to export it's usr/share/mysnap directory and + // make it appear as if it was in /usr/share/mysnap directly. + const snapName = "mysnap" + const currentProfileContent = "" + const desiredProfileContent = "/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0" + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + + c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) + c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) + c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) + + // In order to make that work, /usr/share had to be converted to a writable + // mimic. Some actions were performed under the hood and now we see a + // subset of them as synthetic changes here. + // + // Note that if you compare this to the code that plans a writable mimic + // you will see that there are additional changes that are _not_ + // represented here. The changes have only one goal: tell + // snap-update-ns how the mimic can be undone in case it is no longer + // needed. + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + // The change that we were asked to perform is to create a bind mount + // from within the snap to /usr/share/mysnap. + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Mount, Entry: osutil.MountEntry{ + Name: "/snap/mysnap/42/usr/share/mysnap", + Dir: "/usr/share/mysnap", Type: "none", + Options: []string{"bind", "ro"}}}) + synthetic := []*update.Change{ + // The original directory (which was a part of the core snap and is + // read only) was hidden with a tmpfs. + {Action: update.Mount, Entry: osutil.MountEntry{ + Dir: "/usr/share", Name: "tmpfs", Type: "tmpfs", + Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, + // For the sake of brevity we will only represent a few of the + // entries typically there. Normally this list can get quite long. + // Also note that the entry is a little fake. In reality it was + // constructed using a temporary bind mount that contained the + // original mount entries of /usr/share but this fact was lost. + // Again, the only point of this entry is to correctly perform an + // undo operation when /usr/share/mysnap is no longer needed. + {Action: update.Mount, Entry: osutil.MountEntry{ + Dir: "/usr/share/adduser", Name: "/usr/share/adduser", + Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, + {Action: update.Mount, Entry: osutil.MountEntry{ + Dir: "/usr/share/awk", Name: "/usr/share/awk", + Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, + } + return synthetic, nil + }) + defer restore() + + c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil) + + c.Check(currentProfilePath, testutil.FileEquals, + `tmpfs /usr/share tmpfs x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/usr/share/adduser /usr/share/adduser none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/usr/share/awk /usr/share/awk none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0 +`) +} + +func (s *mainSuite) TestRemovingSyntheticChanges(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + // The snap `mysnap` no longer wishes to export it's usr/share/mysnap + // directory. All the synthetic changes that were associated with that mount + // entry can be discarded. + const snapName = "mysnap" + const currentProfileContent = `tmpfs /usr/share tmpfs x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/usr/share/adduser /usr/share/adduser none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/usr/share/awk /usr/share/awk none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0 +` + const desiredProfileContent = "" + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + + c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) + c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) + c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) + + n := -1 + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + n++ + switch n { + case 0: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Unmount, + Entry: osutil.MountEntry{ + Name: "/snap/mysnap/42/usr/share/mysnap", + Dir: "/usr/share/mysnap", Type: "none", + Options: []string{"bind", "ro"}, + }, + }) + case 1: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Unmount, + Entry: osutil.MountEntry{ + Name: "/usr/share/awk", Dir: "/usr/share/awk", Type: "none", + Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}, + }, + }) + case 2: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Unmount, + Entry: osutil.MountEntry{ + Name: "/usr/share/adduser", Dir: "/usr/share/adduser", Type: "none", + Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}, + }, + }) + case 3: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Unmount, + Entry: osutil.MountEntry{ + Name: "tmpfs", Dir: "/usr/share", Type: "tmpfs", + Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}, + }, + }) + default: + panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) + } + return nil, nil + }) + defer restore() + + c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil) + + c.Check(currentProfilePath, testutil.FileEquals, "") +} + +func (s *mainSuite) TestApplyingLayoutChanges(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + const snapName = "mysnap" + const currentProfileContent = "" + const desiredProfileContent = "/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro,x-snapd.origin=layout 0 0" + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + + c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) + c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) + c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) + + n := -1 + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + n++ + switch n { + case 0: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Mount, + Entry: osutil.MountEntry{ + Name: "/snap/mysnap/42/usr/share/mysnap", + Dir: "/usr/share/mysnap", Type: "none", + Options: []string{"bind", "ro", "x-snapd.origin=layout"}, + }, + }) + return nil, fmt.Errorf("testing") + default: + panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) + } + }) + defer restore() + + // The error was not ignored, we bailed out. + c.Assert(update.ComputeAndSaveChanges(snapName, s.as), ErrorMatches, "testing") + + c.Check(currentProfilePath, testutil.FileEquals, "") +} + +func (s *mainSuite) TestApplyingParallelInstanceChanges(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + const snapName = "mysnap" + const currentProfileContent = "" + const desiredProfileContent = "/snap/mysnap_foo /snap/mysnap none rbind,x-snapd.origin=overname 0 0" + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + + c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) + c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) + c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) + + n := -1 + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + n++ + switch n { + case 0: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Mount, + Entry: osutil.MountEntry{ + Name: "/snap/mysnap_foo", + Dir: "/snap/mysnap", Type: "none", + Options: []string{"rbind", "x-snapd.origin=overname"}, + }, + }) + return nil, fmt.Errorf("testing") + default: + panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) + } + }) + defer restore() + + // The error was not ignored, we bailed out. + c.Assert(update.ComputeAndSaveChanges(snapName, nil), ErrorMatches, "testing") + + c.Check(currentProfilePath, testutil.FileEquals, "") +} + +func (s *mainSuite) TestApplyIgnoredMissingMount(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + const snapName = "mysnap" + const currentProfileContent = "" + const desiredProfileContent = "/source /target none bind,x-snapd.ignore-missing 0 0" + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + + c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) + c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) + c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) + + n := -1 + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + n++ + switch n { + case 0: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Mount, + Entry: osutil.MountEntry{ + Name: "/source", + Dir: "/target", + Type: "none", + Options: []string{"bind", "x-snapd.ignore-missing"}, + }, + }) + return nil, update.ErrIgnoredMissingMount + default: + panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) + } + }) + defer restore() + + // The error was ignored, and no mount was recorded in the profile + c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil) + c.Check(s.log.String(), Equals, "") + c.Check(currentProfilePath, testutil.FileEquals, "") +} + +func (s *mainSuite) TestApplyUserFstab(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + var changes []update.Change + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + changes = append(changes, *chg) + return nil, nil + }) + defer restore() + + snapName := "foo" + desiredProfileContent := `$XDG_RUNTIME_DIR/doc/by-app/snap.foo $XDG_RUNTIME_DIR/doc none bind,rw 0 0` + + desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) + err := os.MkdirAll(filepath.Dir(desiredProfilePath), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644) + c.Assert(err, IsNil) + + err = update.ApplyUserFstab("foo") + c.Assert(err, IsNil) + + xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) + + c.Assert(changes, HasLen, 1) + c.Assert(changes[0].Action, Equals, update.Mount) + c.Assert(changes[0].Entry.Name, Equals, xdgRuntimeDir+"/doc/by-app/snap.foo") + c.Assert(changes[0].Entry.Dir, Matches, xdgRuntimeDir+"/doc") +} diff --git a/cmd/snap-update-ns/secure_bindmount.go b/cmd/snap-update-ns/secure_bindmount.go new file mode 100644 index 00000000..c000d55b --- /dev/null +++ b/cmd/snap-update-ns/secure_bindmount.go @@ -0,0 +1,97 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "syscall" +) + +// BindMount performs a bind mount between two absolute paths containing no +// symlinks. +func BindMount(sourceDir, targetDir string, flags uint) error { + // This function only attempts to handle bind mounts. Expanding to other + // mounts will require examining do_mount() from fs/namespace.c of the + // kernel that called functions (eventually) verify `DCACHE_CANT_MOUNT` is + // not set (eg, by calling lock_mount()). + if flags&syscall.MS_BIND == 0 { + return fmt.Errorf("cannot perform non-bind mount operation") + } + + // The kernel doesn't support recursively switching a tree of bind mounts + // to read only, and we haven't written a work around. + if flags&syscall.MS_RDONLY != 0 && flags&syscall.MS_REC != 0 { + return fmt.Errorf("cannot use MS_RDONLY and MS_REC together") + } + + // Step 1: acquire file descriptors representing the source and destination + // directories, ensuring no symlinks are followed. + sourceFd, err := OpenPath(sourceDir) + if err != nil { + return err + } + defer sysClose(sourceFd) + targetFd, err := OpenPath(targetDir) + if err != nil { + return err + } + defer sysClose(targetFd) + + // Step 2: perform a bind mount between the paths identified by the two + // file descriptors. We primarily care about privilege escalation here and + // trying to race the sysMount() by removing any part of the dir (sourceDir + // or targetDir) after we have an open file descriptor to it (sourceFd or + // targetFd) to then replace an element of the dir's path with a symlink + // will cause the fd path (ie, sourceFdPath or targetFdPath) to be marked + // as unmountable within the kernel (this path is also changed to show as + // '(deleted)'). Alternatively, simply renaming the dir (sourceDir or + // targetDir) after we have an open file descriptor to it (sourceFd or + // targetFd) causes the mount to happen with the newly renamed path, but + // this rename is controlled by DAC so while the user could race the mount + // source or target, this rename can't be used to gain privileged access to + // files. For systems with AppArmor enabled, this raced rename would be + // denied by the per-snap snap-update-ns AppArmor profle. + sourceFdPath := fmt.Sprintf("/proc/self/fd/%d", sourceFd) + targetFdPath := fmt.Sprintf("/proc/self/fd/%d", targetFd) + bindFlags := syscall.MS_BIND | (flags & syscall.MS_REC) + if err := sysMount(sourceFdPath, targetFdPath, "", uintptr(bindFlags), ""); err != nil { + return err + } + + // Step 3: optionally change to readonly + if flags&syscall.MS_RDONLY != 0 { + // We need to look up the target directory a second time, because + // targetFd refers to the path shadowed by the mount point. + mountFd, err := OpenPath(targetDir) + if err != nil { + // FIXME: the mount occurred, but the user moved the target + // somewhere + return err + } + defer sysClose(mountFd) + mountFdPath := fmt.Sprintf("/proc/self/fd/%d", mountFd) + remountFlags := syscall.MS_REMOUNT | syscall.MS_BIND | syscall.MS_RDONLY + if err := sysMount("none", mountFdPath, "", uintptr(remountFlags), ""); err != nil { + sysUnmount(mountFdPath, syscall.MNT_DETACH|umountNoFollow) + return err + } + } + return nil +} diff --git a/cmd/snap-update-ns/secure_bindmount_test.go b/cmd/snap-update-ns/secure_bindmount_test.go new file mode 100644 index 00000000..9d62cc79 --- /dev/null +++ b/cmd/snap-update-ns/secure_bindmount_test.go @@ -0,0 +1,200 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 ( + "syscall" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/testutil" +) + +type secureBindMountSuite struct { + testutil.BaseTest + sys *testutil.SyscallRecorder +} + +var _ = Suite(&secureBindMountSuite{}) + +func (s *secureBindMountSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.sys = &testutil.SyscallRecorder{} + s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) +} + +func (s *secureBindMountSuite) TearDownTest(c *C) { + s.sys.CheckForStrayDescriptors(c) + s.BaseTest.TearDownTest(c) +} + +func (s *secureBindMountSuite) TestMount(c *C) { + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/source" + {C: `close 3`}, // "/" + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/target" + {C: `close 3`}, // "/" + {C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`}, + {C: `close 6`}, // "/target/dir" + {C: `close 5`}, // "/source/dir" + }) +} + +func (s *secureBindMountSuite) TestMountRecursive(c *C) { + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_REC) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/source" + {C: `close 3`}, // "/" + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/target" + {C: `close 3`}, // "/" + {C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND|MS_REC ""`}, + {C: `close 6`}, // "/target/dir" + {C: `close 5`}, // "/source/dir" + }) +} + +func (s *secureBindMountSuite) TestMountReadOnly(c *C) { + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) + err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/source" + {C: `close 3`}, // "/" + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/target" + {C: `close 3`}, // "/" + {C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, + {C: `fstat 7 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/target" + {C: `close 3`}, // "/" + {C: `mount "none" "/proc/self/fd/7" "" MS_REMOUNT|MS_BIND|MS_RDONLY ""`}, + {C: `close 7`}, // "/target/dir" + {C: `close 6`}, // "/target/dir" + {C: `close 5`}, // "/source/dir" + }) +} + +func (s *secureBindMountSuite) TestBindFlagRequired(c *C) { + err := update.BindMount("/source/dir", "/target/dir", syscall.MS_REC) + c.Assert(err, ErrorMatches, "cannot perform non-bind mount operation") + c.Check(s.sys.RCalls(), HasLen, 0) +} + +func (s *secureBindMountSuite) TestMountReadOnlyRecursive(c *C) { + err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY|syscall.MS_REC) + c.Assert(err, ErrorMatches, "cannot use MS_RDONLY and MS_REC together") + c.Check(s.sys.RCalls(), HasLen, 0) +} + +func (s *secureBindMountSuite) TestBindMountFails(c *C) { + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFault(`mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`, errTesting) + err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY) + c.Assert(err, ErrorMatches, "testing") + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/source" + {C: `close 3`}, // "/" + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/target" + {C: `close 3`}, // "/" + {C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`, E: errTesting}, + {C: `close 6`}, // "/target/dir" + {C: `close 5`}, // "/source/dir" + }) +} + +func (s *secureBindMountSuite) TestRemountReadOnlyFails(c *C) { + s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{}) + s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{}) + s.sys.InsertFault(`mount "none" "/proc/self/fd/7" "" MS_REMOUNT|MS_BIND|MS_RDONLY ""`, errTesting) + err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY) + c.Assert(err, ErrorMatches, "testing") + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/source" + {C: `close 3`}, // "/" + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, + {C: `fstat 6 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/target" + {C: `close 3`}, // "/" + {C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`}, + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, + {C: `fstat 7 `, R: syscall.Stat_t{}}, + {C: `close 4`}, // "/target" + {C: `close 3`}, // "/" + {C: `mount "none" "/proc/self/fd/7" "" MS_REMOUNT|MS_BIND|MS_RDONLY ""`, E: errTesting}, + {C: `unmount "/proc/self/fd/7" UMOUNT_NOFOLLOW|MNT_DETACH`}, + {C: `close 7`}, // "/target/dir" + {C: `close 6`}, // "/target/dir" + {C: `close 5`}, // "/source/dir" + }) +} diff --git a/cmd/snap-update-ns/sorting.go b/cmd/snap-update-ns/sorting.go new file mode 100644 index 00000000..b30997c8 --- /dev/null +++ b/cmd/snap-update-ns/sorting.go @@ -0,0 +1,57 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "strings" + + "github.com/snapcore/snapd/osutil" +) + +// byOriginAndMagicDir allows sorting an array of entries by the source of mount +// entry (overname, layout, content) and lexically by mount point name. +// Automagically adds a trailing slash to paths. +type byOriginAndMagicDir []osutil.MountEntry + +func (c byOriginAndMagicDir) Len() int { return len(c) } +func (c byOriginAndMagicDir) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c byOriginAndMagicDir) Less(i, j int) bool { + iMe := c[i] + jMe := c[j] + + iOrigin := iMe.XSnapdOrigin() + jOrigin := jMe.XSnapdOrigin() + if iOrigin == "overname" && iOrigin != jOrigin { + // should ith element be created by 'overname' mapping, it is + // always sorted before jth element, if that one comes from + // layouts or content interface + return true + } + + iDir := c[i].Dir + jDir := c[j].Dir + if !strings.HasSuffix(iDir, "/") { + iDir = iDir + "/" + } + if !strings.HasSuffix(jDir, "/") { + jDir = jDir + "/" + } + return iDir < jDir +} diff --git a/cmd/snap-update-ns/sorting_test.go b/cmd/snap-update-ns/sorting_test.go new file mode 100644 index 00000000..ddbccd17 --- /dev/null +++ b/cmd/snap-update-ns/sorting_test.go @@ -0,0 +1,72 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "sort" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/osutil" +) + +type sortSuite struct{} + +var _ = Suite(&sortSuite{}) + +func (s *sortSuite) TestTrailingSlashesComparison(c *C) { + // Naively sorted entries. + entries := []osutil.MountEntry{ + {Dir: "/a/b"}, + {Dir: "/a/b-1"}, + {Dir: "/a/b-1/3"}, + {Dir: "/a/b/c"}, + } + sort.Sort(byOriginAndMagicDir(entries)) + // Entries sorted as if they had a trailing slash. + c.Assert(entries, DeepEquals, []osutil.MountEntry{ + {Dir: "/a/b-1"}, + {Dir: "/a/b-1/3"}, + {Dir: "/a/b"}, + {Dir: "/a/b/c"}, + }) +} + +func (s *sortSuite) TestParallelInstancesAndSimple(c *C) { + // Naively sorted entries. + entries := []osutil.MountEntry{ + {Dir: "/a/b"}, + {Dir: "/a/b-1"}, + {Dir: "/snap/bar", Options: []string{osutil.XSnapdOriginOvername()}}, + {Dir: "/a/b-1/3"}, + {Dir: "/foo/bar", Options: []string{osutil.XSnapdOriginOvername()}}, + {Dir: "/a/b/c"}, + } + sort.Sort(byOriginAndMagicDir(entries)) + // Entries sorted as if they had a trailing slash. + c.Assert(entries, DeepEquals, []osutil.MountEntry{ + {Dir: "/foo/bar", Options: []string{osutil.XSnapdOriginOvername()}}, + {Dir: "/snap/bar", Options: []string{osutil.XSnapdOriginOvername()}}, + {Dir: "/a/b-1"}, + {Dir: "/a/b-1/3"}, + {Dir: "/a/b"}, + {Dir: "/a/b/c"}, + }) +} diff --git a/cmd/snap-update-ns/trespassing.go b/cmd/snap-update-ns/trespassing.go new file mode 100644 index 00000000..7dbae492 --- /dev/null +++ b/cmd/snap-update-ns/trespassing.go @@ -0,0 +1,255 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017-2018 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" + "path/filepath" + "strings" + "syscall" + + "github.com/snapcore/snapd/logger" +) + +// Assumptions track the assumptions about the state of the filesystem. +// +// Assumptions constitute the global part of the write restriction management. +// Assumptions are global in the sense that they span multiple distinct write +// operations. In contrast, Restrictions track per-operation state. +type Assumptions struct { + unrestrictedPaths []string + pastChanges []*Change + + // verifiedDevices represents the set of devices that are verified as a tmpfs + // that was mounted by snapd. Those are only discovered on-demand. The + // major:minor number is packed into one uint64 as in syscall.Stat_t.Dev + // field. + verifiedDevices map[uint64]bool +} + +// AddUnrestrictedPaths adds a list of directories where writing is allowed +// even if it would hit the real host filesystem (or transit through the host +// filesystem). This is intended to be used with certain well-known locations +// such as /tmp, $SNAP_DATA and $SNAP. +func (as *Assumptions) AddUnrestrictedPaths(paths ...string) { + as.unrestrictedPaths = append(as.unrestrictedPaths, paths...) +} + +// isRestricted checks whether a path falls under restricted writing scheme. +// +// Provided path is the full, absolute path of the entity that needs to be +// created (directory, file or symbolic link). +func (as *Assumptions) isRestricted(path string) bool { + // Anything rooted at one of the unrestricted paths is not restricted. + // Those are for things like /var/snap/, for example. + for _, p := range as.unrestrictedPaths { + if p == "/" || p == path || strings.HasPrefix(path, filepath.Clean(p)+"/") { + return false + } + + } + // All other paths are restricted + return true +} + +// MockUnrestrictedPaths replaces the set of path paths without any restrictions. +func (as *Assumptions) MockUnrestrictedPaths(paths ...string) (restore func()) { + old := as.unrestrictedPaths + as.unrestrictedPaths = paths + return func() { + as.unrestrictedPaths = old + } +} + +// AddChange records the fact that a change was applied to the system. +func (as *Assumptions) AddChange(change *Change) { + as.pastChanges = append(as.pastChanges, change) +} + +// canWriteToDirectory returns true if writing to a given directory is allowed. +// +// Writing is allowed in one of thee cases: +// 1) The directory is in one of the explicitly permitted locations. +// This is the strongest permission as it explicitly allows writing to +// places that may show up on the host, one of the examples being $SNAP_DATA. +// 2) The directory is on a read-only filesystem. +// 3) The directory is on a tmpfs created by snapd. +func (as *Assumptions) canWriteToDirectory(dirFd int, dirName string) (bool, error) { + if !as.isRestricted(dirName) { + return true, nil + } + var fsData syscall.Statfs_t + if err := sysFstatfs(dirFd, &fsData); err != nil { + return false, fmt.Errorf("cannot fstatfs %q: %s", dirName, err) + } + var fileData syscall.Stat_t + if err := sysFstat(dirFd, &fileData); err != nil { + return false, fmt.Errorf("cannot fstat %q: %s", dirName, err) + } + // Writing to read only directories is allowed because EROFS is handled + // by each of the writing helpers already. + if ok := isReadOnly(dirName, &fsData); ok { + return true, nil + } + // Writing to a trusted tmpfs is allowed because those are not leaking to + // the host. Also, each time we find a good tmpfs we explicitly remember the device major/minor, + if as.verifiedDevices[fileData.Dev] { + return true, nil + } + if ok := isPrivateTmpfsCreatedBySnapd(dirName, &fsData, &fileData, as.pastChanges); ok { + if as.verifiedDevices == nil { + as.verifiedDevices = make(map[uint64]bool) + } + // Don't record 0:0 as those are all to easy to add in tests and would + // skew tests using zero-initialized structures. Real device numbers + // are not zero either so this is not a test-only conditional. + if fileData.Dev != 0 { + as.verifiedDevices[fileData.Dev] = true + } + return true, nil + } + // If writing is not not allowed by one of the three rules above then it is + // disallowed. + return false, nil +} + +// RestrictionsFor computes restrictions for the desired path. +func (as *Assumptions) RestrictionsFor(desiredPath string) *Restrictions { + // Writing to a restricted path results in step-by-step validation of each + // directory, starting from the root of the file system. Unless writing is + // allowed a mimic must be constructed to ensure that writes are not visible in + // undesired locations of the host filesystem. + if as.isRestricted(desiredPath) { + return &Restrictions{assumptions: as, desiredPath: desiredPath, restricted: true} + } + return nil +} + +// Restrictions contains meta-data of a compound write operation. +// +// This structure helps functions that write to the filesystem to keep track of +// the ultimate destination across several calls (e.g. the function that +// creates a file needs to call helpers to create subsequent directories). +// Keeping track of the desired path aids in constructing useful error +// messages. +// +// In addition the structure keeps track of the restricted write mode flag which +// is based on the full path of the desired object being constructed. This allows +// various write helpers to avoid trespassing on host filesystem in places that +// are not expected to be written to by snapd (e.g. outside of $SNAP_DATA). +type Restrictions struct { + assumptions *Assumptions + desiredPath string + restricted bool +} + +// Check verifies whether writing to a directory would trespass on the host. +// +// The check is only performed in restricted mode. If the check fails a +// TrespassingError is returned. +func (rs *Restrictions) Check(dirFd int, dirName string) error { + if rs == nil || !rs.restricted { + return nil + } + // In restricted mode check the directory before attempting to write to it. + ok, err := rs.assumptions.canWriteToDirectory(dirFd, dirName) + if ok || err != nil { + return err + } + if dirName == "/" { + // If writing to / is not allowed then we are in a tough spot because + // we cannot construct a writable mimic over /. This should never + // happen in normal circumstances because the root filesystem is some + // kind of base snap. + return fmt.Errorf("cannot recover from trespassing over /") + } + logger.Debugf("trespassing violated %q while striving to %q", dirName, rs.desiredPath) + logger.Debugf("restricted mode: %#v", rs.restricted) + logger.Debugf("unrestricted paths: %q", rs.assumptions.unrestrictedPaths) + logger.Debugf("verified devices: %v", rs.assumptions.verifiedDevices) + logger.Debugf("past changes: %v", rs.assumptions.pastChanges) + return &TrespassingError{ViolatedPath: filepath.Clean(dirName), DesiredPath: rs.desiredPath} +} + +// Lift lifts write restrictions for the desired path. +// +// This function should be called when, as subsequent components of a path are +// either discovered or created, the conditions for using restricted mode are +// no longer true. +func (rs *Restrictions) Lift() { + if rs != nil { + rs.restricted = false + } +} + +// TrespassingError is an error when filesystem operation would affect the host. +type TrespassingError struct { + ViolatedPath string + DesiredPath string +} + +// Error returns a formatted error message. +func (e *TrespassingError) Error() string { + return fmt.Sprintf("cannot write to %q because it would affect the host in %q", e.DesiredPath, e.ViolatedPath) +} + +// isReadOnly checks whether the underlying filesystem is read only or is mounted as such. +func isReadOnly(dirName string, fsData *syscall.Statfs_t) bool { + // If something is mounted with f_flags & ST_RDONLY then is read-only. + if fsData.Flags&StReadOnly == StReadOnly { + return true + } + // If something is a known read-only file-system then it is safe. + // Older copies of snapd were not mounting squashfs as read only. + if fsData.Type == SquashfsMagic { + return true + } + return false +} + +// isPrivateTmpfsCreatedBySnapd checks whether a directory resides on a tmpfs mounted by snapd +// +// The function inspects the directory and a list of changes that were applied +// to the mount namespace. A directory is trusted if it is a tmpfs that was +// mounted by snap-confine or snapd-update-ns. Note that sub-directories of a +// trusted tmpfs are not considered trusted by this function. +func isPrivateTmpfsCreatedBySnapd(dirName string, fsData *syscall.Statfs_t, fileData *syscall.Stat_t, changes []*Change) bool { + // If something is not a tmpfs it cannot be the trusted tmpfs we are looking for. + if fsData.Type != TmpfsMagic { + return false + } + // Any of the past changes that mounted a tmpfs exactly at the directory we + // are inspecting is considered as trusted. This is conservative because it + // doesn't trust sub-directories of a trusted tmpfs. This approach is + // sufficient for the intended use. + // + // The algorithm goes over all the changes in reverse and picks up the + // first tmpfs mount or unmount action that matches the directory name. + // The set of constraints in snap-update-ns and snapd prevent from mounting + // over an existing mount point so we don't need to consider e.g. a bind + // mount shadowing an active tmpfs. + for i := len(changes) - 1; i >= 0; i-- { + change := changes[i] + if change.Entry.Type == "tmpfs" && change.Entry.Dir == dirName { + return change.Action == Mount + } + } + return false +} diff --git a/cmd/snap-update-ns/trespassing_test.go b/cmd/snap-update-ns/trespassing_test.go new file mode 100644 index 00000000..c2f50638 --- /dev/null +++ b/cmd/snap-update-ns/trespassing_test.go @@ -0,0 +1,428 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017-2018 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 ( + "syscall" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/testutil" +) + +type trespassingSuite struct { + testutil.BaseTest + sys *testutil.SyscallRecorder +} + +var _ = Suite(&trespassingSuite{}) + +func (s *trespassingSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.sys = &testutil.SyscallRecorder{} + s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) +} + +func (s *trespassingSuite) TearDownTest(c *C) { + s.BaseTest.TearDownTest(c) + s.sys.CheckForStrayDescriptors(c) +} + +// AddUnrestrictedPaths and IsRestricted + +func (s *trespassingSuite) TestAddUnrestrictedPaths(c *C) { + a := &update.Assumptions{} + c.Assert(a.IsRestricted("/etc/test.conf"), Equals, true) + + a.AddUnrestrictedPaths("/etc") + c.Assert(a.IsRestricted("/etc/test.conf"), Equals, false) + c.Assert(a.IsRestricted("/etc/"), Equals, false) + c.Assert(a.IsRestricted("/etc"), Equals, false) + c.Assert(a.IsRestricted("/etc2"), Equals, true) + + a.AddUnrestrictedPaths("/") + c.Assert(a.IsRestricted("/foo"), Equals, false) + +} + +func (s *trespassingSuite) TestMockUnrestrictedPaths(c *C) { + a := &update.Assumptions{} + c.Assert(a.IsRestricted("/etc/test.conf"), Equals, true) + restore := a.MockUnrestrictedPaths("/etc/") + c.Assert(a.IsRestricted("/etc/test.conf"), Equals, false) + restore() + c.Assert(a.IsRestricted("/etc/test.conf"), Equals, true) +} + +// canWriteToDirectory and AddChange + +// We are not allowed to write to ext4. +func (s *trespassingSuite) TestCanWriteToDirectoryWritableExt4(c *C) { + a := &update.Assumptions{} + + path := "/etc" + fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + + ok, err := a.CanWriteToDirectory(fd, path) + c.Assert(err, IsNil) + c.Assert(ok, Equals, false) +} + +// We are allowed to write to ext4 that was mounted read-only. +func (s *trespassingSuite) TestCanWriteToDirectoryReadOnlyExt4(c *C) { + a := &update.Assumptions{} + + path := "/etc" + fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic, Flags: update.StReadOnly}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + + ok, err := a.CanWriteToDirectory(fd, path) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) +} + +// We are not allowed to write to tmpfs. +func (s *trespassingSuite) TestCanWriteToDirectoryTmpfs(c *C) { + a := &update.Assumptions{} + + path := "/etc" + fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + + ok, err := a.CanWriteToDirectory(fd, path) + c.Assert(err, IsNil) + c.Assert(ok, Equals, false) +} + +// We are allowed to write to tmpfs that was mounted by snapd. +func (s *trespassingSuite) TestCanWriteToDirectoryTmpfsMountedBySnapd(c *C) { + a := &update.Assumptions{} + + path := "/etc" + fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + + a.AddChange(&update.Change{ + Action: update.Mount, + Entry: osutil.MountEntry{Type: "tmpfs", Dir: path}}) + + ok, err := a.CanWriteToDirectory(fd, path) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) +} + +// We are allowed to write to directory beneath a tmpfs that was mounted by snapd. +func (s *trespassingSuite) TestCanWriteToDirectoryUnderTmpfsMountedBySnapd(c *C) { + a := &update.Assumptions{} + + fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{Dev: 0x42}) + + a.AddChange(&update.Change{ + Action: update.Mount, + Entry: osutil.MountEntry{Type: "tmpfs", Dir: "/etc"}}) + + ok, err := a.CanWriteToDirectory(fd, "/etc") + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + // Now we have primed the assumption state with knowledge of 0x42 device as + // a verified tmpfs. We can now exploit it by trying to write to + // /etc/conf.d and seeing that is allowed even though /etc/conf.d itself is + // not a mount point representing tmpfs. + + fd2, err := s.sys.Open("/etc/conf.d", syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd2) + + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Dev: 0x42}) + + ok, err = a.CanWriteToDirectory(fd2, "/etc/conf.d") + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) +} + +// We are allowed to write to directory which is a bind mount of something, beneath a tmpfs that was mounted by snapd. +func (s *trespassingSuite) TestCanWriteToDirectoryUnderReboundTmpfsMountedBySnapd(c *C) { + a := &update.Assumptions{} + + fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + c.Assert(fd, Equals, 3) + defer s.sys.Close(fd) + + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{Dev: 0x42}) + + a.AddChange(&update.Change{ + Action: update.Mount, + Entry: osutil.MountEntry{Type: "tmpfs", Dir: "/etc"}}) + + ok, err := a.CanWriteToDirectory(fd, "/etc") + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + // Now we have primed the assumption state with knowledge of 0x42 device as + // a verified tmpfs. Unlike in the test above though the directory + // /etc/conf.d is a bind mount from another tmpfs that we know nothing + // about. + fd2, err := s.sys.Open("/etc/conf.d", syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + c.Assert(fd2, Equals, 4) + defer s.sys.Close(fd2) + + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Dev: 0xdeadbeef}) + + ok, err = a.CanWriteToDirectory(fd2, "/etc/conf.d") + c.Assert(err, IsNil) + c.Assert(ok, Equals, false) +} + +// We are allowed to write to an unrestricted path. +func (s *trespassingSuite) TestCanWriteToDirectoryUnrestricted(c *C) { + a := &update.Assumptions{} + + path := "/var/snap/foo/common" + fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + + a.AddUnrestrictedPaths(path) + + ok, err := a.CanWriteToDirectory(fd, path) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) +} + +// Errors from fstatfs are propagated to the caller. +func (s *trespassingSuite) TestCanWriteToDirectoryErrorsFstatfs(c *C) { + a := &update.Assumptions{} + + path := "/etc" + fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + + s.sys.InsertFault(`fstatfs 3 `, errTesting) + + ok, err := a.CanWriteToDirectory(fd, path) + c.Assert(err, ErrorMatches, `cannot fstatfs "/etc": testing`) + c.Assert(ok, Equals, false) +} + +// Errors from fstat are propagated to the caller. +func (s *trespassingSuite) TestCanWriteToDirectoryErrorsFstat(c *C) { + a := &update.Assumptions{} + + path := "/etc" + fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{}) + s.sys.InsertFault(`fstat 3 `, errTesting) + + ok, err := a.CanWriteToDirectory(fd, path) + c.Assert(err, ErrorMatches, `cannot fstat "/etc": testing`) + c.Assert(ok, Equals, false) +} + +// RestrictionsFor, Check and LiftRestrictions + +func (s *trespassingSuite) TestRestrictionsForEtc(c *C) { + a := &update.Assumptions{} + + // There are restrictions for writing in /etc. + rs := a.RestrictionsFor("/etc/test.conf") + c.Assert(rs, NotNil) + + fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + + // Check reports trespassing error, restrictions may be lifted though. + err = rs.Check(fd, "/etc") + c.Assert(err, ErrorMatches, `cannot write to "/etc/test.conf" because it would affect the host in "/etc"`) + c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc") + c.Assert(err.(*update.TrespassingError).DesiredPath, Equals, "/etc/test.conf") + + rs.Lift() + c.Assert(rs.Check(fd, "/etc"), IsNil) +} + +// Check returns errors from lower layers. +func (s *trespassingSuite) TestRestrictionsForErrors(c *C) { + a := &update.Assumptions{} + + rs := a.RestrictionsFor("/etc/test.conf") + c.Assert(rs, NotNil) + + fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + s.sys.InsertFault(`fstatfs 3 `, errTesting) + + err = rs.Check(fd, "/etc") + c.Assert(err, ErrorMatches, `cannot fstatfs "/etc": testing`) +} + +func (s *trespassingSuite) TestRestrictionsForVarSnap(c *C) { + a := &update.Assumptions{} + a.AddUnrestrictedPaths("/var/snap") + + // There are no restrictions in $SNAP_COMMON. + rs := a.RestrictionsFor("/var/snap/foo/common/test.conf") + c.Assert(rs, IsNil) + + // Nil restrictions have working Check and Lift methods. + c.Assert(rs.Check(3, "unused"), IsNil) + rs.Lift() +} + +func (s *trespassingSuite) TestRestrictionsForRootfsEntries(c *C) { + a := &update.Assumptions{} + + // The root directory is special, it's not a trespassing error we can + // recover from because we cannot construct a writable mimic for the root + // directory today. + rs := a.RestrictionsFor("/foo.conf") + + fd, err := s.sys.Open("/", syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + + // Nil restrictions have working Check and Lift methods. + c.Assert(rs.Check(fd, "/"), ErrorMatches, `cannot recover from trespassing over /`) +} + +// isReadOnly + +func (s *trespassingSuite) TestIsReadOnlySquashfsMountedRo(c *C) { + path := "/some/path" + statfs := &syscall.Statfs_t{Type: update.SquashfsMagic, Flags: update.StReadOnly} + result := update.IsReadOnly(path, statfs) + c.Assert(result, Equals, true) +} + +func (s *trespassingSuite) TestIsReadOnlySquashfsMountedRw(c *C) { + path := "/some/path" + statfs := &syscall.Statfs_t{Type: update.SquashfsMagic} + result := update.IsReadOnly(path, statfs) + c.Assert(result, Equals, true) +} + +func (s *trespassingSuite) TestIsReadOnlyExt4MountedRw(c *C) { + path := "/some/path" + statfs := &syscall.Statfs_t{Type: update.Ext4Magic} + result := update.IsReadOnly(path, statfs) + c.Assert(result, Equals, false) +} + +// isSnapdCreatedPrivateTmpfs + +func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdNotATmpfs(c *C) { + path := "/some/path" + // An ext4 (which is not a tmpfs) is not a private tmpfs. + statfs := &syscall.Statfs_t{Type: update.Ext4Magic} + stat := &syscall.Stat_t{} + result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, nil) + c.Assert(result, Equals, false) +} + +func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdNotTrusted(c *C) { + path := "/some/path" + // A tmpfs is not private if it doesn't come from a change we made. + statfs := &syscall.Statfs_t{Type: update.TmpfsMagic} + stat := &syscall.Stat_t{} + result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, nil) + c.Assert(result, Equals, false) +} + +func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdViaChanges(c *C) { + path := "/some/path" + // A tmpfs is private because it was mounted by snap-update-ns. + statfs := &syscall.Statfs_t{Type: update.TmpfsMagic} + stat := &syscall.Stat_t{} + + // A tmpfs was mounted in the past so it is private. + result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{ + {Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, + }) + c.Assert(result, Equals, true) + + // A tmpfs was mounted but then it was unmounted so it is not private anymore. + result = update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{ + {Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, + {Action: update.Unmount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, + }) + c.Assert(result, Equals, false) + + // Finally, after the mounting and unmounting the tmpfs was mounted again. + result = update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{ + {Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, + {Action: update.Unmount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, + {Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}}, + }) + c.Assert(result, Equals, true) +} + +func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdDeeper(c *C) { + path := "/some/path/below" + // A tmpfs is not private beyond the exact mount point from a change. + // That is, sub-directories of a private tmpfs are not recognized as private. + statfs := &syscall.Statfs_t{Type: update.TmpfsMagic} + stat := &syscall.Stat_t{} + result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{ + {Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/some/path", Type: "tmpfs"}}, + }) + c.Assert(result, Equals, false) +} diff --git a/cmd/snap-update-ns/utils.go b/cmd/snap-update-ns/utils.go new file mode 100644 index 00000000..5d47aa98 --- /dev/null +++ b/cmd/snap-update-ns/utils.go @@ -0,0 +1,655 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/osutil/sys" + "github.com/snapcore/snapd/strutil" +) + +// not available through syscall +const ( + umountNoFollow = 8 + // StReadOnly is the equivalent of ST_RDONLY + StReadOnly = 1 + // SquashfsMagic is the equivalent of SQUASHFS_MAGIC + SquashfsMagic = 0x73717368 + // Ext4Magic is the equivalent of EXT4_SUPER_MAGIC + Ext4Magic = 0xef53 + // TmpfsMagic is the equivalent of TMPFS_MAGIC + TmpfsMagic = 0x01021994 +) + +// For mocking everything during testing. +var ( + osLstat = os.Lstat + osReadlink = os.Readlink + osRemove = os.Remove + + sysClose = syscall.Close + sysMkdirat = syscall.Mkdirat + sysMount = syscall.Mount + sysOpen = syscall.Open + sysOpenat = syscall.Openat + sysUnmount = syscall.Unmount + sysFchown = sys.Fchown + sysFstat = syscall.Fstat + sysFstatfs = syscall.Fstatfs + sysSymlinkat = osutil.Symlinkat + sysReadlinkat = osutil.Readlinkat + sysFchdir = syscall.Fchdir + sysLstat = syscall.Lstat + + ioutilReadDir = ioutil.ReadDir +) + +// ReadOnlyFsError is an error encapsulating encountered EROFS. +type ReadOnlyFsError struct { + Path string +} + +func (e *ReadOnlyFsError) Error() string { + return fmt.Sprintf("cannot operate on read-only filesystem at %s", e.Path) +} + +// OpenPath creates a path file descriptor for the given +// path, making sure no components are symbolic links. +// +// The file descriptor is opened using the O_PATH, O_NOFOLLOW, +// and O_CLOEXEC flags. +func OpenPath(path string) (int, error) { + iter, err := strutil.NewPathIterator(path) + if err != nil { + return -1, fmt.Errorf("cannot open path: %s", err) + } + if !filepath.IsAbs(iter.Path()) { + return -1, fmt.Errorf("path %v is not absolute", iter.Path()) + } + iter.Next() // Advance iterator to '/' + // We use the following flags to open: + // O_PATH: we don't intend to use the fd for IO + // O_NOFOLLOW: don't follow symlinks + // O_DIRECTORY: we expect to find directories (except for the leaf) + // O_CLOEXEC: don't leak file descriptors over exec() boundaries + openFlags := sys.O_PATH | syscall.O_NOFOLLOW | syscall.O_DIRECTORY | syscall.O_CLOEXEC + fd, err := sysOpen("/", openFlags, 0) + if err != nil { + return -1, err + } + for iter.Next() { + // Ensure the parent file descriptor is closed + defer sysClose(fd) + if !strings.HasSuffix(iter.CurrentName(), "/") { + openFlags &^= syscall.O_DIRECTORY + } + fd, err = sysOpenat(fd, iter.CurrentCleanName(), openFlags, 0) + if err != nil { + return -1, err + } + } + + var statBuf syscall.Stat_t + err = sysFstat(fd, &statBuf) + if err != nil { + sysClose(fd) + return -1, err + } + if statBuf.Mode&syscall.S_IFMT == syscall.S_IFLNK { + sysClose(fd) + return -1, fmt.Errorf("%q is a symbolic link", path) + } + return fd, nil +} + +// MkPrefix creates all the missing directories in a given base path and +// returns the file descriptor to the leaf directory as well as the restricted +// flag. This function is a base for secure variants of mkdir, touch and +// symlink. None of the traversed directories can be symbolic links. +func MkPrefix(base string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, rs *Restrictions) (int, error) { + iter, err := strutil.NewPathIterator(base) + if err != nil { + // TODO: Reword the error and adjust the tests. + return -1, fmt.Errorf("cannot split unclean path %q", base) + } + if !filepath.IsAbs(iter.Path()) { + return -1, fmt.Errorf("path %v is not absolute", iter.Path()) + } + iter.Next() // Advance iterator to '/' + + const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_DIRECTORY + // Open the root directory and start there. + // + // We don't have to check for possible trespassing on / here because we are + // going to check for it in sec.MkDir call below which verifies that + // trespassing restrictions are not violated. + fd, err := sysOpen("/", openFlags, 0) + if err != nil { + return -1, fmt.Errorf("cannot open root directory: %v", err) + } + for iter.Next() { + // Keep closing the previous descriptor as we go, so that we have the + // last one handy from the MkDir below. + defer sysClose(fd) + fd, err = MkDir(fd, iter.CurrentBase(), iter.CurrentCleanName(), perm, uid, gid, rs) + if err != nil { + return -1, err + } + } + + return fd, nil +} + +// MkDir creates a directory with a given name. +// +// The directory is represented with a file descriptor and its name (for +// convenience). This function is meant to be used to construct subsequent +// elements of some path. The return value contains the newly created file +// descriptor for the new directory or -1 on error. +func MkDir(dirFd int, dirName string, name string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, rs *Restrictions) (int, error) { + if err := rs.Check(dirFd, dirName); err != nil { + return -1, err + } + + made := true + const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_DIRECTORY + + if err := sysMkdirat(dirFd, name, uint32(perm.Perm())); err != nil { + switch err { + case syscall.EEXIST: + made = false + case syscall.EROFS: + // Treat EROFS specially: this is a hint that we have to poke a + // hole using tmpfs. The path below is the location where we + // need to poke the hole. + return -1, &ReadOnlyFsError{Path: dirName} + default: + return -1, fmt.Errorf("cannot create directory %q: %v", filepath.Join(dirName, name), err) + } + } + newFd, err := sysOpenat(dirFd, name, openFlags, 0) + if err != nil { + return -1, fmt.Errorf("cannot open directory %q: %v", filepath.Join(dirName, name), err) + } + if made { + // Chown each segment that we made. + if err := sysFchown(newFd, uid, gid); err != nil { + // Close the FD we opened if we fail here since the caller will get + // an error and won't assume responsibility for the FD. + sysClose(newFd) + return -1, fmt.Errorf("cannot chown directory %q to %d.%d: %v", filepath.Join(dirName, name), uid, gid, err) + } + // As soon as we find a place that is safe to write we can switch off + // the restricted mode (and thus any subsequent checks). This is + // because we only allow "writing" to read-only filesystems where + // writes fail with EROFS or to a tmpfs that snapd has privately + // mounted inside the per-snap mount namespace. As soon as we start + // walking over such tmpfs any subsequent children are either read- + // only bind mounts from $SNAP, other tmpfs'es (e.g. one explicitly + // constructed for a layout) or writable places that are bind-mounted + // from $SNAP_DATA or similar. + rs.Lift() + } + return newFd, err +} + +// MkFile creates a file with a given name. +// +// The directory is represented with a file descriptor and its name (for +// convenience). This function is meant to be used to create the leaf file as +// a preparation for a mount point. Existing files are reused without errors. +// Newly created files have the specified mode and ownership. +func MkFile(dirFd int, dirName string, name string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, rs *Restrictions) error { + if err := rs.Check(dirFd, dirName); err != nil { + return err + } + + made := true + // NOTE: Tests don't show O_RDONLY as has a value of 0 and is not + // translated to textual form. It is added here for explicitness. + const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_RDONLY + + // Open the final path segment as a file. Try to create the file (so that + // we know if we need to chown it) but fall back to just opening an + // existing one. + + newFd, err := sysOpenat(dirFd, name, openFlags|syscall.O_CREAT|syscall.O_EXCL, uint32(perm.Perm())) + if err != nil { + switch err { + case syscall.EEXIST: + // If the file exists then just open it without O_CREAT and O_EXCL + newFd, err = sysOpenat(dirFd, name, openFlags, 0) + if err != nil { + return fmt.Errorf("cannot open file %q: %v", filepath.Join(dirName, name), err) + } + made = false + case syscall.EROFS: + // Treat EROFS specially: this is a hint that we have to poke a + // hole using tmpfs. The path below is the location where we + // need to poke the hole. + return &ReadOnlyFsError{Path: dirName} + default: + return fmt.Errorf("cannot open file %q: %v", filepath.Join(dirName, name), err) + } + } + defer sysClose(newFd) + + if made { + // Chown the file if we made it. + if err := sysFchown(newFd, uid, gid); err != nil { + return fmt.Errorf("cannot chown file %q to %d.%d: %v", filepath.Join(dirName, name), uid, gid, err) + } + } + + return nil +} + +// MkSymlink creates a symlink with a given name. +// +// The directory is represented with a file descriptor and its name (for +// convenience). This function is meant to be used to create the leaf symlink. +// Existing and identical symlinks are reused without errors. +func MkSymlink(dirFd int, dirName string, name string, oldname string, rs *Restrictions) error { + if err := rs.Check(dirFd, dirName); err != nil { + return err + } + + // Create the final path segment as a symlink. + if err := sysSymlinkat(oldname, dirFd, name); err != nil { + switch err { + case syscall.EEXIST: + var objFd int + // If the file exists then just open it for examination. + // Maybe it's the symlink we were hoping to create. + objFd, err = sysOpenat(dirFd, name, syscall.O_CLOEXEC|sys.O_PATH|syscall.O_NOFOLLOW, 0) + if err != nil { + return fmt.Errorf("cannot open existing file %q: %v", filepath.Join(dirName, name), err) + } + defer sysClose(objFd) + var statBuf syscall.Stat_t + err = sysFstat(objFd, &statBuf) + if err != nil { + return fmt.Errorf("cannot inspect existing file %q: %v", filepath.Join(dirName, name), err) + } + if statBuf.Mode&syscall.S_IFMT != syscall.S_IFLNK { + return fmt.Errorf("cannot create symbolic link %q: existing file in the way", filepath.Join(dirName, name)) + } + var n int + buf := make([]byte, len(oldname)+2) + n, err = sysReadlinkat(objFd, "", buf) + if err != nil { + return fmt.Errorf("cannot read symbolic link %q: %v", filepath.Join(dirName, name), err) + } + if string(buf[:n]) != oldname { + return fmt.Errorf("cannot create symbolic link %q: existing symbolic link in the way", filepath.Join(dirName, name)) + } + return nil + case syscall.EROFS: + // Treat EROFS specially: this is a hint that we have to poke a + // hole using tmpfs. The path below is the location where we + // need to poke the hole. + return &ReadOnlyFsError{Path: dirName} + default: + return fmt.Errorf("cannot create symlink %q: %v", filepath.Join(dirName, name), err) + } + } + + return nil +} + +// MkdirAll is the secure variant of os.MkdirAll. +// +// Unlike the regular version this implementation does not follow any symbolic +// links. At all times the new directory segment is created using mkdirat(2) +// while holding an open file descriptor to the parent directory. +// +// The only handled error is mkdirat(2) that fails with EEXIST. All other +// errors are fatal but there is no attempt to undo anything that was created. +// +// The uid and gid are used for the fchown(2) system call which is performed +// after each segment is created and opened. The special value -1 may be used +// to request that ownership is not changed. +func MkdirAll(path string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, rs *Restrictions) error { + if path != filepath.Clean(path) { + // TODO: Reword the error and adjust the tests. + return fmt.Errorf("cannot split unclean path %q", path) + } + // Only support absolute paths to avoid bugs in snap-confine when + // called from anywhere. + if !filepath.IsAbs(path) { + return fmt.Errorf("cannot create directory with relative path: %q", path) + } + base, name := filepath.Split(path) + base = filepath.Clean(base) // Needed to chomp the trailing slash. + + // Create the prefix. + dirFd, err := MkPrefix(base, perm, uid, gid, rs) + if err != nil { + return err + } + defer sysClose(dirFd) + + if name != "" { + // Create the leaf as a directory. + leafFd, err := MkDir(dirFd, base, name, perm, uid, gid, rs) + if err != nil { + return err + } + defer sysClose(leafFd) + } + + return nil +} + +// MkfileAll is a secure implementation of "mkdir -p $(dirname $1) && touch $1". +// +// This function is like MkdirAll but it creates an empty file instead of +// a directory for the final path component. Each created directory component +// is chowned to the desired user and group. +func MkfileAll(path string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, rs *Restrictions) error { + if path != filepath.Clean(path) { + // TODO: Reword the error and adjust the tests. + return fmt.Errorf("cannot split unclean path %q", path) + } + // Only support absolute paths to avoid bugs in snap-confine when + // called from anywhere. + if !filepath.IsAbs(path) { + return fmt.Errorf("cannot create file with relative path: %q", path) + } + // Only support file names, not directory names. + if strings.HasSuffix(path, "/") { + return fmt.Errorf("cannot create non-file path: %q", path) + } + base, name := filepath.Split(path) + base = filepath.Clean(base) // Needed to chomp the trailing slash. + + // Create the prefix. + dirFd, err := MkPrefix(base, perm, uid, gid, rs) + if err != nil { + return err + } + defer sysClose(dirFd) + + if name != "" { + // Create the leaf as a file. + err = MkFile(dirFd, base, name, perm, uid, gid, rs) + } + return err +} + +// MksymlinkAll is a secure implementation of "ln -s". +func MksymlinkAll(path string, perm os.FileMode, uid sys.UserID, gid sys.GroupID, oldname string, rs *Restrictions) error { + if path != filepath.Clean(path) { + // TODO: Reword the error and adjust the tests. + return fmt.Errorf("cannot split unclean path %q", path) + } + // Only support absolute paths to avoid bugs in snap-confine when + // called from anywhere. + if !filepath.IsAbs(path) { + return fmt.Errorf("cannot create symlink with relative path: %q", path) + } + // Only support file names, not directory names. + if strings.HasSuffix(path, "/") { + return fmt.Errorf("cannot create non-file path: %q", path) + } + if oldname == "" { + return fmt.Errorf("cannot create symlink with empty target: %q", path) + } + + base, name := filepath.Split(path) + base = filepath.Clean(base) // Needed to chomp the trailing slash. + + // Create the prefix. + dirFd, err := MkPrefix(base, perm, uid, gid, rs) + if err != nil { + return err + } + defer sysClose(dirFd) + + if name != "" { + // Create the leaf as a symlink. + err = MkSymlink(dirFd, base, name, oldname, rs) + } + return err +} + +// planWritableMimic plans how to transform a given directory from read-only to writable. +// +// The algorithm is designed to be universally reversible so that it can be +// always de-constructed back to the original directory. The original directory +// is hidden by tmpfs and a subset of things that were present there originally +// is bind mounted back on top of empty directories or empty files. Symlinks +// are re-created directly. Devices and all other elements are not supported +// because they are forbidden in snaps for which this function is designed to +// be used with. Since the original directory is hidden the algorithm relies on +// a temporary directory where the original is bind-mounted during the +// progression of the algorithm. +func planWritableMimic(dir, neededBy string) ([]*Change, error) { + // We need a place for "safe keeping" of what is present in the original + // directory as we are about to attach a tmpfs there, which will hide + // everything inside. + logger.Debugf("create-writable-mimic %q", dir) + safeKeepingDir := filepath.Join("/tmp/.snap/", dir) + + var changes []*Change + + // Stat the original directory to know which mode and ownership to + // replicate on top of the tmpfs we are about to create below. + var sb syscall.Stat_t + if err := sysLstat(dir, &sb); err != nil { + return nil, err + } + + // Bind mount the original directory elsewhere for safe-keeping. + changes = append(changes, &Change{ + Action: Mount, Entry: osutil.MountEntry{ + // NOTE: Here we recursively bind because we realized that not + // doing so doesn't work on core devices which use bind mounts + // extensively to construct writable spaces in /etc and /var and + // elsewhere. + // + // All directories present in the original are also recursively + // bind mounted back to their original location. To unmount this + // contraption we use MNT_DETACH which frees us from having to + // enumerate the mount table, unmount all the things (starting + // with most nested). + // + // The undo logic handles rbind mounts and adds x-snapd.unbind + // flag to them, which in turns translates to MNT_DETACH on + // umount2(2) system call. + Name: dir, Dir: safeKeepingDir, Options: []string{"rbind"}}, + }) + + // Mount tmpfs over the original directory, hiding its contents. + // The mounted tmpfs will mimic the mode and ownership of the original + // directory. + changes = append(changes, &Change{ + Action: Mount, Entry: osutil.MountEntry{ + Name: "tmpfs", Dir: dir, Type: "tmpfs", + Options: []string{ + osutil.XSnapdSynthetic(), + osutil.XSnapdNeededBy(neededBy), + fmt.Sprintf("mode=%#o", sb.Mode&07777), + fmt.Sprintf("uid=%d", sb.Uid), + fmt.Sprintf("gid=%d", sb.Gid), + }, + }, + }) + // Iterate over the items in the original directory (nothing is mounted _yet_). + entries, err := ioutilReadDir(dir) + if err != nil { + return nil, err + } + for _, fi := range entries { + ch := &Change{Action: Mount, Entry: osutil.MountEntry{ + Name: filepath.Join(safeKeepingDir, fi.Name()), + Dir: filepath.Join(dir, fi.Name()), + }} + // Bind mount each element from the safe-keeping directory into the + // tmpfs. Our Change.Perform() engine can create the missing + // directories automatically so we don't bother creating those. + m := fi.Mode() + switch { + case m.IsDir(): + ch.Entry.Options = []string{"rbind"} + case m.IsRegular(): + ch.Entry.Options = []string{"bind", osutil.XSnapdKindFile()} + case m&os.ModeSymlink != 0: + if target, err := osReadlink(filepath.Join(dir, fi.Name())); err == nil { + ch.Entry.Options = []string{osutil.XSnapdKindSymlink(), osutil.XSnapdSymlink(target)} + } else { + continue + } + default: + logger.Noticef("skipping unsupported file %s", fi) + continue + } + ch.Entry.Options = append(ch.Entry.Options, osutil.XSnapdSynthetic()) + ch.Entry.Options = append(ch.Entry.Options, osutil.XSnapdNeededBy(neededBy)) + changes = append(changes, ch) + } + // Finally unbind the safe-keeping directory as we don't need it anymore. + changes = append(changes, &Change{ + Action: Unmount, Entry: osutil.MountEntry{Name: "none", Dir: safeKeepingDir, Options: []string{osutil.XSnapdDetach()}}, + }) + return changes, nil +} + +// FatalError is an error that we cannot correct. +type FatalError struct { + error +} + +// execWritableMimic executes the plan for a writable mimic. +// The result is a transformed mount namespace and a set of fake mount changes +// that only exist in order to undo the plan. +// +// Certain assumptions are made about the plan, it must closely resemble that +// created by planWritableMimic, in particular the sequence must look like this: +// +// - bind a directory aside into safekeeping location +// - cover the original with tmpfs +// - bind mount something from safekeeping location to an empty file or +// directory in the tmpfs; this step can repeat any number of times +// - unbind the safekeeping location +// +// Apart from merely executing the plan a fake plan is returned for undo. The +// undo plan skips the following elements as compared to the original plan: +// +// - the initial bind mount that constructs the safekeeping directory is gone +// - the final unmount that removes the safekeeping directory +// - the source of each of the bind mounts that re-populate tmpfs. +// +// In the event of a failure the undo plan is executed and an error is +// returned. If the undo plan fails the function returns a FatalError as it +// cannot fix the system from an inconsistent state. +func execWritableMimic(plan []*Change, as *Assumptions) ([]*Change, error) { + undoChanges := make([]*Change, 0, len(plan)-2) + for i, change := range plan { + if _, err := changePerform(change, as); err != nil { + // Drat, we failed! Let's undo everything according to our own undo + // plan, by following it in reverse order. + + recoveryUndoChanges := make([]*Change, 0, len(undoChanges)+1) + if i > 0 { + // The undo plan doesn't contain the entry for the initial bind + // mount of the safe keeping directory but we have already + // performed it. For this recovery phase we need to insert that + // in front of the undo plan manually. + recoveryUndoChanges = append(recoveryUndoChanges, plan[0]) + } + recoveryUndoChanges = append(recoveryUndoChanges, undoChanges...) + + for j := len(recoveryUndoChanges) - 1; j >= 0; j-- { + recoveryUndoChange := recoveryUndoChanges[j] + // All the changes mount something, we need to reverse that. + // The "undo plan" is "a plan that can be undone" not "the plan + // for how to undo" so we need to flip the actions. + recoveryUndoChange.Action = Unmount + if recoveryUndoChange.Entry.OptBool("rbind") { + recoveryUndoChange.Entry.Options = append(recoveryUndoChange.Entry.Options, osutil.XSnapdDetach()) + } + if _, err2 := changePerform(recoveryUndoChange, as); err2 != nil { + // Drat, we failed when trying to recover from an error. + // We cannot do anything at this stage. + return nil, &FatalError{error: fmt.Errorf("cannot undo change %q while recovering from earlier error %v: %v", recoveryUndoChange, err, err2)} + } + } + return nil, err + } + if i == 0 || i == len(plan)-1 { + // Don't represent the initial and final changes in the undo plan. + // The initial change is the safe-keeping bind mount, the final + // change is the safe-keeping unmount. + continue + } + if change.Entry.XSnapdKind() == "symlink" { + // Don't represent symlinks in the undo plan. They are removed when + // the tmpfs is unmounted. + continue + + } + // Store an undo change for the change we just performed. + undoOpts := change.Entry.Options + if change.Entry.OptBool("rbind") { + undoOpts = make([]string, 0, len(change.Entry.Options)+1) + undoOpts = append(undoOpts, change.Entry.Options...) + undoOpts = append(undoOpts, "x-snapd.detach") + } + undoChange := &Change{ + Action: Mount, + Entry: osutil.MountEntry{Dir: change.Entry.Dir, Name: change.Entry.Name, Type: change.Entry.Type, Options: undoOpts}, + } + // Because of the use of a temporary bind mount (aka the safe-keeping + // directory) we cannot represent bind mounts fully (the temporary bind + // mount is unmounted as the last stage of this process). For that + // reason let's hide the original location and overwrite it so to + // appear as if the directory was a bind mount over itself. This is not + // fully true (it is a bind mount from the old self to the new empty + // directory or file in the same path, with the tmpfs in place already) + // but this is closer to the truth and more in line with the idea that + // this is just a plan for undoing the operation. + if undoChange.Entry.OptBool("bind") || undoChange.Entry.OptBool("rbind") { + undoChange.Entry.Name = undoChange.Entry.Dir + } + undoChanges = append(undoChanges, undoChange) + } + return undoChanges, nil +} + +func createWritableMimic(dir, neededBy string, as *Assumptions) ([]*Change, error) { + plan, err := planWritableMimic(dir, neededBy) + if err != nil { + return nil, err + } + changes, err := execWritableMimic(plan, as) + if err != nil { + return nil, err + } + return changes, nil +} diff --git a/cmd/snap-update-ns/utils_test.go b/cmd/snap-update-ns/utils_test.go new file mode 100644 index 00000000..03cacd92 --- /dev/null +++ b/cmd/snap-update-ns/utils_test.go @@ -0,0 +1,1144 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "syscall" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/osutil/sys" + "github.com/snapcore/snapd/testutil" +) + +type utilsSuite struct { + testutil.BaseTest + sys *testutil.SyscallRecorder + log *bytes.Buffer + as *update.Assumptions +} + +var _ = Suite(&utilsSuite{}) + +func (s *utilsSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.sys = &testutil.SyscallRecorder{} + s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) + buf, restore := logger.MockLogger() + s.BaseTest.AddCleanup(restore) + s.log = buf + s.as = &update.Assumptions{} +} + +func (s *utilsSuite) TearDownTest(c *C) { + s.BaseTest.TearDownTest(c) + s.sys.CheckForStrayDescriptors(c) +} + +// secure-mkdir-all + +// Ensure that we reject unclean paths. +func (s *utilsSuite) TestSecureMkdirAllUnclean(c *C) { + err := update.MkdirAll("/unclean//path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot split unclean path .*`) + c.Assert(s.sys.RCalls(), HasLen, 0) +} + +// Ensure that we refuse to create a directory with an relative path. +func (s *utilsSuite) TestSecureMkdirAllRelative(c *C) { + err := update.MkdirAll("rel/path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot create directory with relative path: "rel/path"`) + c.Assert(s.sys.RCalls(), HasLen, 0) +} + +// Ensure that we can "create the root directory. +func (s *utilsSuite) TestSecureMkdirAllLevel0(c *C) { + c.Assert(update.MkdirAll("/", 0755, 123, 456, nil), IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `close 3`}, + }) +} + +// Ensure that we can create a directory in the top-level directory. +func (s *utilsSuite) TestSecureMkdirAllLevel1(c *C) { + c.Assert(update.MkdirAll("/path", 0755, 123, 456, nil), IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "path" 0755`}, + {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 123 456`}, + {C: `close 4`}, + {C: `close 3`}, + }) +} + +// Ensure that we can create a directory two levels from the top-level directory. +func (s *utilsSuite) TestSecureMkdirAllLevel2(c *C) { + c.Assert(update.MkdirAll("/path/to", 0755, 123, 456, nil), IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "path" 0755`}, + {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 123 456`}, + {C: `close 3`}, + {C: `mkdirat 4 "to" 0755`}, + {C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 123 456`}, + {C: `close 3`}, + {C: `close 4`}, + }) +} + +// Ensure that we can create a directory three levels from the top-level directory. +func (s *utilsSuite) TestSecureMkdirAllLevel3(c *C) { + c.Assert(update.MkdirAll("/path/to/something", 0755, 123, 456, nil), IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "path" 0755`}, + {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 123 456`}, + {C: `mkdirat 4 "to" 0755`}, + {C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `fchown 5 123 456`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `mkdirat 5 "something" 0755`}, + {C: `openat 5 "something" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 123 456`}, + {C: `close 3`}, + {C: `close 5`}, + }) +} + +// Ensure that writes to /etc/demo are interrupted if /etc is restricted. +func (s *utilsSuite) TestSecureMkdirAllWithRestrictedEtc(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) + rs := s.as.RestrictionsFor("/etc/demo") + err := update.MkdirAll("/etc/demo", 0755, 123, 456, rs) + c.Assert(err, ErrorMatches, `cannot write to "/etc/demo" because it would affect the host in "/etc"`) + c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc") + c.Assert(err.(*update.TrespassingError).DesiredPath, Equals, "/etc/demo") + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + // we are inspecting the type of the filesystem we are about to perform operation on. + {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, + {C: `fstat 3 `, R: syscall.Stat_t{}}, + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + // ext4 is writable, refuse further operations. + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 4`}, + }) +} + +// Ensure that writes to /etc/demo allowed if /etc is unrestricted. +func (s *utilsSuite) TestSecureMkdirAllWithUnrestrictedEtc(c *C) { + defer s.as.MockUnrestrictedPaths("/etc")() // Mark /etc as unrestricted. + s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) + rs := s.as.RestrictionsFor("/etc/demo") + c.Assert(update.MkdirAll("/etc/demo", 0755, 123, 456, rs), IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + // We are not interested in the type of filesystem at / + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + // We are not interested in the type of filesystem at /etc + {C: `close 3`}, + {C: `mkdirat 4 "demo" 0755`}, + {C: `openat 4 "demo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 123 456`}, + {C: `close 3`}, + {C: `close 4`}, + }) +} + +// Ensure that we can detect read only filesystems. +func (s *utilsSuite) TestSecureMkdirAllROFS(c *C) { + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) // just realistic + s.sys.InsertFault(`mkdirat 4 "path" 0755`, syscall.EROFS) + err := update.MkdirAll("/rofs/path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`) + c.Assert(err.(*update.ReadOnlyFsError).Path, Equals, "/rofs") + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "path" 0755`, E: syscall.EROFS}, + {C: `close 4`}, + }) +} + +// Ensure that we don't chown existing directories. +func (s *utilsSuite) TestSecureMkdirAllExistingDirsDontChown(c *C) { + s.sys.InsertFault(`mkdirat 3 "abs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`mkdirat 4 "path" 0755`, syscall.EEXIST) + err := update.MkdirAll("/abs/path", 0755, 123, 456, nil) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "abs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `mkdirat 4 "path" 0755`, E: syscall.EEXIST}, + {C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `close 3`}, + {C: `close 4`}, + }) +} + +// Ensure that we we close everything when mkdirat fails. +func (s *utilsSuite) TestSecureMkdirAllMkdiratError(c *C) { + s.sys.InsertFault(`mkdirat 3 "abs" 0755`, errTesting) + err := update.MkdirAll("/abs", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot create directory "/abs": testing`) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "abs" 0755`, E: errTesting}, + {C: `close 3`}, + }) +} + +// Ensure that we we close everything when fchown fails. +func (s *utilsSuite) TestSecureMkdirAllFchownError(c *C) { + s.sys.InsertFault(`fchown 4 123 456`, errTesting) + err := update.MkdirAll("/path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot chown directory "/path" to 123.456: testing`) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "path" 0755`}, + {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 123 456`, E: errTesting}, + {C: `close 4`}, + {C: `close 3`}, + }) +} + +// Check error path when we cannot open root directory. +func (s *utilsSuite) TestSecureMkdirAllOpenRootError(c *C) { + s.sys.InsertFault(`open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) + err := update.MkdirAll("/abs/path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, "cannot open root directory: testing") + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting}, + }) +} + +// Check error path when we cannot open non-root directory. +func (s *utilsSuite) TestSecureMkdirAllOpenError(c *C) { + s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) + err := update.MkdirAll("/abs/path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot open directory "/abs": testing`) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "abs" 0755`}, + {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting}, + {C: `close 3`}, + }) +} + +func (s *utilsSuite) TestPlanWritableMimic(c *C) { + s.sys.InsertSysLstatResult(`lstat "/foo" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) + restore := update.MockReadDir(func(dir string) ([]os.FileInfo, error) { + c.Assert(dir, Equals, "/foo") + return []os.FileInfo{ + testutil.FakeFileInfo("file", 0), + testutil.FakeFileInfo("dir", os.ModeDir), + testutil.FakeFileInfo("symlink", os.ModeSymlink), + testutil.FakeFileInfo("error-symlink-readlink", os.ModeSymlink), + // NOTE: None of the filesystem entries below are supported because + // they cannot be placed inside snaps or can only be created at + // runtime in areas that are already writable and this would never + // have to be handled in a writable mimic. + testutil.FakeFileInfo("block-dev", os.ModeDevice), + testutil.FakeFileInfo("char-dev", os.ModeDevice|os.ModeCharDevice), + testutil.FakeFileInfo("socket", os.ModeSocket), + testutil.FakeFileInfo("pipe", os.ModeNamedPipe), + }, nil + }) + defer restore() + restore = update.MockReadlink(func(name string) (string, error) { + switch name { + case "/foo/symlink": + return "target", nil + case "/foo/error-symlink-readlink": + return "", errTesting + } + panic("unexpected") + }) + defer restore() + + changes, err := update.PlanWritableMimic("/foo", "/foo/bar") + c.Assert(err, IsNil) + + c.Assert(changes, DeepEquals, []*update.Change{ + // Store /foo in /tmp/.snap/foo while we set things up + {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount}, + // Put a tmpfs over /foo + {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar", "mode=0755", "uid=0", "gid=0"}}, Action: update.Mount}, + // Bind mount files and directories over. Note that files are identified by x-snapd.kind=file option. + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + // Create symlinks. + // Bad symlinks and all other file types are skipped and not + // recorded in mount changes. + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + // Unmount the safe-keeping directory + {Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount}, + }) +} + +func (s *utilsSuite) TestPlanWritableMimicErrors(c *C) { + s.sys.InsertSysLstatResult(`lstat "/foo" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) + restore := update.MockReadDir(func(dir string) ([]os.FileInfo, error) { + c.Assert(dir, Equals, "/foo") + return nil, errTesting + }) + defer restore() + restore = update.MockReadlink(func(name string) (string, error) { + return "", errTesting + }) + defer restore() + + changes, err := update.PlanWritableMimic("/foo", "/foo/bar") + c.Assert(err, ErrorMatches, "testing") + c.Assert(changes, HasLen, 0) +} + +func (s *utilsSuite) TestExecWirableMimicSuccess(c *C) { + // This plan is the same as in the test above. This is what comes out of planWritableMimic. + plan := []*update.Change{ + {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount}, + } + + // Mock the act of performing changes, each of the change we perform is coming from the plan. + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + c.Assert(plan, testutil.DeepContains, chg) + return nil, nil + }) + defer restore() + + // The executed plan leaves us with a simplified view of the plan that is suitable for undo. + undoPlan, err := update.ExecWritableMimic(plan, s.as) + c.Assert(err, IsNil) + c.Assert(undoPlan, DeepEquals, []*update.Change{ + {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar", "x-snapd.detach"}}, Action: update.Mount}, + }) +} + +func (s *utilsSuite) TestExecWirableMimicErrorWithRecovery(c *C) { + // This plan is the same as in the test above. This is what comes out of planWritableMimic. + plan := []*update.Change{ + {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + // NOTE: the next perform will fail. Notably the symlink did not fail. + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount}, + } + + // Mock the act of performing changes. Before we inject a failure we ensure + // that each of the change we perform is coming from the plan. For the + // purpose of the test the change that bind mounts the "dir" over itself + // will fail and will trigger an recovery path. The changes performed in + // the recovery path are recorded. + var recoveryPlan []*update.Change + recovery := false + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + if !recovery { + c.Assert(plan, testutil.DeepContains, chg) + if chg.Entry.Name == "/tmp/.snap/foo/dir" { + recovery = true // switch to recovery mode + return nil, errTesting + } + } else { + recoveryPlan = append(recoveryPlan, chg) + } + return nil, nil + }) + defer restore() + + // The executed plan fails, leaving us with the error and an empty undo plan. + undoPlan, err := update.ExecWritableMimic(plan, s.as) + c.Assert(err, Equals, errTesting) + c.Assert(undoPlan, HasLen, 0) + // The changes we managed to perform were undone correctly. + c.Assert(recoveryPlan, DeepEquals, []*update.Change{ + // NOTE: there is no symlink undo entry as it is implicitly undone by unmounting the tmpfs. + {Entry: osutil.MountEntry{Name: "/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Unmount}, + {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Unmount}, + {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind", "x-snapd.detach"}}, Action: update.Unmount}, + }) +} + +func (s *utilsSuite) TestExecWirableMimicErrorNothingDone(c *C) { + // This plan is the same as in the test above. This is what comes out of planWritableMimic. + plan := []*update.Change{ + {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount}, + } + + // Mock the act of performing changes and just fail on any request. + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + return nil, errTesting + }) + defer restore() + + // The executed plan fails, the recovery didn't fail (it's empty) so we just return that error. + undoPlan, err := update.ExecWritableMimic(plan, s.as) + c.Assert(err, Equals, errTesting) + c.Assert(undoPlan, HasLen, 0) +} + +func (s *utilsSuite) TestExecWirableMimicErrorCannotUndo(c *C) { + // This plan is the same as in the test above. This is what comes out of planWritableMimic. + plan := []*update.Change{ + {Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount}, + {Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount}, + } + + // Mock the act of performing changes. After performing the first change + // correctly we will fail forever (this includes the recovery path) so the + // execute function ends up in a situation where it cannot perform the + // recovery path and will have to return a fatal error. + i := -1 + restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { + i++ + if i > 0 { + return nil, fmt.Errorf("failure-%d", i) + } + return nil, nil + }) + defer restore() + + // The plan partially succeeded and we cannot undo those changes. + _, err := update.ExecWritableMimic(plan, s.as) + c.Assert(err, ErrorMatches, `cannot undo change ".*" while recovering from earlier error failure-1: failure-2`) + c.Assert(err, FitsTypeOf, &update.FatalError{}) +} + +// realSystemSuite is not isolated / mocked from the system. +type realSystemSuite struct { + as *update.Assumptions +} + +var _ = Suite(&realSystemSuite{}) + +func (s *realSystemSuite) SetUpTest(c *C) { + s.as = &update.Assumptions{} + s.as.AddUnrestrictedPaths("/tmp") +} + +// Check that we can actually create directories. +// This doesn't test the chown logic as that requires root. +func (s *realSystemSuite) TestSecureMkdirAllForReal(c *C) { + d := c.MkDir() + + // Create d (which already exists) with mode 0777 (but c.MkDir() used 0700 + // internally and since we are not creating the directory we should not be + // changing that. + c.Assert(update.MkdirAll(d, 0777, sys.FlagID, sys.FlagID, nil), IsNil) + fi, err := os.Stat(d) + c.Assert(err, IsNil) + c.Check(fi.IsDir(), Equals, true) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0700)) + + // Create d1, which is a simple subdirectory, with a distinct mode and + // check that it was applied. Note that default umask 022 is subtracted so + // effective directory has different permissions. + d1 := filepath.Join(d, "subdir") + c.Assert(update.MkdirAll(d1, 0707, sys.FlagID, sys.FlagID, nil), IsNil) + fi, err = os.Stat(d1) + c.Assert(err, IsNil) + c.Check(fi.IsDir(), Equals, true) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0705)) + + // Create d2, which is a deeper subdirectory, with another distinct mode + // and check that it was applied. + d2 := filepath.Join(d, "subdir/subdir/subdir") + c.Assert(update.MkdirAll(d2, 0750, sys.FlagID, sys.FlagID, nil), IsNil) + fi, err = os.Stat(d2) + c.Assert(err, IsNil) + c.Check(fi.IsDir(), Equals, true) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0750)) +} + +// secure-mkfile-all + +// Ensure that we reject unclean paths. +func (s *utilsSuite) TestSecureMkfileAllUnclean(c *C) { + err := update.MkfileAll("/unclean//path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot split unclean path .*`) + c.Assert(s.sys.RCalls(), HasLen, 0) +} + +// Ensure that we refuse to create a file with an relative path. +func (s *utilsSuite) TestSecureMkfileAllRelative(c *C) { + err := update.MkfileAll("rel/path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot create file with relative path: "rel/path"`) + c.Assert(s.sys.RCalls(), HasLen, 0) +} + +// Ensure that we refuse creating the root directory as a file. +func (s *utilsSuite) TestSecureMkfileAllLevel0(c *C) { + err := update.MkfileAll("/", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot create non-file path: "/"`) + c.Assert(s.sys.RCalls(), HasLen, 0) +} + +// Ensure that we can create a file in the top-level directory. +func (s *utilsSuite) TestSecureMkfileAllLevel1(c *C) { + c.Assert(update.MkfileAll("/path", 0755, 123, 456, nil), IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4}, + {C: `fchown 4 123 456`}, + {C: `close 4`}, + {C: `close 3`}, + }) +} + +// Ensure that we can create a file two levels from the top-level directory. +func (s *utilsSuite) TestSecureMkfileAllLevel2(c *C) { + c.Assert(update.MkfileAll("/path/to", 0755, 123, 456, nil), IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "path" 0755`}, + {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 123 456`}, + {C: `close 3`}, + {C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, + {C: `fchown 3 123 456`}, + {C: `close 3`}, + {C: `close 4`}, + }) +} + +// Ensure that we can create a file three levels from the top-level directory. +func (s *utilsSuite) TestSecureMkfileAllLevel3(c *C) { + c.Assert(update.MkfileAll("/path/to/something", 0755, 123, 456, nil), IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "path" 0755`}, + {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fchown 4 123 456`}, + {C: `mkdirat 4 "to" 0755`}, + {C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `fchown 5 123 456`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `openat 5 "something" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, + {C: `fchown 3 123 456`}, + {C: `close 3`}, + {C: `close 5`}, + }) +} + +// Ensure that we can detect read only filesystems. +func (s *utilsSuite) TestSecureMkfileAllROFS(c *C) { + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) // just realistic + s.sys.InsertFault(`openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EROFS) + err := update.MkfileAll("/rofs/path", 0755, 123, 456, nil) + c.Check(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`) + c.Assert(err.(*update.ReadOnlyFsError).Path, Equals, "/rofs") + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EROFS}, + {C: `close 4`}, + }) +} + +// Ensure that we don't chown existing files or directories. +func (s *utilsSuite) TestSecureMkfileAllExistingDirsDontChown(c *C) { + s.sys.InsertFault(`mkdirat 3 "abs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EEXIST) + err := update.MkfileAll("/abs/path", 0755, 123, 456, nil) + c.Check(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "abs" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EEXIST}, + {C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC 0`, R: 3}, + {C: `close 3`}, + {C: `close 4`}, + }) +} + +// Ensure that we we close everything when openat fails. +func (s *utilsSuite) TestSecureMkfileAllOpenat2ndError(c *C) { + s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EEXIST) + s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC 0`, errTesting) + err := update.MkfileAll("/abs", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot open file "/abs": testing`) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EEXIST}, + {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC 0`, E: errTesting}, + {C: `close 3`}, + }) +} + +// Ensure that we we close everything when openat (non-exclusive) fails. +func (s *utilsSuite) TestSecureMkfileAllOpenatError(c *C) { + s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting) + err := update.MkfileAll("/abs", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot open file "/abs": testing`) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting}, + {C: `close 3`}, + }) +} + +// Ensure that we we close everything when fchown fails. +func (s *utilsSuite) TestSecureMkfileAllFchownError(c *C) { + s.sys.InsertFault(`fchown 4 123 456`, errTesting) + err := update.MkfileAll("/path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot chown file "/path" to 123.456: testing`) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4}, + {C: `fchown 4 123 456`, E: errTesting}, + {C: `close 4`}, + {C: `close 3`}, + }) +} + +// Check error path when we cannot open root directory. +func (s *utilsSuite) TestSecureMkfileAllOpenRootError(c *C) { + s.sys.InsertFault(`open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) + err := update.MkfileAll("/abs/path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, "cannot open root directory: testing") + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting}, + }) +} + +// Check error path when we cannot open non-root directory. +func (s *utilsSuite) TestSecureMkfileAllOpenError(c *C) { + s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) + err := update.MkfileAll("/abs/path", 0755, 123, 456, nil) + c.Assert(err, ErrorMatches, `cannot open directory "/abs": testing`) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "abs" 0755`}, + {C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting}, + {C: `close 3`}, + }) +} + +// We want to create a symlink in $SNAP_DATA and that's fine. +func (s *utilsSuite) TestSecureMksymlinkAllInSnapData(c *C) { + s.sys.InsertFault(`mkdirat 3 "var" 0755`, syscall.EEXIST) + s.sys.InsertFault(`mkdirat 4 "snap" 0755`, syscall.EEXIST) + s.sys.InsertFault(`mkdirat 5 "foo" 0755`, syscall.EEXIST) + s.sys.InsertFault(`mkdirat 6 "42" 0755`, syscall.EEXIST) + + err := update.MksymlinkAll("/var/snap/foo/42/symlink", 0755, 0, 0, "/oldname", nil) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "var" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "var" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `mkdirat 4 "snap" 0755`, E: syscall.EEXIST}, + {C: `openat 4 "snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `mkdirat 5 "foo" 0755`, E: syscall.EEXIST}, + {C: `openat 5 "foo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 6}, + {C: `mkdirat 6 "42" 0755`, E: syscall.EEXIST}, + {C: `openat 6 "42" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 7}, + {C: `close 6`}, + {C: `close 5`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `symlinkat "/oldname" 7 "symlink"`}, + {C: `close 7`}, + }) +} + +// We want to create a symlink in /etc but the host filesystem would be affected. +func (s *utilsSuite) TestSecureMksymlinkAllInEtc(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) + rs := s.as.RestrictionsFor("/etc/symlink") + err := update.MksymlinkAll("/etc/symlink", 0755, 0, 0, "/oldname", rs) + c.Assert(err, ErrorMatches, `cannot write to "/etc/symlink" because it would affect the host in "/etc"`) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, + {C: `fstat 3 `, R: syscall.Stat_t{}}, + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 4`}, + }) +} + +// We want to create a symlink deep in /etc but the host filesystem would be affected. +// This just shows that we pick the right place to construct the mimic +func (s *utilsSuite) TestSecureMksymlinkAllDeepInEtc(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) + rs := s.as.RestrictionsFor("/etc/some/other/stuff/symlink") + err := update.MksymlinkAll("/etc/some/other/stuff/symlink", 0755, 0, 0, "/oldname", rs) + c.Assert(err, ErrorMatches, `cannot write to "/etc/some/other/stuff/symlink" because it would affect the host in "/etc"`) + c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc") + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, + {C: `fstat 3 `, R: syscall.Stat_t{}}, + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 4`}, + {C: `close 3`}, + }) +} + +// We want to create a file in /etc but the host filesystem would be affected. +func (s *utilsSuite) TestSecureMkfileAllInEtc(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) + rs := s.as.RestrictionsFor("/etc/file") + err := update.MkfileAll("/etc/file", 0755, 0, 0, rs) + c.Assert(err, ErrorMatches, `cannot write to "/etc/file" because it would affect the host in "/etc"`) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, + {C: `fstat 3 `, R: syscall.Stat_t{}}, + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 4`}, + }) +} + +// We want to create a directory in /etc but the host filesystem would be affected. +func (s *utilsSuite) TestSecureMkdirAllInEtc(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) + rs := s.as.RestrictionsFor("/etc/dir") + err := update.MkdirAll("/etc/dir", 0755, 0, 0, rs) + c.Assert(err, ErrorMatches, `cannot write to "/etc/dir" because it would affect the host in "/etc"`) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, + {C: `fstat 3 `, R: syscall.Stat_t{}}, + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `close 4`}, + }) +} + +// We want to create a directory in /snap/foo/42/dir and want to know what happens. +func (s *utilsSuite) TestSecureMkdirAllInSNAP(c *C) { + // Allow creating directories under /snap/ related to this snap ("foo"). + // This matches what is done inside main(). + restore := s.as.MockUnrestrictedPaths("/snap/foo") + defer restore() + + s.sys.InsertFault(`mkdirat 3 "snap" 0755`, syscall.EEXIST) + s.sys.InsertFault(`mkdirat 4 "foo" 0755`, syscall.EEXIST) + s.sys.InsertFault(`mkdirat 5 "42" 0755`, syscall.EEXIST) + + rs := s.as.RestrictionsFor("/snap/foo/42/dir") + err := update.MkdirAll("/snap/foo/42/dir", 0755, 0, 0, rs) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "snap" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `mkdirat 4 "foo" 0755`, E: syscall.EEXIST}, + {C: `openat 4 "foo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `mkdirat 5 "42" 0755`, E: syscall.EEXIST}, + {C: `openat 5 "42" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 6}, + {C: `close 5`}, + {C: `close 4`}, + {C: `close 3`}, + {C: `mkdirat 6 "dir" 0755`}, + {C: `openat 6 "dir" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 6`}, + }) +} + +// We want to create a symlink in /etc which is a tmpfs that we mounted so that is ok. +func (s *utilsSuite) TestSecureMksymlinkAllInEtcAfterMimic(c *C) { + // Because /etc is not on a list of unrestricted paths the write to + // /etc/symlink must be validated with step-by-step operation. + rootStatfs := syscall.Statfs_t{Type: update.SquashfsMagic, Flags: update.StReadOnly} + rootStat := syscall.Stat_t{} + etcStatfs := syscall.Statfs_t{Type: update.TmpfsMagic} + etcStat := syscall.Stat_t{} + s.as.AddChange(&update.Change{Action: update.Mount, Entry: osutil.MountEntry{Dir: "/etc", Type: "tmpfs", Name: "tmpfs"}}) + s.sys.InsertFstatfsResult(`fstatfs 3 `, rootStatfs) + s.sys.InsertFstatResult(`fstat 3 `, rootStat) + s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) + s.sys.InsertFstatfsResult(`fstatfs 4 `, etcStatfs) + s.sys.InsertFstatResult(`fstat 4 `, etcStat) + rs := s.as.RestrictionsFor("/etc/symlink") + err := update.MksymlinkAll("/etc/symlink", 0755, 0, 0, "/oldname", rs) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fstatfs 3 `, R: rootStatfs}, + {C: `fstat 3 `, R: rootStat}, + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: etcStatfs}, + {C: `fstat 4 `, R: etcStat}, + {C: `symlinkat "/oldname" 4 "symlink"`}, + {C: `close 4`}, + }) +} + +// We want to create a file in /etc which is a tmpfs created by snapd so that's okay. +func (s *utilsSuite) TestSecureMkfileAllInEtcAfterMimic(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) + s.as.AddChange(&update.Change{Action: update.Mount, Entry: osutil.MountEntry{Dir: "/etc", Type: "tmpfs", Name: "tmpfs"}}) + rs := s.as.RestrictionsFor("/etc/file") + err := update.MkfileAll("/etc/file", 0755, 0, 0, rs) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, + {C: `fstat 3 `, R: syscall.Stat_t{}}, + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `openat 4 "file" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 4`}, + }) +} + +// We want to create a directory in /etc which is a tmpfs created by snapd so that is ok. +func (s *utilsSuite) TestSecureMkdirAllInEtcAfterMimic(c *C) { + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) + s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{}) + s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) + s.as.AddChange(&update.Change{Action: update.Mount, Entry: osutil.MountEntry{Dir: "/etc", Type: "tmpfs", Name: "tmpfs"}}) + rs := s.as.RestrictionsFor("/etc/dir") + err := update.MkdirAll("/etc/dir", 0755, 0, 0, rs) + c.Assert(err, IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, + {C: `fstat 3 `, R: syscall.Stat_t{}}, + {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, + {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `close 3`}, + {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, + {C: `fstat 4 `, R: syscall.Stat_t{}}, + {C: `mkdirat 4 "dir" 0755`}, + {C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 4`}, + }) +} + +// Check that we can actually create files. +// This doesn't test the chown logic as that requires root. +func (s *realSystemSuite) TestSecureMkfileAllForReal(c *C) { + d := c.MkDir() + + // Create f1, which is a simple subdirectory, with a distinct mode and + // check that it was applied. Note that default umask 022 is subtracted so + // effective directory has different permissions. + f1 := filepath.Join(d, "file") + c.Assert(update.MkfileAll(f1, 0707, sys.FlagID, sys.FlagID, nil), IsNil) + fi, err := os.Stat(f1) + c.Assert(err, IsNil) + c.Check(fi.Mode().IsRegular(), Equals, true) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0705)) + + // Create f2, which is a deeper subdirectory, with another distinct mode + // and check that it was applied. + f2 := filepath.Join(d, "subdir/subdir/file") + c.Assert(update.MkfileAll(f2, 0750, sys.FlagID, sys.FlagID, nil), IsNil) + fi, err = os.Stat(f2) + c.Assert(err, IsNil) + c.Check(fi.Mode().IsRegular(), Equals, true) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0750)) +} + +// Check that we can actually create symlinks. +// This doesn't test the chown logic as that requires root. +func (s *realSystemSuite) TestSecureMksymlinkAllForReal(c *C) { + d := c.MkDir() + + // Create symlink f1 that points to "oldname" and check that it + // is correct. Note that symlink permissions are always set to 0777 + f1 := filepath.Join(d, "symlink") + err := update.MksymlinkAll(f1, 0755, sys.FlagID, sys.FlagID, "oldname", nil) + c.Assert(err, IsNil) + fi, err := os.Lstat(f1) + c.Assert(err, IsNil) + c.Check(fi.Mode()&os.ModeSymlink, Equals, os.ModeSymlink) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0777)) + + target, err := os.Readlink(f1) + c.Assert(err, IsNil) + c.Check(target, Equals, "oldname") + + // Create an identical symlink to see that it doesn't fail. + err = update.MksymlinkAll(f1, 0755, sys.FlagID, sys.FlagID, "oldname", nil) + c.Assert(err, IsNil) + + // Create a different symlink and see that it fails now + err = update.MksymlinkAll(f1, 0755, sys.FlagID, sys.FlagID, "other", nil) + c.Assert(err, ErrorMatches, `cannot create symbolic link ".*/symlink": existing symbolic link in the way`) + + // Create an file and check that it clashes with a symlink we attempt to create. + f2 := filepath.Join(d, "file") + err = update.MkfileAll(f2, 0755, sys.FlagID, sys.FlagID, nil) + c.Assert(err, IsNil) + err = update.MksymlinkAll(f2, 0755, sys.FlagID, sys.FlagID, "oldname", nil) + c.Assert(err, ErrorMatches, `cannot create symbolic link ".*/file": existing file in the way`) + + // Create an file and check that it clashes with a symlink we attempt to create. + f3 := filepath.Join(d, "dir") + err = update.MkdirAll(f3, 0755, sys.FlagID, sys.FlagID, nil) + c.Assert(err, IsNil) + err = update.MksymlinkAll(f3, 0755, sys.FlagID, sys.FlagID, "oldname", nil) + c.Assert(err, ErrorMatches, `cannot create symbolic link ".*/dir": existing file in the way`) + + err = update.MksymlinkAll("/", 0755, sys.FlagID, sys.FlagID, "oldname", nil) + c.Assert(err, ErrorMatches, `cannot create non-file path: "/"`) +} + +func (s *utilsSuite) TestCleanTrailingSlash(c *C) { + // This is a sanity test for the use of filepath.Clean in secureMk{dir,file}All + c.Assert(filepath.Clean("/path/"), Equals, "/path") + c.Assert(filepath.Clean("path/"), Equals, "path") + c.Assert(filepath.Clean("path/."), Equals, "path") + c.Assert(filepath.Clean("path/.."), Equals, ".") + c.Assert(filepath.Clean("other/path/.."), Equals, "other") +} + +// secure-open-path + +func (s *utilsSuite) TestSecureOpenPath(c *C) { + stat := syscall.Stat_t{Mode: syscall.S_IFDIR} + s.sys.InsertFstatResult("fstat 5 ", stat) + fd, err := update.OpenPath("/foo/bar") + c.Assert(err, IsNil) + defer s.sys.Close(fd) + c.Assert(fd, Equals, 5) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "foo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, + {C: `openat 4 "bar" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, + {C: `fstat 5 `, R: stat}, + {C: `close 4`}, + {C: `close 3`}, + }) +} + +func (s *utilsSuite) TestSecureOpenPathSingleSegment(c *C) { + stat := syscall.Stat_t{Mode: syscall.S_IFDIR} + s.sys.InsertFstatResult("fstat 4 ", stat) + fd, err := update.OpenPath("/foo") + c.Assert(err, IsNil) + defer s.sys.Close(fd) + c.Assert(fd, Equals, 4) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `openat 3 "foo" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, + {C: `fstat 4 `, R: stat}, + {C: `close 3`}, + }) +} + +func (s *utilsSuite) TestSecureOpenPathRoot(c *C) { + stat := syscall.Stat_t{Mode: syscall.S_IFDIR} + s.sys.InsertFstatResult("fstat 3 ", stat) + fd, err := update.OpenPath("/") + c.Assert(err, IsNil) + defer s.sys.Close(fd) + c.Assert(fd, Equals, 3) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, + {C: `fstat 3 `, R: stat}, + }) +} + +func (s *realSystemSuite) TestSecureOpenPathDirectory(c *C) { + path := filepath.Join(c.MkDir(), "test") + c.Assert(os.Mkdir(path, 0755), IsNil) + + fd, err := update.OpenPath(path) + c.Assert(err, IsNil) + defer syscall.Close(fd) + + // check that the file descriptor is for the expected path + origDir, err := os.Getwd() + c.Assert(err, IsNil) + defer os.Chdir(origDir) + + c.Assert(syscall.Fchdir(fd), IsNil) + cwd, err := os.Getwd() + c.Assert(err, IsNil) + c.Check(cwd, Equals, path) +} + +func (s *realSystemSuite) TestSecureOpenPathRelativePath(c *C) { + fd, err := update.OpenPath("relative/path") + c.Check(fd, Equals, -1) + c.Check(err, ErrorMatches, "path .* is not absolute") +} + +func (s *realSystemSuite) TestSecureOpenPathUncleanPath(c *C) { + base := c.MkDir() + path := filepath.Join(base, "test") + c.Assert(os.Mkdir(path, 0755), IsNil) + + fd, err := update.OpenPath(base + "//test") + c.Check(fd, Equals, -1) + c.Check(err, ErrorMatches, `cannot open path: cannot iterate over unclean path ".*//test"`) + + fd, err = update.OpenPath(base + "/./test") + c.Check(fd, Equals, -1) + c.Check(err, ErrorMatches, `cannot open path: cannot iterate over unclean path ".*/./test"`) + + fd, err = update.OpenPath(base + "/test/../test") + c.Check(fd, Equals, -1) + c.Check(err, ErrorMatches, `cannot open path: cannot iterate over unclean path ".*/test/../test"`) +} + +func (s *realSystemSuite) TestSecureOpenPathFile(c *C) { + path := filepath.Join(c.MkDir(), "file.txt") + c.Assert(ioutil.WriteFile(path, []byte("hello"), 0644), IsNil) + + fd, err := update.OpenPath(path) + c.Assert(err, IsNil) + defer syscall.Close(fd) + + // Check that the file descriptor matches the file. + var pathStat, fdStat syscall.Stat_t + c.Assert(syscall.Stat(path, &pathStat), IsNil) + c.Assert(syscall.Fstat(fd, &fdStat), IsNil) + c.Check(pathStat, Equals, fdStat) +} + +func (s *realSystemSuite) TestSecureOpenPathNotFound(c *C) { + path := filepath.Join(c.MkDir(), "test") + + fd, err := update.OpenPath(path) + c.Check(fd, Equals, -1) + c.Check(err, ErrorMatches, "no such file or directory") +} + +func (s *realSystemSuite) TestSecureOpenPathSymlink(c *C) { + base := c.MkDir() + dir := filepath.Join(base, "test") + c.Assert(os.Mkdir(dir, 0755), IsNil) + + symlink := filepath.Join(base, "symlink") + c.Assert(os.Symlink(dir, symlink), IsNil) + + fd, err := update.OpenPath(symlink) + c.Check(fd, Equals, -1) + c.Check(err, ErrorMatches, `".*" is a symbolic link`) +} + +func (s *realSystemSuite) TestSecureOpenPathSymlinkedParent(c *C) { + base := c.MkDir() + dir := filepath.Join(base, "dir1") + symlink := filepath.Join(base, "symlink") + + path := filepath.Join(dir, "dir2") + symlinkedPath := filepath.Join(symlink, "dir2") + + c.Assert(os.Mkdir(dir, 0755), IsNil) + c.Assert(os.Symlink(dir, symlink), IsNil) + c.Assert(os.Mkdir(path, 0755), IsNil) + + fd, err := update.OpenPath(symlinkedPath) + c.Check(fd, Equals, -1) + c.Check(err, ErrorMatches, "not a directory") +} diff --git a/cmd/snap/cmd_abort.go b/cmd/snap/cmd_abort.go new file mode 100644 index 00000000..54c92baa --- /dev/null +++ b/cmd/snap/cmd_abort.go @@ -0,0 +1,62 @@ +// -*- 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{ changeIDMixin } + +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{} + }, + changeIDMixinOptDesc, + changeIDMixinArgDesc, + ) +} + +func (x *cmdAbort) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + id, err := x.GetChangeID() + if err != nil { + if err == noChangeFoundOK { + return nil + } + return err + } + _, err = x.client.Abort(id) + return err +} diff --git a/cmd/snap/cmd_abort_test.go b/cmd/snap/cmd_abort_test.go new file mode 100644 index 00000000..a50cdaef --- /dev/null +++ b/cmd/snap/cmd_abort_test.go @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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) TestAbortLast(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes") + fmt.Fprintln(w, mockChangesJSON) + case 2: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/changes/two") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{"action": "abort"}) + fmt.Fprintln(w, mockChangeJSON) + default: + c.Errorf("expected 2 queries, currently on %d", n) + } + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"abort", "--last=install"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") + + c.Assert(n, check.Equals, 2) +} + +func (s *SnapSuite) TestAbortLastQuestionmark(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + c.Check(r.Method, check.Equals, "GET") + c.Assert(r.URL.Path, check.Equals, "/v2/changes") + switch n { + case 1, 2: + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + case 3, 4: + fmt.Fprintln(w, mockChangesJSON) + default: + c.Errorf("expected 4 calls, now on %d", n) + } + }) + for i := 0; i < 2; i++ { + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"abort", "--last=foobar?"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, "") + c.Check(s.Stderr(), check.Equals, "") + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"abort", "--last=foobar"}) + if i == 0 { + c.Assert(err, check.ErrorMatches, `no changes found`) + } else { + c.Assert(err, check.ErrorMatches, `no changes of type "foobar" found`) + } + } + + c.Check(n, check.Equals, 4) +} diff --git a/cmd/snap/cmd_ack.go b/cmd/snap/cmd_ack.go new file mode 100644 index 00000000..92f40ddf --- /dev/null +++ b/cmd/snap/cmd_ack.go @@ -0,0 +1,79 @@ +// -*- 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/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type cmdAck struct { + clientMixin + AckOptions struct { + AssertionFile flags.Filename + } `positional-args:"true" required:"true"` +} + +var shortAckHelp = i18n.G("Add 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 pre-existing 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{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Assertion file"), + }}) +} + +func ackFile(cli *client.Client, assertFile string) error { + assertData, err := ioutil.ReadFile(assertFile) + if err != nil { + return err + } + + return cli.Ack(assertData) +} + +func (x *cmdAck) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + if err := ackFile(x.client, string(x.AckOptions.AssertionFile)); err != nil { + return fmt.Errorf("cannot assert: %v", err) + } + return nil +} diff --git a/cmd/snap/cmd_advise.go b/cmd/snap/cmd_advise.go new file mode 100644 index 00000000..faa2fbbd --- /dev/null +++ b/cmd/snap/cmd_advise.go @@ -0,0 +1,287 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "encoding/json" + "fmt" + "io" + "net" + "os" + "strconv" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/advisor" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" +) + +type cmdAdviseSnap struct { + Positionals struct { + CommandOrPkg string + } `positional-args:"true"` + + Format string `long:"format" default:"pretty" choice:"pretty" choice:"json"` + // Command makes advise try to find snaps that provide this command + Command bool `long:"command"` + + // FromApt tells advise that it got started from an apt hook + // and needs to communicate over a socket + FromApt bool `long:"from-apt"` +} + +var shortAdviseSnapHelp = i18n.G("Advise on available snaps") +var longAdviseSnapHelp = i18n.G(` +The advise-snap command searches for and suggests the installation of snaps. + +If --command is given, it suggests snaps that provide the given command. +Otherwise it suggests snaps with the given name. +`) + +func init() { + cmd := addCommand("advise-snap", shortAdviseSnapHelp, longAdviseSnapHelp, func() flags.Commander { + return &cmdAdviseSnap{} + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "command": i18n.G("Advise on snaps that provide the given command"), + // TRANSLATORS: This should not start with a lowercase letter. + "from-apt": i18n.G("Advise will talk to apt via an apt hook"), + // TRANSLATORS: This should not start with a lowercase letter. + "format": i18n.G("Use the given output format"), + }, []argDesc{ + // TRANSLATORS: This needs to begin with < and end with > + {name: i18n.G("")}, + }) + cmd.hidden = true +} + +func outputAdviseExactText(command string, result []advisor.Command) error { + fmt.Fprintf(Stdout, "\n") + // TRANSLATORS: %q is a command name (like "gimp" or "loimpress") + fmt.Fprintf(Stdout, i18n.G("Command %q not found, but can be installed with:\n"), command) + fmt.Fprintf(Stdout, "\n") + for _, snap := range result { + fmt.Fprintf(Stdout, "sudo snap install %s\n", snap.Snap) + } + fmt.Fprintf(Stdout, "\n") + fmt.Fprintln(Stdout, i18n.G("See 'snap info ' for additional versions.")) + fmt.Fprintf(Stdout, "\n") + return nil +} + +func outputAdviseMisspellText(command string, result []advisor.Command) error { + fmt.Fprintf(Stdout, "\n") + fmt.Fprintf(Stdout, i18n.G("Command %q not found, did you mean:\n"), command) + fmt.Fprintf(Stdout, "\n") + for _, snap := range result { + fmt.Fprintf(Stdout, i18n.G(" command %q from snap %q\n"), snap.Command, snap.Snap) + } + fmt.Fprintf(Stdout, "\n") + fmt.Fprintln(Stdout, i18n.G("See 'snap info ' for additional versions.")) + fmt.Fprintf(Stdout, "\n") + return nil +} + +func outputAdviseJSON(command string, results []advisor.Command) error { + enc := json.NewEncoder(Stdout) + enc.Encode(results) + return nil +} + +type jsonRPC struct { + JsonRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params struct { + Command string `json:"command"` + SearchTerms []string `json:"search-terms"` + UnknownPackages []string `json:"unknown-packages"` + } +} + +// readRpc reads a apt json rpc protocol 0.1 message as described in +// https://salsa.debian.org/apt-team/apt/blob/master/doc/json-hooks-protocol.md#wire-protocol +func readRpc(r *bufio.Reader) (*jsonRPC, error) { + line, err := r.ReadBytes('\n') + if err != nil && err != io.EOF { + return nil, fmt.Errorf("cannot read json-rpc: %v", err) + } + if osutil.GetenvBool("SNAP_APT_HOOK_DEBUG") { + fmt.Fprintf(os.Stderr, "%s\n", line) + } + + var rpc jsonRPC + if err := json.Unmarshal(line, &rpc); err != nil { + return nil, err + } + // empty \n + emptyNL, _, err := r.ReadLine() + if err != nil { + return nil, err + } + if string(emptyNL) != "" { + return nil, fmt.Errorf("unexpected line: %q (empty)", emptyNL) + } + + return &rpc, nil +} + +func adviseViaAptHook() error { + sockFd := os.Getenv("APT_HOOK_SOCKET") + if sockFd == "" { + return fmt.Errorf("cannot find APT_HOOK_SOCKET env") + } + fd, err := strconv.Atoi(sockFd) + if err != nil { + return fmt.Errorf("expected APT_HOOK_SOCKET to be a decimal integer, found %q", sockFd) + } + + f := os.NewFile(uintptr(fd), "apt-hook-socket") + if f == nil { + return fmt.Errorf("cannot open file descriptor %v", fd) + } + defer f.Close() + + conn, err := net.FileConn(f) + if err != nil { + return fmt.Errorf("cannot connect to %v: %v", fd, err) + } + defer conn.Close() + + r := bufio.NewReader(conn) + + // handshake + rpc, err := readRpc(r) + if err != nil { + return err + } + if rpc.Method != "org.debian.apt.hooks.hello" { + return fmt.Errorf("expected 'hello' method, got: %v", rpc.Method) + } + if _, err := conn.Write([]byte(`{"jsonrpc":"2.0","id":0,"result":{"version":"0.1"}}` + "\n\n")); err != nil { + return err + } + + // payload + rpc, err = readRpc(r) + if err != nil { + return err + } + if rpc.Method == "org.debian.apt.hooks.install.fail" { + for _, pkgName := range rpc.Params.UnknownPackages { + match, err := advisor.FindPackage(pkgName) + if err == nil && match != nil { + fmt.Fprintf(Stdout, "\n") + fmt.Fprintf(Stdout, i18n.G("No apt package %q, but there is a snap with that name.\n"), pkgName) + fmt.Fprintf(Stdout, i18n.G("Try \"snap install %s\"\n"), pkgName) + fmt.Fprintf(Stdout, "\n") + } + } + + } + // if rpc.Method == "org.debian.apt.hooks.search.post" { + // // FIXME: do a snap search here + // // FIXME2: figure out why apt does not tell us the search results + // } + + // bye + rpc, err = readRpc(r) + if err != nil { + return err + } + if rpc.Method != "org.debian.apt.hooks.bye" { + return fmt.Errorf("expected 'bye' method, got: %v", rpc.Method) + } + + return nil +} + +func (x *cmdAdviseSnap) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if x.FromApt { + return adviseViaAptHook() + } + + if len(x.Positionals.CommandOrPkg) == 0 { + return fmt.Errorf("the required argument `` was not provided") + } + + if x.Command { + return adviseCommand(x.Positionals.CommandOrPkg, x.Format) + } + + return advisePkg(x.Positionals.CommandOrPkg) +} + +func advisePkg(pkgName string) error { + match, err := advisor.FindPackage(pkgName) + if err != nil { + return fmt.Errorf("advise for pkgname failed: %s", err) + } + if match != nil { + fmt.Fprintf(Stdout, i18n.G("Packages matching %q:\n"), pkgName) + fmt.Fprintf(Stdout, " * %s - %s\n", match.Snap, match.Summary) + fmt.Fprintf(Stdout, i18n.G("Try: snap install \n")) + } + + // FIXME: find mispells + + return nil +} + +func adviseCommand(cmd string, format string) error { + // find exact matches + matches, err := advisor.FindCommand(cmd) + if err != nil { + return fmt.Errorf("advise for command failed: %s", err) + } + if len(matches) > 0 { + switch format { + case "json": + return outputAdviseJSON(cmd, matches) + case "pretty": + return outputAdviseExactText(cmd, matches) + default: + return fmt.Errorf("unsupported format %q", format) + } + } + + // find misspellings + matches, err = advisor.FindMisspelledCommand(cmd) + if err != nil { + return err + } + if len(matches) > 0 { + switch format { + case "json": + return outputAdviseJSON(cmd, matches) + case "pretty": + return outputAdviseMisspellText(cmd, matches) + default: + return fmt.Errorf("unsupported format %q", format) + } + } + + return fmt.Errorf("%s: command not found", cmd) +} diff --git a/cmd/snap/cmd_advise_test.go b/cmd/snap/cmd_advise_test.go new file mode 100644 index 00000000..6f9a6b1d --- /dev/null +++ b/cmd/snap/cmd_advise_test.go @@ -0,0 +1,243 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 ( + "bufio" + "bytes" + "fmt" + "net" + "os" + "strconv" + "strings" + "syscall" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/advisor" + snap "github.com/snapcore/snapd/cmd/snap" +) + +type sillyFinder struct{} + +func mkSillyFinder() (advisor.Finder, error) { + return &sillyFinder{}, nil +} + +func (sf *sillyFinder) FindCommand(command string) ([]advisor.Command, error) { + switch command { + case "hello": + return []advisor.Command{ + {Snap: "hello", Command: "hello"}, + {Snap: "hello-wcm", Command: "hello"}, + }, nil + case "error-please": + return nil, fmt.Errorf("get failed") + default: + return nil, nil + } +} + +func (sf *sillyFinder) FindPackage(pkgName string) (*advisor.Package, error) { + switch pkgName { + case "hello": + return &advisor.Package{Snap: "hello", Summary: "summary for hello"}, nil + case "error-please": + return nil, fmt.Errorf("find-pkg failed") + default: + return nil, nil + } +} + +func (*sillyFinder) Close() error { return nil } + +func (s *SnapSuite) TestAdviseCommandHappyText(c *C) { + restore := advisor.ReplaceCommandsFinder(mkSillyFinder) + defer restore() + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"advise-snap", "--command", "hello"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, ` +Command "hello" not found, but can be installed with: + +sudo snap install hello +sudo snap install hello-wcm + +See 'snap info ' for additional versions. + +`) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestAdviseCommandHappyJSON(c *C) { + restore := advisor.ReplaceCommandsFinder(mkSillyFinder) + defer restore() + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"advise-snap", "--command", "--format=json", "hello"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, `[{"Snap":"hello","Command":"hello"},{"Snap":"hello-wcm","Command":"hello"}]`+"\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestAdviseCommandMisspellText(c *C) { + restore := advisor.ReplaceCommandsFinder(mkSillyFinder) + defer restore() + + for _, misspelling := range []string{"helo", "0hello", "hell0", "hello0"} { + err := snap.AdviseCommand(misspelling, "pretty") + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, fmt.Sprintf(` +Command "%s" not found, did you mean: + + command "hello" from snap "hello" + command "hello" from snap "hello-wcm" + +See 'snap info ' for additional versions. + +`, misspelling)) + c.Assert(s.Stderr(), Equals, "") + + s.stdout.Reset() + s.stderr.Reset() + } +} + +func (s *SnapSuite) TestAdviseFromAptIntegrationNoAptPackage(c *C) { + restore := advisor.ReplaceCommandsFinder(mkSillyFinder) + defer restore() + + fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + c.Assert(err, IsNil) + + os.Setenv("APT_HOOK_SOCKET", strconv.Itoa(int(fds[1]))) + // note we don't close fds[1] ourselves; adviseViaAptHook might, or we might leak it + // (we don't close it here to avoid accidentally closing an arbitrary file descriptor that reused the number) + + done := make(chan bool, 1) + go func() { + f := os.NewFile(uintptr(fds[0]), "advise-sock") + conn, err := net.FileConn(f) + c.Assert(err, IsNil) + defer conn.Close() + defer f.Close() + + // handshake + _, err = conn.Write([]byte(`{"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1"]}}` + "\n\n")) + c.Assert(err, IsNil) + + // reply from snap + r := bufio.NewReader(conn) + buf, _, err := r.ReadLine() + c.Assert(err, IsNil) + c.Assert(string(buf), Equals, `{"jsonrpc":"2.0","id":0,"result":{"version":"0.1"}}`) + // plus empty line + buf, _, err = r.ReadLine() + c.Assert(err, IsNil) + c.Assert(string(buf), Equals, ``) + + // payload + _, err = conn.Write([]byte(`{"jsonrpc":"2.0","method":"org.debian.apt.hooks.install.fail","params":{"command":"install","search-terms":["aws-cli"],"unknown-packages":["hello"],"packages":[]}}` + "\n\n")) + c.Assert(err, IsNil) + + // bye + _, err = conn.Write([]byte(`{"jsonrpc":"2.0","method":"org.debian.apt.hooks.bye","params":{}}` + "\n\n")) + c.Assert(err, IsNil) + + done <- true + }() + + cmd := snap.CmdAdviseSnap() + cmd.FromApt = true + err = cmd.Execute(nil) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ` +No apt package "hello", but there is a snap with that name. +Try "snap install hello" + +`) + c.Assert(s.Stderr(), Equals, "") + c.Assert(<-done, Equals, true) +} + +func (s *SnapSuite) TestReadRpc(c *C) { + rpc := strings.Replace(` +{ + "jsonrpc": "2.0", + "method": "org.debian.apt.hooks.install.pre-prompt", + "params": { + "command": "install", + "packages": [ + { + "architecture": "amd64", + "automatic": false, + "id": 38033, + "mode": "install", + "name": "hello", + "versions": { + "candidate": { + "architecture": "amd64", + "id": 22712, + "pin": 500, + "version": "4:17.12.3-1ubuntu1" + }, + "install": { + "architecture": "amd64", + "id": 22712, + "pin": 500, + "version": "4:17.12.3-1ubuntu1" + } + } + }, + { + "architecture": "amd64", + "automatic": true, + "id": 38202, + "mode": "install", + "name": "hello-kpart", + "versions": { + "candidate": { + "architecture": "amd64", + "id": 22713, + "pin": 500, + "version": "4:17.12.3-1ubuntu1" + }, + "install": { + "architecture": "amd64", + "id": 22713, + "pin": 500, + "version": "4:17.12.3-1ubuntu1" + } + } + } + ], + "search-terms": [ + "hello" + ], + "unknown-packages": [] + } +}`, "\n", "", -1) + // all apt rpc ends with \n\n + rpc = rpc + "\n\n" + // this can be parsed without errors + _, err := snap.ReadRpc(bufio.NewReader(bytes.NewBufferString(rpc))) + c.Assert(err, IsNil) +} diff --git a/cmd/snap/cmd_alias.go b/cmd/snap/cmd_alias.go new file mode 100644 index 00000000..2d627461 --- /dev/null +++ b/cmd/snap/cmd_alias.go @@ -0,0 +1,118 @@ +// -*- 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" + "text/tabwriter" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/snap" +) + +type cmdAlias struct { + waitMixin + Positionals struct { + SnapApp appName `required:"yes"` + Alias string `required:"yes"` + } `positional-args:"true"` +} + +// TODO: implement a completer for snapApp + +var shortAliasHelp = i18n.G("Set up a manual alias") +var longAliasHelp = i18n.G(` +The alias command aliases the given snap application to the given alias. + +Once this manual alias is setup the respective application command can be +invoked just using the alias. +`) + +func init() { + addCommand("alias", shortAliasHelp, longAliasHelp, func() flags.Commander { + return &cmdAlias{} + }, waitDescs, []argDesc{ + {name: ""}, + // TRANSLATORS: This needs to begin with < and end with > + {name: i18n.G("")}, + }) +} + +func (x *cmdAlias) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapName, appName := snap.SplitSnapApp(string(x.Positionals.SnapApp)) + alias := x.Positionals.Alias + + id, err := x.client.Alias(snapName, appName, alias) + if err != nil { + return err + } + chg, err := x.wait(id) + if err != nil { + if err == noWait { + return nil + } + return err + } + + return showAliasChanges(chg) +} + +type changedAlias struct { + Snap string `json:"snap"` + App string `json:"app"` + Alias string `json:"alias"` +} + +func showAliasChanges(chg *client.Change) error { + var added, removed []*changedAlias + if err := chg.Get("aliases-added", &added); err != nil && err != client.ErrNoData { + return err + } + if err := chg.Get("aliases-removed", &removed); err != nil && err != client.ErrNoData { + return err + } + w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) + if len(added) != 0 { + // TRANSLATORS: this is used to introduce a list of aliases that were added + printChangedAliases(w, i18n.G("Added"), added) + } + if len(removed) != 0 { + // TRANSLATORS: this is used to introduce a list of aliases that were removed + printChangedAliases(w, i18n.G("Removed"), removed) + } + w.Flush() + return nil +} + +func printChangedAliases(w io.Writer, label string, changed []*changedAlias) { + fmt.Fprintf(w, "%s:\n", label) + for _, a := range changed { + // TRANSLATORS: the first %s is a snap command (e.g. "hello-world.echo"), the second is the alias + fmt.Fprintf(w, i18n.G("\t- %s as %s\n"), snap.JoinSnapApp(a.Snap, a.App), a.Alias) + } +} diff --git a/cmd/snap/cmd_alias_test.go b/cmd/snap/cmd_alias_test.go new file mode 100644 index 00000000..bede9d62 --- /dev/null +++ b/cmd/snap/cmd_alias_test.go @@ -0,0 +1,74 @@ +// -*- 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 alias [alias-OPTIONS] [] [] + +The alias command aliases the given snap application to the given alias. + +Once this manual alias is setup the respective application command can be +invoked just using the alias. + +[alias command options] + --no-wait Do not wait for the operation to finish but just print + the change id. +` + s.testSubCommandHelp(c, "alias", msg) +} + +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", + "app": "cmd1", + "alias": "alias1", + }) + 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", "data": {"aliases-added": [{"alias": "alias1", "snap": "alias-snap", "app": "cmd1"}]}}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser(Client()).ParseArgs([]string{"alias", "alias-snap.cmd1", "alias1"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, ""+ + "Added:\n"+ + " - alias-snap.cmd1 as alias1\n", + ) + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_aliases.go b/cmd/snap/cmd_aliases.go new file mode 100644 index 00000000..d0cd10aa --- /dev/null +++ b/cmd/snap/cmd_aliases.go @@ -0,0 +1,147 @@ +// -*- 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 { + clientMixin + Positionals struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"true"` +} + +var shortAliasesHelp = i18n.G("List 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 temporarily (e.g. +because of a revert). This can cleared with 'snap alias --reset'. +`) + +func init() { + addCommand("aliases", shortAliasesHelp, longAliasesHelp, func() flags.Commander { + return &cmdAliases{} + }, nil, nil) +} + +type aliasInfo struct { + Snap string + Command string + Alias string + Status string + Auto 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].Command < infos[j].Command { + return true + } + if infos[i].Command == infos[j].Command { + 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 := x.client.Aliases() + if err != nil { + return err + } + + 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, + Command: aliasStatus.Command, + Alias: alias, + Status: aliasStatus.Status, + Auto: aliasStatus.Auto, + }) + } + } + + if len(infos) > 0 { + w := tabWriter() + fmt.Fprintln(w, i18n.G("Command\tAlias\tNotes")) + defer w.Flush() + + sort.Sort(infos) + + for _, info := range infos { + var notes []string + if info.Status != "auto" { + notes = append(notes, info.Status) + if info.Status == "manual" && info.Auto != "" { + notes = append(notes, "override") + } + } + notesStr := strings.Join(notes, ",") + if notesStr == "" { + notesStr = "-" + } + fmt.Fprintf(w, "%s\t%s\t%s\n", info.Command, info.Alias, notesStr) + } + } else { + if filterSnap != "" { + fmt.Fprintf(Stderr, i18n.G("No aliases are currently defined for snap %q.\n"), filterSnap) + } else { + fmt.Fprintln(Stderr, i18n.G("No aliases are currently defined.")) + } + fmt.Fprintln(Stderr, i18n.G("\nUse 'snap help alias' to learn how to create aliases manually.")) + } + return nil +} diff --git a/cmd/snap/cmd_aliases_test.go b/cmd/snap/cmd_aliases_test.go new file mode 100644 index 00000000..4563c503 --- /dev/null +++ b/cmd/snap/cmd_aliases_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 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 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 temporarily (e.g. +because of a revert). This can cleared with 'snap alias --reset'. +` + s.testSubCommandHelp(c, "aliases", msg) +} + +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": {Command: "foo", Status: "auto", Auto: "foo"}, + "foo_reset": {Command: "foo.reset", Manual: "reset", Status: "manual"}, + }, + "bar": { + "bar_dump": {Command: "bar.dump", Status: "manual", Manual: "dump"}, + "bar_dump.1": {Command: "bar.dump", Status: "disabled", Auto: "dump"}, + "bar_restore": {Command: "bar.safe-restore", Status: "manual", Auto: "restore", Manual: "safe-restore"}, + }, + }, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"aliases"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Command Alias Notes\n" + + "bar.dump bar_dump manual\n" + + "bar.dump bar_dump.1 disabled\n" + + "bar.safe-restore bar_restore manual,override\n" + + "foo foo0 -\n" + + "foo.reset foo_reset manual\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": {Command: "foo", Status: "auto", Auto: "foo"}, + "foo_reset": {Command: "foo.reset", Manual: "reset", Status: "manual"}, + }, + "bar": { + "bar_dump": {Command: "bar.dump", Status: "manual", Manual: "dump"}, + "bar_dump.1": {Command: "bar.dump", Status: "disabled", Auto: "dump"}, + }, + }, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"aliases", "foo"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Command Alias Notes\n" + + "foo foo0 -\n" + + "foo.reset foo_reset manual\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(Client()).ParseArgs([]string{"aliases"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "No aliases are currently defined.\n\nUse 'snap help alias' to learn how to create aliases manually.\n") +} + +func (s *SnapSuite) TestAliasesNoneFilterSnap(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{ + "bar": { + "bar0": {Command: "foo", Status: "auto", Auto: "foo"}, + }}, + }) + }) + _, err := Parser(Client()).ParseArgs([]string{"aliases", "not-bar"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "No aliases are currently defined for snap \"not-bar\".\n\nUse 'snap help alias' to learn how to create aliases manually.\n") +} + +func (s *SnapSuite) TestAliasesSorting(c *C) { + tests := []struct { + snap1 string + cmd1 string + alias1 string + snap2 string + cmd2 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.app1", "appx", "bar", "bar.app1", "appy"}, + } + + for _, test := range tests { + res := AliasInfoLess(test.snap1, test.alias1, test.cmd1, test.snap2, test.alias2, test.cmd2) + c.Check(res, Equals, true, Commentf("%v", test)) + + rres := AliasInfoLess(test.snap2, test.alias2, test.cmd2, test.snap1, test.alias1, test.cmd1) + 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..7408371e --- /dev/null +++ b/cmd/snap/cmd_auto_import.go @@ -0,0 +1,305 @@ +// -*- 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 + } + // skip all ram disks (unless in tests) + if !osutil.GetenvBool("SNAPPY_TESTING") && strings.HasPrefix(mountSrc, "/dev/ram") { + 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(cli *client.Client) (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(cli, 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(cli *client.Client) (int, error) { + cands, err := autoImportCandidates() + if err != nil { + return 0, err + } + + added := 0 + for _, cand := range cands { + err := ackFile(cli, 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", "-t", "ext4,vfat", "-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 { + clientMixin + Mount []string `long:"mount" arg-name:""` + + ForceClassic bool `long:"force-classic"` +} + +var shortAutoImportHelp = i18n.G("Inspect 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 temporarily +mounted to be inspected as well. Even in that case the command will still +consider all available mounted devices for inspection. + +Assertions to be imported 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{ + // TRANSLATORS: This should not start with a lowercase letter. + "mount": i18n.G("Temporarily mount device before inspecting"), + // TRANSLATORS: This should not start with a lowercase letter. + "force-classic": i18n.G("Force import on classic systems"), + }, nil) + cmd.hidden = true +} + +func (x *cmdAutoImport) autoAddUsers() error { + cmd := cmdCreateUser{ + clientMixin: x.clientMixin, + 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(x.client) + if err != nil { + return err + } + + added2, err := autoImportFromAllMounts(x.client) + if err != nil { + return err + } + + if added1+added2 > 0 { + return x.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..6f0d76a7 --- /dev/null +++ b/cmd/snap/cmd_auto_import_test.go @@ -0,0 +1,302 @@ +// -*- 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() + + logbuf, restore := logger.MockLogger() + defer restore() + + rest, err := snap.Parser(snap.Client()).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(logbuf.String(), 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(snap.Client()).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:]) +} + +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(snap.Client()).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() + + logbuf, restore := logger.MockLogger() + defer restore() + + 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(snap.Client()).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(logbuf.String(), 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) + } + + }) + + 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) + + logbuf, restore := logger.MockLogger() + defer restore() + + rest, err := snap.Parser(snap.Client()).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(logbuf.String(), 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() + + _, restoreLogger := logger.MockLogger() + defer restoreLogger() + + // 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(snap.Client()).ParseArgs([]string{"auto-import"}) + c.Assert(err, ErrorMatches, "cannot queue .*, file size too big: 656384") +} diff --git a/cmd/snap/cmd_blame.go b/cmd/snap/cmd_blame.go new file mode 100644 index 00000000..d3f4d2c5 --- /dev/null +++ b/cmd/snap/cmd_blame.go @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 + +//go:generate mkauthors.sh + +import ( + "fmt" + "math/rand" + + "github.com/jessevdk/go-flags" +) + +type cmdBlame struct{} + +var authors []string + +func init() { + cmd := addCommand("blame", + "", + "", + func() flags.Commander { + return &cmdBlame{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdBlame) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + if len(authors) == 0 { + return nil + } + + fmt.Fprintf(Stdout, "It's all %s's fault.\n", authors[rand.Intn(len(authors))]) + return nil +} diff --git a/cmd/snap/cmd_blame_generated.go b/cmd/snap/cmd_blame_generated.go new file mode 100644 index 00000000..c32f6686 --- /dev/null +++ b/cmd/snap/cmd_blame_generated.go @@ -0,0 +1,7 @@ +package main + +// generated by mkauthors.sh; do not edit + +func init() { + authors = []string{"Mark Shuttleworth", "Gustavo Niemeyer", "Sergio Schvezov", "Simon Fels", "Kyle Fazzari", "Leo Arias", "Sergio Cazzolato", "Gustavo Niemeyer", "Federico Gimenez", "Maciej Borzecki", "Jamie Strandboge", "Pawel Stolowski", "John R. Lenton", "Samuele Pedroni", "Zygmunt Krynicki", "Michael Vogt"} +} diff --git a/cmd/snap/cmd_booted.go b/cmd/snap/cmd_booted.go new file mode 100644 index 00000000..1d64240a --- /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", + "The booted command is only retained for backwards compatibility.", + 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\n") + return nil +} diff --git a/cmd/snap/cmd_buy.go b/cmd/snap/cmd_buy.go new file mode 100644 index 00000000..df6cf0b3 --- /dev/null +++ b/cmd/snap/cmd_buy.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 main + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortBuyHelp = i18n.G("Buy a snap") +var longBuyHelp = i18n.G(` +The buy command buys a snap from the store. +`) + +type cmdBuy struct { + clientMixin + Positional struct { + SnapName remoteSnapName + } `positional-args:"yes" required:"yes"` +} + +func init() { + cmd := addCommand("buy", shortBuyHelp, longBuyHelp, func() flags.Commander { + return &cmdBuy{} + }, map[string]string{}, []argDesc{{ + name: "", + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Snap name"), + }}) + cmd.hidden = true +} + +func (x *cmdBuy) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + return buySnap(x.client, string(x.Positional.SnapName)) +} + +func buySnap(cli *client.Client, snapName string) error { + 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 := &client.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 need to have a payment method associated with your account in order to buy a snap, please visit https://my.ubuntu.com/payment/edit to add one. + +Once you’ve added your payment details, you just need to run 'snap buy %s' again.`), snap.Name) + case client.ErrorKindTermsNotAccepted: + return fmt.Errorf(i18n.G(`In order to buy %q, you need to agree to the latest terms and conditions. Please visit https://my.ubuntu.com/payment/edit to do this. + +Once completed, return here and run 'snap buy %s' again.`), snap.Name, 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.Publisher.Username, formatPrice(opts.Price, opts.Currency)) + fmt.Fprint(Stdout, "\n") + + err = requestLogin(cli, 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..003ce990 --- /dev/null +++ b/cmd/snap/cmd_buy_test.go @@ -0,0 +1,462 @@ +// -*- 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(snap.Client()).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(snap.Client()).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(snap.Client()).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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "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(snap.Client()).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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "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(snap.Client()).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(snap.Client()).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(snap.Client()).ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `You need to have a payment method associated with your account in order to buy a snap, please visit https://my.ubuntu.com/payment/edit to add one. + +Once you’ve added your payment details, you just need to 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(snap.Client()).ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `In order to buy "hello", you need to agree to the latest terms and conditions. Please visit https://my.ubuntu.com/payment/edit to do this. + +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(snap.Client()).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_can_manage_refreshes.go b/cmd/snap/cmd_can_manage_refreshes.go new file mode 100644 index 00000000..32b8855d --- /dev/null +++ b/cmd/snap/cmd_can_manage_refreshes.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 main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" +) + +type cmdCanManageRefreshes struct { + clientMixin +} + +func init() { + cmd := addDebugCommand("can-manage-refreshes", + "(internal) return if refresh.schedule=managed can be used", + "(internal) return if refresh.schedule=managed can be used", + func() flags.Commander { + return &cmdCanManageRefreshes{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdCanManageRefreshes) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + var resp bool + if err := x.client.Debug("can-manage-refreshes", nil, &resp); err != nil { + return err + } + fmt.Fprintf(Stdout, "%v\n", resp) + return nil +} diff --git a/cmd/snap/cmd_changes.go b/cmd/snap/cmd_changes.go new file mode 100644 index 00000000..caf4f10e --- /dev/null +++ b/cmd/snap/cmd_changes.go @@ -0,0 +1,209 @@ +// -*- 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" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortChangesHelp = i18n.G("List system changes") +var shortTasksHelp = i18n.G("List a change's tasks") +var longChangesHelp = i18n.G(` +The changes command displays a summary of system changes performed recently. +`) +var longTasksHelp = i18n.G(` +The tasks command displays a summary of tasks associated with an individual +change. +`) + +type cmdChanges struct { + clientMixin + timeMixin + Positional struct { + Snap string `positional-arg-name:""` + } `positional-args:"yes"` +} + +type cmdTasks struct { + timeMixin + changeIDMixin +} + +func init() { + addCommand("changes", shortChangesHelp, longChangesHelp, + func() flags.Commander { return &cmdChanges{} }, timeDescs, nil) + addCommand("tasks", shortTasksHelp, longTasksHelp, + func() flags.Commander { return &cmdTasks{} }, + changeIDMixinOptDesc.also(timeDescs), + changeIDMixinArgDesc).alias = "change" +} + +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 queryChanges(cli *client.Client, opts *client.ChangesOptions) ([]*client.Change, error) { + chgs, err := cli.Changes(opts) + if err != nil { + return nil, err + } + if err := warnMaintenance(cli); err != nil { + return nil, err + } + return chgs, nil +} + +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 tasks %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, + } + + changes, err := queryChanges(c.client, &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 := c.fmtTime(chg.SpawnTime) + readyTime := c.fmtTime(chg.ReadyTime) + 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 *cmdTasks) Execute([]string) error { + chid, err := c.GetChangeID() + if err != nil { + if err == noChangeFoundOK { + return nil + } + return err + } + + return c.showChange(chid) +} + +func queryChange(cli *client.Client, chid string) (*client.Change, error) { + chg, err := cli.Change(chid) + if err != nil { + return nil, err + } + if err := warnMaintenance(cli); err != nil { + return nil, err + } + return chg, nil +} + +func (c *cmdTasks) showChange(chid string) error { + chg, err := queryChange(c.client, chid) + if err != nil { + return err + } + + w := tabWriter() + + fmt.Fprintf(w, i18n.G("Status\tSpawn\tReady\tSummary\n")) + for _, t := range chg.Tasks { + spawnTime := c.fmtTime(t.SpawnTime) + readyTime := c.fmtTime(t.ReadyTime) + 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 = "......................................................................" + +func warnMaintenance(cli *client.Client) error { + if maintErr := cli.Maintenance(); maintErr != nil { + msg, err := errorToCmdMessage("", maintErr, nil) + if err != nil { + return err + } + fmt.Fprintf(Stderr, "WARNING: %s\n", msg) + } + return nil +} diff --git a/cmd/snap/cmd_changes_test.go b/cmd/snap/cmd_changes_test.go new file mode 100644 index 00000000..35aa9bd8 --- /dev/null +++ b/cmd/snap/cmd_changes_test.go @@ -0,0 +1,233 @@ +// -*- 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" + + 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) { + if n < 2 { + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, mockChangeJSON) + } else { + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + expectedChange := `(?ms)Status +Spawn +Ready +Summary +Do +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary +` + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"change", "--abs-time", "42"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, expectedChange) + c.Check(s.Stderr(), check.Equals, "") + + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--abs-time", "42"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, expectedChange) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestChangeSimpleRebooting(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + if n < 2 { + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, strings.Replace(mockChangeJSON, `"type": "sync"`, `"type": "sync", "maintenance": {"kind": "system-restart", "message": "system is restarting"}`, 1)) + } else { + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"change", "42"}) + c.Assert(err, check.IsNil) + c.Check(s.Stderr(), check.Equals, "WARNING: snapd is about to reboot the system\n") +} + +var mockChangesJSON = `{"type": "sync", "result": [ + { + "id": "four", + "kind": "install-snap", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2015-02-21T01:02:03Z", + "ready-time": "2015-02-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2015-02-21T01:02:03Z", "ready-time": "2015-02-21T01:02:04Z"}] + }, + { + "id": "one", + "kind": "remove-snap", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-03-21T01:02:03Z", + "ready-time": "2016-03-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-03-21T01:02:03Z", "ready-time": "2016-03-21T01:02:04Z"}] + }, + { + "id": "two", + "kind": "install-snap", + "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"}] + }, + { + "id": "three", + "kind": "install-snap", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-01-21T01:02:03Z", + "ready-time": "2016-01-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-01-21T01:02:03Z", "ready-time": "2016-01-21T01:02:04Z"}] + } +]}` + +func (s *SnapSuite) TestTasksLast(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "GET") + if r.URL.Path == "/v2/changes" { + fmt.Fprintln(w, mockChangesJSON) + return + } + c.Assert(r.URL.Path, check.Equals, "/v2/changes/two") + fmt.Fprintln(w, mockChangeJSON) + }) + expectedChange := `(?ms)Status +Spawn +Ready +Summary +Do +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary +` + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--abs-time", "--last=install"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, expectedChange) + c.Check(s.Stderr(), check.Equals, "") + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--abs-time", "--last=foobar"}) + c.Assert(err, check.NotNil) + c.Assert(err, check.ErrorMatches, `no changes of type "foobar" found`) +} + +func (s *SnapSuite) TestTasksLastQuestionmark(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + c.Check(r.Method, check.Equals, "GET") + c.Assert(r.URL.Path, check.Equals, "/v2/changes") + switch n { + case 1, 2: + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + case 3, 4: + fmt.Fprintln(w, mockChangesJSON) + default: + c.Errorf("expected 4 calls, now on %d", n) + } + }) + for i := 0; i < 2; i++ { + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--last=foobar?"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, "") + c.Check(s.Stderr(), check.Equals, "") + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--last=foobar"}) + if i == 0 { + c.Assert(err, check.ErrorMatches, `no changes found`) + } else { + c.Assert(err, check.ErrorMatches, `no changes of type "foobar" found`) + } + } + + c.Check(n, check.Equals, 4) +} + +func (s *SnapSuite) TestTasksSyntaxError(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"tasks", "--abs-time", "--last=install", "42"}) + c.Assert(err, check.NotNil) + c.Assert(err, check.ErrorMatches, `cannot use change ID and type together`) + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"tasks"}) + c.Assert(err, check.NotNil) + c.Assert(err, check.ErrorMatches, `please provide change ID or type with --last=`) +} + +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(snap.Client()).ParseArgs([]string{"change", "--abs-time", "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_confinement.go b/cmd/snap/cmd_confinement.go new file mode 100644 index 00000000..a5b1473f --- /dev/null +++ b/cmd/snap/cmd_confinement.go @@ -0,0 +1,57 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/i18n" + + "fmt" + + "github.com/jessevdk/go-flags" +) + +var shortConfinementHelp = i18n.G("Print the confinement mode the system operates in") +var longConfinementHelp = i18n.G(` +The confinement command will print the confinement mode (strict, +partial or none) the system operates in. +`) + +type cmdConfinement struct { + clientMixin +} + +func init() { + addDebugCommand("confinement", shortConfinementHelp, longConfinementHelp, func() flags.Commander { + return &cmdConfinement{} + }, nil, nil) +} + +func (cmd cmdConfinement) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + sysInfo, err := cmd.client.SysInfo() + if err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", sysInfo.Confinement) + return nil +} diff --git a/cmd/snap/cmd_confinement_test.go b/cmd/snap/cmd_confinement_test.go new file mode 100644 index 00000000..b76e5f75 --- /dev/null +++ b/cmd/snap/cmd_confinement_test.go @@ -0,0 +1,39 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestConfinement(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"confinement": "strict"}}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "confinement"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "strict\n") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_connect.go b/cmd/snap/cmd_connect.go new file mode 100644 index 00000000..909992f4 --- /dev/null +++ b/cmd/snap/cmd_connect.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 + +import ( + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdConnect struct { + waitMixin + Positionals struct { + PlugSpec connectPlugSpec `required:"yes"` + SlotSpec connectSlotSpec + } `positional-args:"true"` +} + +var shortConnectHelp = i18n.G("Connect 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{} + }, waitDescs, []argDesc{ + // TRANSLATORS: This needs to begin with < and end with > + {name: i18n.G(":")}, + // TRANSLATORS: This needs to begin with < and end with > + {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 = "" + } + + id, err := x.client.Connect(x.Positionals.PlugSpec.Snap, x.Positionals.PlugSpec.Name, x.Positionals.SlotSpec.Snap, x.Positionals.SlotSpec.Name) + if err != nil { + return err + } + + if _, err := x.wait(id); err != nil { + if err == noWait { + return nil + } + return err + } + + return nil +} diff --git a/cmd/snap/cmd_connect_test.go b/cmd/snap/cmd_connect_test.go new file mode 100644 index 00000000..c7a85dce --- /dev/null +++ b/cmd/snap/cmd_connect_test.go @@ -0,0 +1,325 @@ +// -*- 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" + "os" + + "github.com/jessevdk/go-flags" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestConnectHelp(c *C) { + msg := `Usage: + snap.test connect [connect-OPTIONS] [:] [:] + +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. + +[connect command options] + --no-wait Do not wait for the operation to finish but just print + the change id. +` + s.testSubCommandHelp(c, "connect", msg) +} + +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(Client()).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(Client()).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(Client()).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(Client()).ParseArgs([]string{"connect", "plug", "consumer"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} + +var fortestingConnectionList = client.Connections{ + Slots: []client.Slot{ + { + Snap: "core", + Name: "x11", + Interface: "x11", + }, + { + Snap: "core", + Name: "core-support", + Interface: "core-support", + Connections: []client.PlugRef{ + { + Snap: "core", + Name: "core-support-plug", + }, + }, + }, + { + Snap: "wake-up-alarm", + Name: "toggle", + Interface: "bool-file", + Label: "Alarm toggle", + }, + { + 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: "core", + Name: "core-support-plug", + Interface: "core-support", + Connections: []client.SlotRef{ + { + Snap: "core", + Name: "core-support", + }, + }, + }, + { + Snap: "core", + Name: "network-bind-plug", + Interface: "network-bind", + }, + { + Snap: "paste-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + }, + { + Snap: "potato", + Name: "frying", + Interface: "frying", + Label: "Ability to fry a network service", + }, + { + Snap: "keyboard-lights", + Name: "capslock-led", + Interface: "bool-file", + Label: "Capslock indicator LED", + Connections: []client.SlotRef{ + { + Snap: "canonical-pi2", + Name: "pin-13", + }, + }, + }, + }, +} + +func (s *SnapSuite) TestConnectCompletion(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Assert(r.Method, Equals, "GET") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": fortestingConnectionList, + }) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + os.Setenv("GO_FLAGS_COMPLETION", "verbose") + defer os.Unsetenv("GO_FLAGS_COMPLETION") + + expected := []flags.Completion{} + parser := Parser(Client()) + parser.CompletionHandler = func(obtained []flags.Completion) { + c.Check(obtained, DeepEquals, expected) + } + + expected = []flags.Completion{{Item: "core:"}, {Item: "paste-daemon:"}, {Item: "potato:"}} + _, err := parser.ParseArgs([]string{"connect", ""}) + c.Assert(err, IsNil) + + // connect's first argument can't start with : (only for the 2nd arg, the slot) + expected = nil + _, err = parser.ParseArgs([]string{"connect", ":"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "paste-daemon:network-listening", Description: "plug"}} + _, err = parser.ParseArgs([]string{"connect", "pa"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "core:"}, {Item: "wake-up-alarm:"}} + _, err = parser.ParseArgs([]string{"connect", "paste-daemon:network-listening", ""}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "wake-up-alarm:toggle", Description: "slot"}} + _, err = parser.ParseArgs([]string{"connect", "paste-daemon:network-listening", "w"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: ":x11", Description: "slot"}} + _, err = parser.ParseArgs([]string{"connect", "paste-daemon:network-listening", ":"}) + c.Assert(err, IsNil) + + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_connectivity_check.go b/cmd/snap/cmd_connectivity_check.go new file mode 100644 index 00000000..9f102ccf --- /dev/null +++ b/cmd/snap/cmd_connectivity_check.go @@ -0,0 +1,64 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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 cmdConnectivityCheck struct { + clientMixin +} + +func init() { + addDebugCommand("connectivity", + "Check network connectivity status", + "The connectivity command checks the network connectivity of snapd.", + func() flags.Commander { + return &cmdConnectivityCheck{} + }, nil, nil) +} + +func (x *cmdConnectivityCheck) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + var status struct { + Connectivity bool + Unreachable []string + } + if err := x.client.Debug("connectivity", nil, &status); err != nil { + return err + } + + fmt.Fprintf(Stdout, "Connectivity status:\n") + if len(status.Unreachable) == 0 { + fmt.Fprintf(Stdout, " * PASS\n") + return nil + } + + for _, uri := range status.Unreachable { + fmt.Fprintf(Stdout, " * %s: unreachable\n", uri) + } + return fmt.Errorf("%v servers unreachable", len(status.Unreachable)) +} diff --git a/cmd/snap/cmd_connectivity_check_test.go b/cmd/snap/cmd_connectivity_check_test.go new file mode 100644 index 00000000..ca242bbd --- /dev/null +++ b/cmd/snap/cmd_connectivity_check_test.go @@ -0,0 +1,84 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" +) + +func (s *SnapSuite) TestConnectivityHappy(c *check.C) { + n := 0 + s.RedirectClientToTestServer(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/debug") + c.Check(r.URL.RawQuery, check.Equals, "") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`)) + fmt.Fprintln(w, `{"type": "sync", "result": {}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "connectivity"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `Connectivity status: + * PASS +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestConnectivityUnhappy(c *check.C) { + n := 0 + s.RedirectClientToTestServer(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/debug") + c.Check(r.URL.RawQuery, check.Equals, "") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`)) + fmt.Fprintln(w, `{"type": "sync", "result": {"connectivity":false,"unreachable":["foo.bar.com"]}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "connectivity"}) + c.Assert(err, check.ErrorMatches, "1 servers unreachable") + // note that only the unreachable hosts are displayed + c.Check(s.Stdout(), check.Equals, `Connectivity status: + * foo.bar.com: unreachable +`) + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_create_key.go b/cmd/snap/cmd_create_key.go new file mode 100644 index 00000000..b10e4d1a --- /dev/null +++ b/cmd/snap/cmd_create_key.go @@ -0,0 +1,91 @@ +// -*- 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(` +The create-key command creates a cryptographic key pair that can be +used for signing assertions. +`), + func() flags.Commander { + return &cmdCreateKey{} + }, nil, []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + 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..c4484577 --- /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(snap.Client()).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..f2df5018 --- /dev/null +++ b/cmd/snap/cmd_create_user.go @@ -0,0 +1,118 @@ +// -*- 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("Create 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 { + clientMixin + 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{ + // TRANSLATORS: This should not start with a lowercase letter. + "json": i18n.G("Output results in JSON format"), + // TRANSLATORS: This should not start with a lowercase letter. + "sudoer": i18n.G("Grant sudo access to the created user"), + // TRANSLATORS: This should not start with a lowercase letter. + "known": i18n.G("Use known assertions for user creation"), + // TRANSLATORS: This should not start with a lowercase letter. + "force-managed": i18n.G("Force adding the user, even if the device is already managed"), + }, []argDesc{{ + // TRANSLATORS: This is a noun and it needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter (unless it's "login.ubuntu.com"). Also, 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 + } + + 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 = x.client.CreateUsers([]*client.CreateUserOptions{&options}) + } else { + result, err = x.client.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..d8e8bf14 --- /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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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_debug.go b/cmd/snap/cmd_debug.go new file mode 100644 index 00000000..f7e11b27 --- /dev/null +++ b/cmd/snap/cmd_debug.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 cmdDebug struct{} + +var shortDebugHelp = i18n.G("Run debug commands") +var longDebugHelp = i18n.G(` +The debug command contains a selection of additional sub-commands. + +Debug commands can be removed without notice and may not work on +non-development systems. +`) diff --git a/cmd/snap/cmd_delete_key.go b/cmd/snap/cmd_delete_key.go new file mode 100644 index 00000000..f5502f2e --- /dev/null +++ b/cmd/snap/cmd_delete_key.go @@ -0,0 +1,60 @@ +// -*- 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(` +The delete-key command deletes the local cryptographic key pair with +the given name. +`), + func() flags.Commander { + return &cmdDeleteKey{} + }, nil, []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + 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..1f7dfdcd --- /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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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..c26ed9a9 --- /dev/null +++ b/cmd/snap/cmd_disconnect.go @@ -0,0 +1,101 @@ +// -*- 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/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdDisconnect struct { + waitMixin + Positionals struct { + Offer disconnectSlotOrPlugSpec `required:"true"` + Use disconnectSlotSpec + } `positional-args:"true"` +} + +var shortDisconnectHelp = i18n.G("Disconnect 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{} + }, waitDescs, []argDesc{ + // TRANSLATORS: This needs to begin with < and end with > + {name: i18n.G(":")}, + // TRANSLATORS: This needs to begin with < and end with > + {name: i18n.G(":")}, + }) +} + +func (x *cmdDisconnect) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + offer := x.Positionals.Offer.SnapAndName + use := x.Positionals.Use.SnapAndName + + // snap disconnect : + // snap disconnect + if use.Snap == "" && use.Name == "" { + // Swap Offer and Use around + offer, use = use, offer + } + if use.Name == "" { + return fmt.Errorf("please provide the plug or slot name to disconnect from snap %q", use.Snap) + } + + id, err := x.client.Disconnect(offer.Snap, offer.Name, use.Snap, use.Name) + if err != nil { + if client.IsInterfacesUnchangedError(err) { + fmt.Fprintf(Stdout, i18n.G("No connections to disconnect")) + fmt.Fprintf(Stdout, "\n") + return nil + } + return err + } + + if _, err := x.wait(id); err != nil { + if err == noWait { + return nil + } + return err + } + + return nil +} diff --git a/cmd/snap/cmd_disconnect_test.go b/cmd/snap/cmd_disconnect_test.go new file mode 100644 index 00000000..6e88ed22 --- /dev/null +++ b/cmd/snap/cmd_disconnect_test.go @@ -0,0 +1,224 @@ +// -*- 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" + "os" + + "github.com/jessevdk/go-flags" + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestDisconnectHelp(c *C) { + msg := `Usage: + snap.test disconnect [disconnect-OPTIONS] [:] [:] + +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. + +[disconnect command options] + --no-wait Do not wait for the operation to finish but just print + the change id. +` + s.testSubCommandHelp(c, "disconnect", msg) +} + +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(Client()).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(Client()).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(Client()).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(Client()).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, "") +} + +func (s *SnapSuite) TestDisconnectCompletion(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Assert(r.Method, Equals, "GET") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": fortestingConnectionList, + }) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + os.Setenv("GO_FLAGS_COMPLETION", "verbose") + defer os.Unsetenv("GO_FLAGS_COMPLETION") + + expected := []flags.Completion{} + parser := Parser(Client()) + parser.CompletionHandler = func(obtained []flags.Completion) { + c.Check(obtained, DeepEquals, expected) + } + + expected = []flags.Completion{{Item: "canonical-pi2:"}, {Item: "core:"}, {Item: "keyboard-lights:"}} + _, err := parser.ParseArgs([]string{"disconnect", ""}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "canonical-pi2:pin-13", Description: "slot"}} + _, err = parser.ParseArgs([]string{"disconnect", "ca"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: ":core-support", Description: "slot"}, {Item: ":core-support-plug", Description: "plug"}} + _, err = parser.ParseArgs([]string{"disconnect", ":"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "keyboard-lights:capslock-led", Description: "plug"}} + _, err = parser.ParseArgs([]string{"disconnect", "k"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "canonical-pi2:"}, {Item: "core:"}} + _, err = parser.ParseArgs([]string{"disconnect", "keyboard-lights:capslock-led", ""}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "canonical-pi2:pin-13", Description: "slot"}} + _, err = parser.ParseArgs([]string{"disconnect", "keyboard-lights:capslock-led", "ca"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: ":core-support", Description: "slot"}} + _, err = parser.ParseArgs([]string{"disconnect", ":core-support-plug", ":"}) + c.Assert(err, IsNil) + + 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..dab63c93 --- /dev/null +++ b/cmd/snap/cmd_download.go @@ -0,0 +1,151 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-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 . + * + */ + +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/snap" +) + +type cmdDownload struct { + channelMixin + Revision string `long:"revision"` + + Positional struct { + Snap remoteSnapName + } `positional-args:"true" required:"true"` +} + +var shortDownloadHelp = i18n.G("Download the given snap") +var longDownloadHelp = i18n.G(` +The download command downloads the given snap and its supporting assertions +to the current directory with .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: "", + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Snap name"), + }}) +} + +func fetchSnapAssertions(tsto *image.ToolingStore, snapPath string, snapInfo *snap.Info) (string, 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 := tsto.AssertionFetcher(db, save) + + _, err = image.FetchAndCheckSnapAssertions(snapPath, snapInfo, f, db) + return assertPath, err +} + +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 { + if x.Channel != "" { + return fmt.Errorf(i18n.G("cannot specify both channel and revision")) + } + var err error + revision, err = snap.ParseRevision(x.Revision) + if err != nil { + return err + } + } + + snapName := string(x.Positional.Snap) + + tsto, err := image.NewToolingStore() + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("Fetching snap %q\n"), snapName) + dlOpts := image.DownloadOptions{ + TargetDir: "", // cwd + Channel: x.Channel, + } + snapPath, snapInfo, err := tsto.DownloadSnap(snapName, revision, &dlOpts) + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("Fetching assertions for %q\n"), snapName) + assertPath, err := fetchSnapAssertions(tsto, snapPath, snapInfo) + if err != nil { + return err + } + + // simplify paths + wd, _ := os.Getwd() + if p, err := filepath.Rel(wd, assertPath); err == nil { + assertPath = p + } + if p, err := filepath.Rel(wd, snapPath); err == nil { + snapPath = p + } + // add a hint what to do with the downloaded snap (LP:1676707) + fmt.Fprintf(Stdout, i18n.G(`Install the snap with: + snap ack %s + snap install %s +`), assertPath, snapPath) + + return nil +} diff --git a/cmd/snap/cmd_ensure_state_soon.go b/cmd/snap/cmd_ensure_state_soon.go new file mode 100644 index 00000000..8e8476f4 --- /dev/null +++ b/cmd/snap/cmd_ensure_state_soon.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 + +import ( + "github.com/jessevdk/go-flags" +) + +type cmdEnsureStateSoon struct { + clientMixin +} + +func init() { + cmd := addDebugCommand("ensure-state-soon", + "(internal) trigger an ensure run in the state engine", + "(internal) trigger an ensure run in the state engine", + func() flags.Commander { + return &cmdEnsureStateSoon{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdEnsureStateSoon) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + return x.client.Debug("ensure-state-soon", nil, nil) +} diff --git a/cmd/snap/cmd_ensure_state_soon_test.go b/cmd/snap/cmd_ensure_state_soon_test.go new file mode 100644 index 00000000..bf1580ff --- /dev/null +++ b/cmd/snap/cmd_ensure_state_soon_test.go @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestEnsureStateSoon(c *check.C) { + n := 0 + s.RedirectClientToTestServer(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/debug") + c.Check(r.URL.RawQuery, check.Equals, "") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(data, check.DeepEquals, []byte(`{"action":"ensure-state-soon"}`)) + fmt.Fprintln(w, `{"type": "sync", "result": true}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "ensure-state-soon"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_export_key.go b/cmd/snap/cmd_export_key.go new file mode 100644 index 00000000..94aa7029 --- /dev/null +++ b/cmd/snap/cmd_export_key.go @@ -0,0 +1,100 @@ +// -*- 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(` +The export-key command exports 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{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + 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..ab6ff72d --- /dev/null +++ b/cmd/snap/cmd_export_key_test.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_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(snap.Client()).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(snap.Client()).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(snap.Client()).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) { + storeSigning := assertstest.NewStoreStack("canonical", nil) + manager := asserts.NewGPGKeypairManager() + assertstest.NewAccount(storeSigning, "developer1", nil, "") + rest, err := snap.Parser(snap.Client()).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..a2f20f91 --- /dev/null +++ b/cmd/snap/cmd_find.go @@ -0,0 +1,274 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2018 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" + "errors" + "fmt" + "os" + "sort" + "strings" + + "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/strutil" +) + +var shortFindHelp = i18n.G("Find packages to install") +var longFindHelp = i18n.G(` +The find command queries the store for available packages in the stable channel. + +With the --private flag, which requires the user to be logged-in to the store +(see 'snap help login'), it instead searches for private snaps that the user +has developer access to, either directly or through the store's collaboration +feature. + +A green check mark (given color and unicode support) after a publisher name +indicates that the publisher has been verified. +`) + +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 { + if ret, err := completeFromSortedFile(dirs.SnapSectionsFile, match); err == nil { + return ret + } + + cli := mkClient() + 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 +} + +func cachedSections() (sections []string, err error) { + cachedSections, err := os.Open(dirs.SnapSectionsFile) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + defer cachedSections.Close() + + r := bufio.NewScanner(cachedSections) + for r.Scan() { + sections = append(sections, r.Text()) + } + if r.Err() != nil { + return nil, r.Err() + } + + return sections, nil +} + +func getSections(cli *client.Client) (sections []string, err error) { + // try loading from cached sections file + sections, err = cachedSections() + if err != nil { + return nil, err + } + if sections != nil { + return sections, nil + } + // fallback to listing from the daemon + return cli.Sections() +} + +func showSections(cli *client.Client) error { + sections, err := getSections(cli) + if err != nil { + return err + } + sort.Strings(sections) + + fmt.Fprintf(Stdout, i18n.G("No section specified. Available sections:\n")) + for _, sec := range sections { + fmt.Fprintf(Stdout, " * %s\n", sec) + } + fmt.Fprintf(Stdout, i18n.G("Please try 'snap find --section='\n")) + return nil +} + +type cmdFind struct { + clientMixin + Private bool `long:"private"` + Narrow bool `long:"narrow"` + Section SectionName `long:"section" optional:"true" optional-value:"show-all-sections-please" default:"no-section-specified"` + Positional struct { + Query string + } `positional-args:"yes"` + colorMixin +} + +func init() { + addCommand("find", shortFindHelp, longFindHelp, func() flags.Commander { + return &cmdFind{} + }, colorDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "private": i18n.G("Search private snaps"), + // TRANSLATORS: This should not start with a lowercase letter. + "narrow": i18n.G("Only search for snaps in “stable”"), + // TRANSLATORS: This should not start with a lowercase letter. + "section": i18n.G("Restrict the search to a given section"), + }), []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + }}).alias = "search" + +} + +func (x *cmdFind) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + // LP: 1740605 + if strings.TrimSpace(x.Positional.Query) == "" { + x.Positional.Query = "" + } + + // section will be: + // - "show-all-sections-please" if the user specified --section + // without any argument + // - "no-section-specified" if "--section" was not specified on + // the commandline at all + switch x.Section { + case "show-all-sections-please": + return showSections(x.client) + case "no-section-specified": + x.Section = "" + } + + // magic! `snap find` returns the featured snaps + showFeatured := (x.Positional.Query == "" && x.Section == "") + if showFeatured { + x.Section = "featured" + } + + if x.Section != "" && x.Section != "featured" { + sections, err := cachedSections() + if err != nil { + return err + } + if !strutil.ListContains(sections, string(x.Section)) { + // try the store just in case it was added in the last 24 hours + sections, err = x.client.Sections() + if err != nil { + return err + } + if !strutil.ListContains(sections, string(x.Section)) { + // TRANSLATORS: the %q is the (quoted) name of the section the user entered + return fmt.Errorf(i18n.G("No matching section %q, use --section to list existing sections"), x.Section) + } + } + } + + opts := &client.FindOptions{ + Private: x.Private, + Section: string(x.Section), + Query: x.Positional.Query, + } + + if !x.Narrow { + opts.Scope = "wide" + } + + snaps, resInfo, err := x.client.Find(opts) + if e, ok := err.(*client.Error); ok && (e.Kind == client.ErrorKindNetworkTimeout || e.Kind == client.ErrorKindDNSFailure) { + logger.Debugf("cannot list snaps: %v", e) + return fmt.Errorf("unable to contact snap store") + } + if err != nil { + return err + } + if len(snaps) == 0 { + if x.Section == "" { + // TRANSLATORS: the %q is the (quoted) query the user entered + fmt.Fprintf(Stderr, i18n.G("No matching snaps for %q\n"), opts.Query) + } else { + // TRANSLATORS: the first %q is the (quoted) query, the + // second %q is the (quoted) name of the section the + // user entered + fmt.Fprintf(Stderr, i18n.G("No matching snaps for %q in section %q\n"), opts.Query, x.Section) + } + return nil + } + + // show featured header *after* we checked for errors from the find + if showFeatured { + fmt.Fprint(Stdout, i18n.G("No search term specified. Here are some interesting snaps:\n\n")) + } + + esc := x.getEscapes() + w := tabWriter() + // TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces) + fmt.Fprintf(w, i18n.G("Name\tVersion\tPublisher%s\tNotes\tSummary\n"), fillerPublisher(esc)) + for _, snap := range snaps { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, shortPublisher(esc, snap.Publisher), NotesFromRemote(snap, resInfo), snap.Summary) + } + w.Flush() + if showFeatured { + fmt.Fprint(Stdout, i18n.G("\nProvide a search term for more specific results.\n")) + } + return nil +} diff --git a/cmd/snap/cmd_find_test.go b/cmd/snap/cmd_find_test.go new file mode 100644 index 00000000..8e6de081 --- /dev/null +++ b/cmd/snap/cmd_find_test.go @@ -0,0 +1,594 @@ +// -*- 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" + + "github.com/jessevdk/go-flags" + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" +) + +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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "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", + "publisher": { + "id": "noise-id", + "username": "noise", + "display-name": "Bret", + "validation": "unproven" + }, + "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(snap.Client()).ParseArgs([]string{"find", "hello"}) + + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "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", + "publisher": { + "id": "noise-id", + "username": "noise", + "display-name": "Bret", + "validation": "unproven" + }, + "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, check.HasLen, 2) + c.Check(q.Get("q"), check.Equals, "hello") + c.Check(q.Get("scope"), check.Equals, "wide") + fmt.Fprintln(w, findHelloJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +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, "") +} + +func (s *SnapSuite) TestFindHelloNarrow(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, check.HasLen, 1) + 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(snap.Client()).ParseArgs([]string{"find", "--narrow", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "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", + "license": "Proprietary" + } + ], + "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(snap.Client()).ParseArgs([]string{"find", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "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(snap.Client()).ParseArgs([]string{"find", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +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(snap.Client()).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"}, + }) +} + +const findNetworkTimeoutErrorJSON = ` +{ + "type": "error", + "result": { + "message": "Get https://search.apps.ubuntu.com/api/v1/snaps/search?confinement=strict%2Cclassic&fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha3_384%2Csummary%2Cdescription%2Cdeltas%2Cbinary_filesize%2Cdownload_url%2Cepoch%2Cicon_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ccontact%2Ctitle%2Ccontent%2Cversion%2Corigin%2Cdeveloper_id%2Cprivate%2Cconfinement%2Cchannel_maps_list&q=test: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)", + "kind": "network-timeout" + }, + "status-code": 400 +}` + +func (s *SnapSuite) TestFindNetworkTimeoutError(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, findNetworkTimeoutErrorJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "hello"}) + c.Assert(err, check.ErrorMatches, `unable to contact snap store`) + c.Check(s.Stdout(), check.Equals, "") +} + +func (s *SnapSuite) TestFindSnapSectionOverview(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{"sec2", "sec1"}, + }) + default: + c.Fatalf("expected to get 2 requests, now on #%d", n+1) + } + n++ + }) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section"}) + + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + + c.Check(s.Stdout(), check.Equals, `No section specified. Available sections: + * sec1 + * sec2 +Please try 'snap find --section=' +`) + c.Check(s.Stderr(), check.Equals, "") + + s.ResetStdStreams() +} + +func (s *SnapSuite) TestFindSnapInvalidSection(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/sections") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []string{"sec1"}, + }) + default: + c.Fatalf("expected to get 1 request, now on %d", n+1) + } + + n++ + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=foobar", "hello"}) + c.Assert(err, check.ErrorMatches, `No matching section "foobar", use --section to list existing sections`) +} + +func (s *SnapSuite) TestFindSnapNotFoundInSection(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/sections") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []string{"foobar"}, + }) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + v, ok := r.URL.Query()["section"] + c.Check(ok, check.Equals, true) + c.Check(v, check.DeepEquals, []string{"foobar"}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []string{}, + }) + default: + c.Fatalf("expected to get 2 requests, now on #%d", n+1) + } + n++ + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=foobar", "hello"}) + c.Assert(err, check.IsNil) + c.Check(s.Stderr(), check.Equals, "No matching snaps for \"hello\" in section \"foobar\"\n") + c.Check(s.Stdout(), check.Equals, "") + + s.ResetStdStreams() +} + +func (s *SnapSuite) TestFindSnapCachedSection(c *check.C) { + numHits := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + numHits++ + c.Check(numHits, check.Equals, 1) + c.Check(r.URL.Path, check.Equals, "/v2/sections") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []string{"sec1", "sec2", "sec3"}, + }) + }) + + os.MkdirAll(path.Dir(dirs.SnapSectionsFile), 0755) + ioutil.WriteFile(dirs.SnapSectionsFile, []byte("sec1\nsec2\nsec3"), 0644) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=foobar", "hello"}) + c.Logf("stdout: %s", s.Stdout()) + c.Assert(err, check.ErrorMatches, `No matching section "foobar", use --section to list existing sections`) + + s.ResetStdStreams() + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section"}) + + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + + c.Check(s.Stdout(), check.Equals, `No section specified. Available sections: + * sec1 + * sec2 + * sec3 +Please try 'snap find --section=' +`) + + s.ResetStdStreams() + c.Check(numHits, check.Equals, 1) +} diff --git a/cmd/snap/cmd_first_boot.go b/cmd/snap/cmd_first_boot.go new file mode 100644 index 00000000..2522d99b --- /dev/null +++ b/cmd/snap/cmd_first_boot.go @@ -0,0 +1,50 @@ +// -*- 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", + "The firstboot command is only retained for backwards compatibility.", + 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\n") + return nil +} diff --git a/cmd/snap/cmd_get.go b/cmd/snap/cmd_get.go new file mode 100644 index 00000000..90979fa3 --- /dev/null +++ b/cmd/snap/cmd_get.go @@ -0,0 +1,259 @@ +// -*- 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" + "sort" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +var shortGetHelp = i18n.G("Print 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 { + clientMixin + Positional struct { + Snap installedSnapName `required:"yes"` + Keys []string + } `positional-args:"yes"` + + Typed bool `short:"t"` + Document bool `short:"d"` + List bool `short:"l"` +} + +func init() { + addCommand("get", shortGetHelp, longGetHelp, func() flags.Commander { return &cmdGet{} }, + map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "d": i18n.G("Always return document, even with single key"), + // TRANSLATORS: This should not start with a lowercase letter. + "l": i18n.G("Always return list, even with single key"), + // TRANSLATORS: This should not start with a lowercase letter. + "t": i18n.G("Strict typing with nulls and quoted strings"), + }, []argDesc{ + { + name: "", + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("The snap whose conf is being requested"), + }, + { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Key of interest within the configuration"), + }, + }) +} + +type ConfigValue struct { + Path string + Value interface{} +} + +type byConfigPath []ConfigValue + +func (s byConfigPath) Len() int { return len(s) } +func (s byConfigPath) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byConfigPath) Less(i, j int) bool { + other := s[j].Path + for k, c := range s[i].Path { + if len(other) <= k { + return false + } + + switch { + case c == rune(other[k]): + continue + case c == '.': + return true + case other[k] == '.' || c > rune(other[k]): + return false + } + return true + } + return true +} + +func sortByPath(config []ConfigValue) { + sort.Sort(byConfigPath(config)) +} + +func flattenConfig(cfg map[string]interface{}, root bool) (values []ConfigValue) { + const docstr = "{...}" + for k, v := range cfg { + if input, ok := v.(map[string]interface{}); ok { + if root { + values = append(values, ConfigValue{k, docstr}) + } else { + for kk, vv := range input { + p := k + "." + kk + if _, ok := vv.(map[string]interface{}); ok { + values = append(values, ConfigValue{p, docstr}) + } else { + values = append(values, ConfigValue{p, vv}) + } + } + } + } else { + values = append(values, ConfigValue{k, v}) + } + } + sortByPath(values) + return values +} + +func rootRequested(confKeys []string) bool { + return len(confKeys) == 0 +} + +// outputJson will be used when the user requested "document" output via +// the "-d" commandline switch. +func (c *cmdGet) outputJson(conf interface{}) error { + bytes, err := json.MarshalIndent(conf, "", "\t") + if err != nil { + return err + } + + fmt.Fprintln(Stdout, string(bytes)) + return nil +} + +// outputList will be used when the user requested list output via the +// "-l" commandline switch. +func (x *cmdGet) outputList(conf map[string]interface{}) error { + if rootRequested(x.Positional.Keys) && len(conf) == 0 { + return fmt.Errorf("snap %q has no configuration", x.Positional.Snap) + } + + w := tabWriter() + defer w.Flush() + + fmt.Fprintf(w, "Key\tValue\n") + values := flattenConfig(conf, rootRequested(x.Positional.Keys)) + for _, v := range values { + fmt.Fprintf(w, "%s\t%v\n", v.Path, v.Value) + } + return nil +} + +// outputDefault will be used when no commandline switch to override the +// output where used. The output follows the following rules: +// - a single key with a string value is printed directly +// - multiple keys are printed as a list to the terminal (if there is one) +// or as json if there is no terminal +// - the option "typed" is honored +func (x *cmdGet) outputDefault(conf map[string]interface{}, snapName string, confKeys []string) error { + if rootRequested(confKeys) && len(conf) == 0 { + return fmt.Errorf("snap %q has no configuration", snapName) + } + + var confToPrint interface{} = conf + + if len(confKeys) == 1 { + // if single key was requested, then just output the + // value unless it's a map, in which case it will be + // printed as a list below. + if _, ok := conf[confKeys[0]].(map[string]interface{}); !ok { + confToPrint = conf[confKeys[0]] + } + } + + // conf looks like a map + if cfg, ok := confToPrint.(map[string]interface{}); ok { + if isStdinTTY { + return x.outputList(cfg) + } + + // TODO: remove this conditional and the warning below + // after a transition period. + fmt.Fprintf(Stderr, i18n.G(`WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`)) + return x.outputJson(confToPrint) + } + + if s, ok := confToPrint.(string); ok && !x.Typed { + fmt.Fprintln(Stdout, s) + return nil + } + + if confToPrint != nil || x.Typed { + return x.outputJson(confToPrint) + } + + fmt.Fprintln(Stdout, "") + return nil + +} + +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") + } + + if x.Document && x.List { + return fmt.Errorf("cannot use -d and -l together") + } + + snapName := string(x.Positional.Snap) + confKeys := x.Positional.Keys + + conf, err := x.client.Conf(snapName, confKeys) + if err != nil { + return err + } + + switch { + case x.Document: + return x.outputJson(conf) + case x.List: + return x.outputList(conf) + default: + return x.outputDefault(conf, snapName, confKeys) + } +} diff --git a/cmd/snap/cmd_get_base_declaration.go b/cmd/snap/cmd_get_base_declaration.go new file mode 100644 index 00000000..a3442088 --- /dev/null +++ b/cmd/snap/cmd_get_base_declaration.go @@ -0,0 +1,54 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" +) + +type cmdGetBaseDeclaration struct { + clientMixin +} + +func init() { + cmd := addDebugCommand("get-base-declaration", + "(internal) obtain the base declaration for all interfaces", + "(internal) obtain the base declaration for all interfaces", + func() flags.Commander { + return &cmdGetBaseDeclaration{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdGetBaseDeclaration) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + var resp struct { + BaseDeclaration string `json:"base-declaration"` + } + if err := x.client.Debug("get-base-declaration", nil, &resp); err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", resp.BaseDeclaration) + return nil +} diff --git a/cmd/snap/cmd_get_base_declaration_test.go b/cmd/snap/cmd_get_base_declaration_test.go new file mode 100644 index 00000000..a93a9e9e --- /dev/null +++ b/cmd/snap/cmd_get_base_declaration_test.go @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestGetBaseDeclaration(c *check.C) { + n := 0 + s.RedirectClientToTestServer(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/debug") + c.Check(r.URL.RawQuery, check.Equals, "") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(data, check.DeepEquals, []byte(`{"action":"get-base-declaration"}`)) + fmt.Fprintln(w, `{"type": "sync", "result": {"base-declaration": "hello"}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "get-base-declaration"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "hello\n") + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_get_test.go b/cmd/snap/cmd_get_test.go new file mode 100644 index 00000000..c38f2cc5 --- /dev/null +++ b/cmd/snap/cmd_get_test.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 main_test + +import ( + "fmt" + "net/http" + "strings" + + . "gopkg.in/check.v1" + + snapset "github.com/snapcore/snapd/cmd/snap" +) + +type getCmdArgs struct { + args, stdout, stderr, error string + isTerminal bool +} + +var getTests = []getCmdArgs{{ + 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 -l snapname test-key1", + stdout: "Key Value\ntest-key1 test-value1\n", +}, { + args: "get snapname -l test-key1 test-key2", + stdout: "Key Value\ntest-key1 test-value1\ntest-key2 2\n", +}, { + args: "get snapname document", + stderr: `WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`, + stdout: "{\n\t\"document\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n", +}, { + isTerminal: true, + args: "get snapname document", + stdout: "Key Value\ndocument.key1 value1\ndocument.key2 value2\n", +}, { + args: "get snapname -d test-key1 test-key2", + stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n", +}, { + args: "get snapname -l document", + stdout: "Key Value\ndocument.key1 value1\ndocument.key2 value2\n", +}, { + args: "get -d snapname document", + stdout: "{\n\t\"document\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n", +}, { + args: "get -l snapname", + stdout: "Key Value\nbar 100\nfoo {...}\n", +}, { + args: "get snapname -l test-key3 test-key4", + stdout: "Key Value\ntest-key3.a 1\ntest-key3.b 2\ntest-key3-a 9\ntest-key4.a 3\ntest-key4.b 4\n", +}, { + args: "get -d snapname", + stdout: "{\n\t\"bar\": 100,\n\t\"foo\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n", +}, { + isTerminal: true, + args: "get snapname test-key1 test-key2", + stdout: "Key Value\ntest-key1 test-value1\ntest-key2 2\n", +}, { + isTerminal: false, + args: "get snapname test-key1 test-key2", + stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n", + stderr: `WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`, +}, +} + +func (s *SnapSuite) runTests(cmds []getCmdArgs, c *C) { + for _, test := range cmds { + s.stdout.Truncate(0) + s.stderr.Truncate(0) + + c.Logf("Test: %s", test.args) + + restore := snapset.MockIsStdinTTY(test.isTerminal) + defer restore() + + _, err := snapset.Parser(snapset.Client()).ParseArgs(strings.Fields(test.args)) + if test.error != "" { + c.Check(err, ErrorMatches, test.error) + } else { + c.Check(err, IsNil) + c.Check(s.Stderr(), Equals, test.stderr) + c.Check(s.Stdout(), Equals, test.stdout) + } + } +} + +func (s *SnapSuite) TestSnapGetTests(c *C) { + s.mockGetConfigServer(c) + s.runTests(getTests, c) +} + +var getNoConfigTests = []getCmdArgs{{ + args: "get -l snapname", + error: `snap "snapname" has no configuration`, +}, { + args: "get snapname", + error: `snap "snapname" has no configuration`, +}, { + args: "get -d snapname", + stdout: "{}\n", +}} + +func (s *SnapSuite) TestSnapGetNoConfiguration(c *C) { + s.mockGetEmptyConfigServer(c) + s.runTests(getNoConfigTests, c) +} + +func (s *SnapSuite) TestSortByPath(c *C) { + values := []snapset.ConfigValue{ + {Path: "test-key3.b"}, + {Path: "a"}, + {Path: "test-key3.a"}, + {Path: "a.b.c"}, + {Path: "test-key4.a"}, + {Path: "test-key4.b"}, + {Path: "a-b"}, + {Path: "zzz"}, + {Path: "aa"}, + {Path: "test-key3-a"}, + {Path: "a.b"}, + } + snapset.SortByPath(values) + + expected := []string{ + "a", + "a.b", + "a.b.c", + "a-b", + "aa", + "test-key3.a", + "test-key3.b", + "test-key3-a", + "test-key4.a", + "test-key4.b", + "zzz", + } + + c.Assert(values, HasLen, len(expected)) + + for i, e := range expected { + c.Assert(values[i].Path, Equals, e) + } +} + +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 "test-key3,test-key4": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key3":{"a":1,"b":2},"test-key3-a":9,"test-key4":{"a":3,"b":4}}}`) + case "missing-key": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`) + case "document": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"document":{"key1":"value1","key2":"value2"}}}`) + case "": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"foo":{"key1":"value1","key2":"value2"},"bar":100}}`) + default: + c.Errorf("unexpected keys %q", query.Get("keys")) + } + }) +} + +func (s *SnapSuite) mockGetEmptyConfigServer(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") + + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`) + }) +} diff --git a/cmd/snap/cmd_handle_link.go b/cmd/snap/cmd_handle_link.go new file mode 100644 index 00000000..0201c14a --- /dev/null +++ b/cmd/snap/cmd_handle_link.go @@ -0,0 +1,91 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/userd/ui" +) + +type cmdHandleLink struct { + waitMixin + + Positional struct { + Uri string `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func init() { + cmd := addCommand("handle-link", + i18n.G("Handle a snap:// URI"), + i18n.G("The handle-link command installs the snap-store snap and then invokes it."), + func() flags.Commander { + return &cmdHandleLink{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdHandleLink) ensureSnapStoreInstalled() error { + // If the snap-store snap is installed, our work is done + if _, _, err := x.client.Snap("snap-store"); err == nil { + return nil + } + + dialog, err := ui.New() + if err != nil { + return err + } + answeredYes := dialog.YesNo( + i18n.G("Install snap-aware Snap Store snap?"), + i18n.G("The Snap Store is required to open snaps from a web browser."), + &ui.DialogOptions{ + Timeout: 5 * time.Minute, + Footer: i18n.G("This dialog will close automatically after 5 minutes of inactivity."), + }) + if !answeredYes { + return fmt.Errorf(i18n.G("Snap Store required")) + } + + changeID, err := x.client.Install("snap-store", nil) + if err != nil { + return err + } + _, err = x.wait(changeID) + if err != nil && err != noWait { + return err + } + return nil +} + +func (x *cmdHandleLink) Execute([]string) error { + if err := x.ensureSnapStoreInstalled(); err != nil { + return err + } + + argv := []string{"snap", "run", "snap-store", x.Positional.Uri} + return syscall.Exec("/proc/self/exe", argv, os.Environ()) +} diff --git a/cmd/snap/cmd_help.go b/cmd/snap/cmd_help.go new file mode 100644 index 00000000..1f5ed92b --- /dev/null +++ b/cmd/snap/cmd_help.go @@ -0,0 +1,276 @@ +// -*- 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" + "strings" + "unicode/utf8" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +var shortHelpHelp = i18n.G("Show help about a command") +var longHelpHelp = i18n.G(` +The help command displays information about snap commands. +`) + +// addHelp adds --help like what go-flags would do for us, but hidden +func addHelp(parser *flags.Parser) error { + var help struct { + ShowHelp func() error `short:"h" long:"help"` + } + help.ShowHelp = func() error { + // this function is called via --help (or -h). In that + // case, parser.Command.Active should be the command + // on which help is being requested (like "snap foo + // --help", active is foo), or nil in the toplevel. + if parser.Command.Active == nil { + // toplevel --help will get handled via ErrCommandRequired + return nil + } + // not toplevel, so ask for regular help + return &flags.Error{Type: flags.ErrHelp} + } + hlpgrp, err := parser.AddGroup("Help Options", "", &help) + if err != nil { + return err + } + hlpgrp.Hidden = true + hlp := parser.FindOptionByLongName("help") + hlp.Description = i18n.G("Show this help message") + hlp.Hidden = true + + return nil +} + +type cmdHelp struct { + All bool `long:"all"` + Manpage bool `long:"man" hidden:"true"` + Positional struct { + // TODO: find a way to make Command tab-complete + Sub string `positional-arg-name:""` + } `positional-args:"yes"` + parser *flags.Parser +} + +func init() { + addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} }, + map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "all": i18n.G("Show a short summary of all commands"), + // TRANSLATORS: This should not start with a lowercase letter. + "man": i18n.G("Generate the manpage"), + }, nil) +} + +func (cmd *cmdHelp) setParser(parser *flags.Parser) { + cmd.parser = parser +} + +// manfixer is a hackish way to get the generated manpage into section 8 +// (go-flags doesn't have an option for this; I'll be proposing something +// there soon, but still waiting on some other PRs to make it through) +type manfixer struct { + done bool +} + +func (w *manfixer) Write(buf []byte) (int, error) { + if !w.done { + w.done = true + if bytes.HasPrefix(buf, []byte(".TH snap 1 ")) { + // io.Writer.Write must not modify the buffer, even temporarily + n, _ := Stdout.Write(buf[:9]) + Stdout.Write([]byte{'8'}) + m, err := Stdout.Write(buf[10:]) + return n + m + 1, err + } + } + return Stdout.Write(buf) +} + +func (cmd cmdHelp) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + if cmd.Manpage { + // you shouldn't try to to combine --man with --all nor a + // subcommand, but --man is hidden so no real need to check. + cmd.parser.WriteManPage(&manfixer{}) + return nil + } + if cmd.All { + if cmd.Positional.Sub != "" { + return fmt.Errorf(i18n.G("help accepts a command, or '--all', but not both.")) + } + printLongHelp(cmd.parser) + return nil + } + + if cmd.Positional.Sub != "" { + subcmd := cmd.parser.Find(cmd.Positional.Sub) + if subcmd == nil { + return fmt.Errorf(i18n.G("Unknown command %q. Try 'snap help'."), cmd.Positional.Sub) + } + // this makes "snap help foo" work the same as "snap foo --help" + cmd.parser.Command.Active = subcmd + return &flags.Error{Type: flags.ErrHelp} + } + + return &flags.Error{Type: flags.ErrCommandRequired} +} + +type helpCategory struct { + Label string + Description string + Commands []string +} + +// helpCategories helps us by grouping commands +var helpCategories = []helpCategory{ + { + Label: i18n.G("Basics"), + Description: i18n.G("basic snap management"), + Commands: []string{"find", "info", "install", "list", "remove"}, + }, { + Label: i18n.G("...more"), + Description: i18n.G("slightly more advanced snap management"), + Commands: []string{"refresh", "revert", "switch", "disable", "enable"}, + }, { + Label: i18n.G("History"), + Description: i18n.G("manage system change transactions"), + Commands: []string{"changes", "tasks", "abort", "watch"}, + }, { + Label: i18n.G("Daemons"), + Description: i18n.G("manage services"), + Commands: []string{"services", "start", "stop", "restart", "logs"}, + }, { + Label: i18n.G("Commands"), + Description: i18n.G("manage aliases"), + Commands: []string{"alias", "aliases", "unalias", "prefer"}, + }, { + Label: i18n.G("Configuration"), + Description: i18n.G("system administration and configuration"), + Commands: []string{"get", "set", "wait"}, + }, { + Label: i18n.G("Account"), + Description: i18n.G("authentication to snapd and the snap store"), + Commands: []string{"login", "logout", "whoami"}, + }, { + Label: i18n.G("Permissions"), + Description: i18n.G("manage permissions"), + Commands: []string{"interfaces", "interface", "connect", "disconnect"}, + }, { + Label: i18n.G("Snapshots"), + Description: i18n.G("archives of snap data"), + Commands: []string{"saved", "save", "check-snapshot", "restore", "forget"}, + }, { + Label: i18n.G("Other"), + Description: i18n.G("miscellanea"), + Commands: []string{"version", "warnings", "okay"}, + }, { + Label: i18n.G("Development"), + Description: i18n.G("developer-oriented features"), + Commands: []string{"run", "pack", "try", "ack", "known", "download"}, + }, +} + +var ( + longSnapDescription = strings.TrimSpace(i18n.G(` +The snap command lets you install, configure, refresh and remove snaps. +Snaps are packages that work across many different Linux distributions, +enabling secure delivery and operation of the latest apps and utilities. +`)) + snapUsage = i18n.G("Usage: snap [...]") + snapHelpCategoriesIntro = i18n.G("Commands can be classified as follows:") + snapHelpAllFooter = i18n.G("For more information about a command, run 'snap help '.") + snapHelpFooter = i18n.G("For a short summary of all commands, run 'snap help --all'.") +) + +func printHelpHeader() { + fmt.Fprintln(Stdout, longSnapDescription) + fmt.Fprintln(Stdout) + fmt.Fprintln(Stdout, snapUsage) + fmt.Fprintln(Stdout) + fmt.Fprintln(Stdout, snapHelpCategoriesIntro) + fmt.Fprintln(Stdout) +} + +func printHelpAllFooter() { + fmt.Fprintln(Stdout) + fmt.Fprintln(Stdout, snapHelpAllFooter) +} + +func printHelpFooter() { + printHelpAllFooter() + fmt.Fprintln(Stdout, snapHelpFooter) +} + +// this is called when the Execute returns a flags.Error with ErrCommandRequired +func printShortHelp() { + printHelpHeader() + maxLen := 0 + for _, categ := range helpCategories { + if l := utf8.RuneCountInString(categ.Label); l > maxLen { + maxLen = l + } + } + for _, categ := range helpCategories { + fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, categ.Label, strings.Join(categ.Commands, ", ")) + } + printHelpFooter() +} + +// this is "snap help --all" +func printLongHelp(parser *flags.Parser) { + printHelpHeader() + maxLen := 0 + for _, categ := range helpCategories { + for _, command := range categ.Commands { + if l := len(command); l > maxLen { + maxLen = l + } + } + } + + // flags doesn't have a LookupCommand? + commands := parser.Commands() + cmdLookup := make(map[string]*flags.Command, len(commands)) + for _, cmd := range commands { + cmdLookup[cmd.Name] = cmd + } + + for _, categ := range helpCategories { + fmt.Fprintln(Stdout) + fmt.Fprintf(Stdout, " %s (%s):\n", categ.Label, categ.Description) + for _, name := range categ.Commands { + cmd := cmdLookup[name] + if cmd == nil { + fmt.Fprintf(Stderr, "??? Cannot find command %q mentioned in help categories, please report!\n", name) + } else { + fmt.Fprintf(Stdout, " %*s %s\n", -maxLen, name, cmd.ShortDescription) + } + } + } + printHelpAllFooter() +} diff --git a/cmd/snap/cmd_help_test.go b/cmd/snap/cmd_help_test.go new file mode 100644 index 00000000..402238ea --- /dev/null +++ b/cmd/snap/cmd_help_test.go @@ -0,0 +1,176 @@ +// -*- 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" + "fmt" + "os" + "regexp" + "strings" + + "github.com/jessevdk/go-flags" + "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"}, + {"snap", "help"}, + {"snap", "--help"}, + {"snap", "-h"}, + } { + s.ResetStdStreams() + + os.Args = cmdLine + comment := check.Commentf("%q", cmdLine) + + err := snap.RunMain() + c.Assert(err, check.IsNil, comment) + c.Check(s.Stdout(), check.Matches, "(?s)"+strings.Join([]string{ + snap.LongSnapDescription, + "", + regexp.QuoteMeta(snap.SnapUsage), + "", ".*", "", + snap.SnapHelpAllFooter, + snap.SnapHelpFooter, + }, "\n")+`\s*`, comment) + c.Check(s.Stderr(), check.Equals, "", comment) + } +} + +func (s *SnapSuite) TestHelpAllPrintsLongHelp(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + + os.Args = []string{"snap", "help", "--all"} + + err := snap.RunMain() + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, "(?sm)"+strings.Join([]string{ + snap.LongSnapDescription, + "", + regexp.QuoteMeta(snap.SnapUsage), + "", + snap.SnapHelpCategoriesIntro, + "", ".*", "", + snap.SnapHelpAllFooter, + }, "\n")+`\s*`) + c.Check(s.Stderr(), check.Equals, "") +} + +func nonHiddenCommands() map[string]bool { + parser := snap.Parser(snap.Client()) + commands := parser.Commands() + names := make(map[string]bool, len(commands)) + for _, cmd := range commands { + if cmd.Hidden { + continue + } + names[cmd.Name] = true + } + return names +} + +func (s *SnapSuite) testSubCommandHelp(c *check.C, sub, expected string) { + parser := snap.Parser(snap.Client()) + rest, err := parser.ParseArgs([]string{sub, "--help"}) + c.Assert(err, check.DeepEquals, &flags.Error{Type: flags.ErrHelp}) + c.Assert(rest, check.HasLen, 0) + var buf bytes.Buffer + parser.WriteHelp(&buf) + c.Check(buf.String(), check.Equals, expected) +} + +func (s *SnapSuite) TestSubCommandHelpPrintsHelp(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + + for cmd := range nonHiddenCommands() { + s.ResetStdStreams() + os.Args = []string{"snap", cmd, "--help"} + + err := snap.RunMain() + comment := check.Commentf("%q", cmd) + c.Assert(err, check.IsNil, comment) + // regexp matches "Usage: snap " plus an arbitrary + // number of [] plus an arbitrary number of + // <> optionally ending in ellipsis + c.Check(s.Stdout(), check.Matches, fmt.Sprintf(`(?sm)Usage:\s+snap %s(?: \[[^][]+\])*(?:(?: <[^<>]+>)+(?:\.\.\.)?)?$.*`, cmd), comment) + c.Check(s.Stderr(), check.Equals, "", comment) + } +} + +func (s *SnapSuite) TestHelpCategories(c *check.C) { + // non-hidden commands that are not expected to appear in the help summary + excluded := []string{ + "help", + } + all := nonHiddenCommands() + categorised := make(map[string]bool, len(all)+len(excluded)) + for _, cmd := range excluded { + categorised[cmd] = true + } + seen := make(map[string]string, len(all)) + for _, categ := range snap.HelpCategories { + for _, cmd := range categ.Commands { + categorised[cmd] = true + if seen[cmd] != "" { + c.Errorf("duplicated: %q in %q and %q", cmd, seen[cmd], categ.Label) + } + seen[cmd] = categ.Label + } + } + for cmd := range all { + if !categorised[cmd] { + c.Errorf("uncategorised: %q", cmd) + } + } + for cmd := range categorised { + if !all[cmd] { + c.Errorf("unknown (hidden?): %q", cmd) + } + } +} + +func (s *SnapSuite) TestHelpCommandAllFails(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + os.Args = []string{"snap", "help", "interfaces", "--all"} + + err := snap.RunMain() + c.Assert(err, check.ErrorMatches, "help accepts a command, or '--all', but not both.") +} + +func (s *SnapSuite) TestManpageInSection8(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + os.Args = []string{"snap", "help", "--man"} + + err := snap.RunMain() + c.Assert(err, check.IsNil) + + c.Check(s.Stdout(), check.Matches, `\.TH snap 8 (?s).*`) +} diff --git a/cmd/snap/cmd_info.go b/cmd/snap/cmd_info.go new file mode 100644 index 00000000..74d7eca9 --- /dev/null +++ b/cmd/snap/cmd_info.go @@ -0,0 +1,511 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2018 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" + "path/filepath" + "strings" + "text/tabwriter" + "time" + "unicode" + "unicode/utf8" + + "github.com/jessevdk/go-flags" + "gopkg.in/yaml.v2" + + "github.com/snapcore/snapd/asserts" + "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 { + clientMixin + colorMixin + timeMixin + + 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 snaps") +var longInfoHelp = i18n.G(` +The info command shows detailed information about snaps. + +The snaps can be specified by name or by path; names are looked for both in the +store and in the installed snaps; paths can refer to a .snap file, or to a +directory that contains an unpacked snap suitable for 'snap try' (an example +of this would be the 'prime' directory snapcraft produces). +`) + +func init() { + addCommand("info", + shortInfoHelp, + longInfoHelp, + func() flags.Commander { + return &infoCmd{} + }, colorDescs.also(timeDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "verbose": i18n.G("Include more details on the snap (expanded notes, base, etc.)"), + }), nil) +} + +func norm(path string) string { + path = filepath.Clean(path) + if osutil.IsDirectory(path) { + path = path + "/" + } + + return path +} + +func maybePrintPrice(w io.Writer, snap *client.Snap, resInfo *client.ResultInfo) { + if resInfo == nil { + return + } + price, currency, err := getPrice(snap.Prices, resInfo.SuggestedCurrency) + if err != nil { + return + } + fmt.Fprintf(w, "price:\t%s\n", formatPrice(price, currency)) +} + +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 maybePrintID(w io.Writer, snap *client.Snap) { + if snap.ID != "" { + fmt.Fprintf(w, "snap-id:\t%s\n", snap.ID) + } +} + +func maybePrintBase(w io.Writer, base string, verbose bool) { + if verbose && base != "" { + fmt.Fprintf(w, "base:\t%s\n", base) + } +} + +func tryDirect(w io.Writer, path string, verbose bool) bool { + path = norm(path) + + snapf, err := snap.Open(path) + if err != nil { + return false + } + + var sha3_384 string + if verbose && !osutil.IsDirectory(path) { + var err error + sha3_384, _, err = asserts.SnapFileSHA3_384(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.InstanceName()) + fmt.Fprintf(w, "summary:\t%s\n", formatSummary(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)) + maybePrintBase(w, info.Base, verbose) + if sha3_384 != "" { + fmt.Fprintf(w, "sha3-384:\t%s\n", sha3_384) + } + + return true +} + +func coalesce(snaps ...*client.Snap) *client.Snap { + for _, s := range snaps { + if s != nil { + return s + } + } + return nil +} + +// runesTrimRightSpace returns text, with any trailing whitespace dropped. +func runesTrimRightSpace(text []rune) []rune { + j := len(text) + for j > 0 && unicode.IsSpace(text[j-1]) { + j-- + } + return text[:j] +} + +// runesLastIndexSpace returns the index of the last whitespace rune +// in the text. If the text has no whitespace, returns -1. +func runesLastIndexSpace(text []rune) int { + for i := len(text) - 1; i >= 0; i-- { + if unicode.IsSpace(text[i]) { + return i + } + } + return -1 +} + +// wrapLine wraps a line to fit into width, preserving the line's indent, and +// writes it out prepending padding to each line. +func wrapLine(out io.Writer, text []rune, pad string, width int) error { + // Note: this is _wrong_ for much of unicode (because the width of a rune on + // the terminal is anything between 0 and 2, not always 1 as this code + // assumes) but fixing that is Hard. Long story short, you can get close + // using a couple of big unicode tables (which is what wcwidth + // does). Getting it 100% requires a terminfo-alike of unicode behaviour. + // However, before this we'd count bytes instead of runes, so we'd be + // even more broken. Think of it as successive approximations... at least + // with this work we share tabwriter's opinion on the width of things! + + // This (and possibly printDescr below) should move to strutil once + // we're happy with it getting wider (heh heh) use. + + // discard any trailing whitespace + text = runesTrimRightSpace(text) + // establish the indent of the whole block + idx := 0 + for idx < len(text) && unicode.IsSpace(text[idx]) { + idx++ + } + indent := pad + string(text[:idx]) + text = text[idx:] + width -= idx + utf8.RuneCountInString(pad) + var err error + for len(text) > width && err == nil { + // find a good place to chop the text + idx = runesLastIndexSpace(text[:width+1]) + if idx < 0 { + // there's no whitespace; just chop at line width + idx = width + } + _, err = fmt.Fprint(out, indent, string(text[:idx]), "\n") + // prune any remaining whitespace before the start of the next line + for idx < len(text) && unicode.IsSpace(text[idx]) { + idx++ + } + text = text[idx:] + } + if err != nil { + return err + } + _, err = fmt.Fprint(out, indent, string(text), "\n") + return err +} + +// printDescr formats a given string (typically a snap description) +// in a user friendly way. +// +// The rules are (intentionally) very simple: +// - trim trailing whitespace +// - word wrap at "max" chars preserving line indent +// - keep \n intact and break there +func printDescr(w io.Writer, descr string, max int) error { + var err error + descr = strings.TrimRightFunc(descr, unicode.IsSpace) + for _, line := range strings.Split(descr, "\n") { + err = wrapLine(w, []rune(line), " ", max) + if err != nil { + break + } + } + return err +} + +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.IsService() { + continue + } + + cmdStr := snap.JoinSnapApp(snapName, app.Name) + 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 maybePrintServices(w io.Writer, snapName string, allApps []client.AppInfo, n int) { + if len(allApps) == 0 { + return + } + + services := make([]string, 0, len(allApps)) + for _, app := range allApps { + if !app.IsService() { + continue + } + + var active, enabled string + if app.Active { + active = "active" + } else { + active = "inactive" + } + if app.Enabled { + enabled = "enabled" + } else { + enabled = "disabled" + } + services = append(services, fmt.Sprintf(" %s:\t%s, %s, %s", snap.JoinSnapApp(snapName, app.Name), app.Daemon, enabled, active)) + } + if len(services) == 0 { + return + } + + fmt.Fprintf(w, "services:\n") + for _, svc := range services { + fmt.Fprintln(w, svc) + } +} + +var channelRisks = []string{"stable", "candidate", "beta", "edge"} + +// displayChannels displays channels and tracks in the right order +func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap, revLen, sizeLen int) (maxRevLen, maxSizeLen int) { + fmt.Fprintln(w, "channels:") + + releasedfmt := "2006-01-02" + if x.AbsTime { + releasedfmt = time.RFC3339 + } + + type chInfoT struct { + name, version, released, revision, size, notes string + } + var chInfos []*chInfoT + maxRevLen, maxSizeLen = revLen, sizeLen + + // order by tracks + for _, tr := range remote.Tracks { + trackHasOpenChannel := false + for _, risk := range channelRisks { + chName := fmt.Sprintf("%s/%s", tr, risk) + ch, ok := remote.Channels[chName] + if tr == "latest" { + chName = risk + } + chInfo := chInfoT{name: chName} + if ok { + chInfo.version = ch.Version + chInfo.revision = fmt.Sprintf("(%s)", ch.Revision) + if len(chInfo.revision) > maxRevLen { + maxRevLen = len(chInfo.revision) + } + chInfo.released = ch.ReleasedAt.Format(releasedfmt) + chInfo.size = strutil.SizeToStr(ch.Size) + if len(chInfo.size) > maxSizeLen { + maxSizeLen = len(chInfo.size) + } + chInfo.notes = NotesFromChannelSnapInfo(ch).String() + trackHasOpenChannel = true + } else { + if trackHasOpenChannel { + chInfo.version = esc.uparrow + } else { + chInfo.version = esc.dash + } + } + chInfos = append(chInfos, &chInfo) + } + } + + for _, chInfo := range chInfos { + fmt.Fprintf(w, " "+chantpl, chInfo.name, chInfo.version, chInfo.released, maxRevLen, chInfo.revision, maxSizeLen, chInfo.size, chInfo.notes) + } + + return maxRevLen, maxSizeLen +} + +func formatSummary(raw string) string { + s, err := yaml.Marshal(raw) + if err != nil { + return fmt.Sprintf("cannot marshal summary: %s", err) + } + return strings.TrimSpace(string(s)) +} + +func (x *infoCmd) Execute([]string) error { + termWidth, _ := termSize() + termWidth -= 3 + if termWidth > 100 { + // any wider than this and it gets hard to read + termWidth = 100 + } + + esc := x.getEscapes() + 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 snapName == "system" { + fmt.Fprintln(w, "system: You can't have it.") + continue + } + + if tryDirect(w, snapName, x.Verbose) { + noneOK = false + continue + } + remote, resInfo, _ := x.client.FindOne(snapName) + local, _, _ := x.client.Snap(snapName) + + both := coalesce(local, remote) + + if both == nil { + if len(x.Positional.Snaps) == 1 { + return fmt.Errorf("no snap found for %q", snapName) + } + + fmt.Fprintf(w, fmt.Sprintf(i18n.G("warning:\tno snap found for %q\n"), snapName)) + continue + } + noneOK = false + + fmt.Fprintf(w, "name:\t%s\n", both.Name) + fmt.Fprintf(w, "summary:\t%s\n", formatSummary(both.Summary)) + fmt.Fprintf(w, "publisher:\t%s\n", longPublisher(esc, both.Publisher)) + if both.Contact != "" { + fmt.Fprintf(w, "contact:\t%s\n", strings.TrimPrefix(both.Contact, "mailto:")) + } + license := both.License + if license == "" { + license = "unset" + } + fmt.Fprintf(w, "license:\t%s\n", license) + maybePrintPrice(w, remote, resInfo) + fmt.Fprintln(w, "description: |") + printDescr(w, both.Description, termWidth) + maybePrintCommands(w, snapName, both.Apps, termWidth) + maybePrintServices(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) + } + + var notes *Notes + if local != nil { + 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) + } + + fmt.Fprintf(w, " ignore-validation:\t%t\n", local.IgnoreValidation) + } else { + notes = NotesFromLocal(local) + } + } + // stops the notes etc trying to be aligned with channels + w.Flush() + maybePrintType(w, both.Type) + maybePrintBase(w, both.Base, x.Verbose) + maybePrintID(w, both) + var localRev, localSize string + var revLen, sizeLen int + if local != nil { + if local.TrackingChannel != "" { + fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel) + } + if !local.InstallDate.IsZero() { + fmt.Fprintf(w, "refresh-date:\t%s\n", x.fmtTime(local.InstallDate)) + } + localRev = fmt.Sprintf("(%s)", local.Revision) + revLen = len(localRev) + localSize = strutil.SizeToStr(local.InstalledSize) + sizeLen = len(localSize) + } + + chantpl := "%s:\t%s %s%*s %*s %s\n" + if remote != nil && remote.Channels != nil && remote.Tracks != nil { + chantpl = "%s:\t%s\t%s\t%*s\t%*s\t%s\n" + + w.Flush() + revLen, sizeLen = x.displayChannels(w, chantpl, esc, remote, revLen, sizeLen) + } + if local != nil { + fmt.Fprintf(w, chantpl, + "installed", local.Version, "", revLen, localRev, sizeLen, localSize, notes) + } + + } + w.Flush() + + if noneOK { + return fmt.Errorf(i18n.G("no valid snaps given")) + } + + return nil +} diff --git a/cmd/snap/cmd_info_test.go b/cmd/snap/cmd_info_test.go new file mode 100644 index 00000000..185fd7df --- /dev/null +++ b/cmd/snap/cmd_info_test.go @@ -0,0 +1,552 @@ +// -*- 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" + "fmt" + "net/http" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +var cmdAppInfos = []client.AppInfo{{Name: "app1"}, {Name: "app2"}} +var svcAppInfos = []client.AppInfo{ + { + Name: "svc1", + Daemon: "simple", + Enabled: false, + Active: true, + }, + { + Name: "svc2", + Daemon: "simple", + Enabled: true, + Active: false, + }, +} + +var mixedAppInfos = append(append([]client.AppInfo(nil), cmdAppInfos...), svcAppInfos...) + +type infoSuite struct { + BaseSnapSuite +} + +var _ = check.Suite(&infoSuite{}) + +func (s *infoSuite) TestMaybePrintServices(c *check.C) { + for _, infos := range [][]client.AppInfo{svcAppInfos, mixedAppInfos} { + var buf bytes.Buffer + snap.MaybePrintServices(&buf, "foo", infos, -1) + + c.Check(buf.String(), check.Equals, `services: + foo.svc1: simple, disabled, active + foo.svc2: simple, enabled, inactive +`) + } +} + +func (s *infoSuite) TestMaybePrintServicesNoServices(c *check.C) { + for _, infos := range [][]client.AppInfo{cmdAppInfos, nil} { + var buf bytes.Buffer + snap.MaybePrintServices(&buf, "foo", infos, -1) + + c.Check(buf.String(), check.Equals, "") + } +} + +func (s *infoSuite) TestMaybePrintCommands(c *check.C) { + for _, infos := range [][]client.AppInfo{cmdAppInfos, mixedAppInfos} { + var buf bytes.Buffer + snap.MaybePrintCommands(&buf, "foo", infos, -1) + + c.Check(buf.String(), check.Equals, `commands: + - foo.app1 + - foo.app2 +`) + } +} + +func (s *infoSuite) TestMaybePrintCommandsNoCommands(c *check.C) { + for _, infos := range [][]client.AppInfo{svcAppInfos, nil} { + var buf bytes.Buffer + snap.MaybePrintCommands(&buf, "foo", infos, -1) + + c.Check(buf.String(), check.Equals, "") + } +} + +func (s *infoSuite) TestInfoPriced(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) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, "{}") + default: + c.Fatalf("expected to get 1 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: GNU Hello, the "hello world" snap +publisher: Canonical* +license: Proprietary +price: 1.99GBP +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const mockInfoJSON = ` +{ + "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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "The GNU Hello snap", + "type": "app", + "version": "2.10", + "license": "MIT" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +const mockInfoJSONWithChannels = ` +{ + "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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "The GNU Hello snap", + "type": "app", + "version": "2.10", + "license": "MIT", + "channels": { + "1/stable": { + "revision": "1", + "version": "2.10", + "channel": "1/stable", + "size": 65536, + "released-at": "2018-12-18T15:16:56.723501Z" + } + }, + "tracks": ["1"] + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *infoSuite) TestInfoUnquoted(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, mockInfoJSON) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, "{}") + default: + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical* +license: MIT +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const mockInfoJSONOtherLicense = ` +{ + "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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "install-date": "2006-01-02T22:04:07.123456789Z", + "installed-size": 1024, + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "The GNU Hello snap", + "type": "app", + "version": "2.10", + "license": "BSD-3", + "tracking-channel": "beta" + } +} +` +const mockInfoJSONNoLicense = ` +{ + "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", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "install-date": "2006-01-02T22:04:07.123456789Z", + "installed-size": 1024, + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "100", + "status": "available", + "summary": "The GNU Hello snap", + "type": "app", + "version": "2.10", + "license": "", + "tracking-channel": "beta" + } +} +` + +func (s *infoSuite) TestInfoWithLocalDifferentLicense(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, mockInfoJSON) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, mockInfoJSONOtherLicense) + default: + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--abs-time", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical* +license: BSD-3 +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02T22:04:07Z +installed: 2.10 (1) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *infoSuite) TestInfoWithLocalNoLicense(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, mockInfoJSON) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, mockInfoJSONNoLicense) + default: + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--abs-time", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical* +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02T22:04:07Z +installed: 2.10 (100) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *infoSuite) TestInfoWithChannelsAndLocal(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0, 2, 4: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + fmt.Fprintln(w, mockInfoJSONWithChannels) + case 1, 3, 5: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, mockInfoJSONNoLicense) + default: + c.Fatalf("expected to get 6 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--abs-time", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical* +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02T22:04:07Z +channels: + 1/stable: 2.10 2018-12-18T15:16:56Z (1) 65kB - + 1/candidate: ^ + 1/beta: ^ + 1/edge: ^ +installed: 2.10 (100) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 2) + + // now the same but without abs-time + s.ResetStdStreams() + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical* +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02 +channels: + 1/stable: 2.10 2018-12-18 (1) 65kB - + 1/candidate: ^ + 1/beta: ^ + 1/edge: ^ +installed: 2.10 (100) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 4) + + // now the same but with unicode on + s.ResetStdStreams() + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"info", "--unicode=always", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical✓ +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02 +channels: + 1/stable: 2.10 2018-12-18 (1) 65kB - + 1/candidate: ↑ + 1/beta: ↑ + 1/edge: ↑ +installed: 2.10 (100) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 6) +} + +func (s *infoSuite) TestInfoHumanTimes(c *check.C) { + // checks that tiemutil.Human is called when no --abs-time is given + restore := snap.MockTimeutilHuman(func(time.Time) string { return "TOTALLY NOT A ROBOT" }) + defer restore() + + 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, "{}") + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, mockInfoJSONNoLicense) + default: + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical* +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: TOTALLY NOT A ROBOT +installed: 2.10 (100) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (infoSuite) TestDescr(c *check.C) { + for k, v := range map[string]string{ + "": " \n", + `one: + * two three four five six + * seven height nine ten +`: ` one: + * two three four + five six + * seven height + nine ten +`, + "abcdefghijklm nopqrstuvwxyz ABCDEFGHIJKLMNOPQR STUVWXYZ": ` + abcdefghijklm + nopqrstuvwxyz + ABCDEFGHIJKLMNOPQR + STUVWXYZ +`[1:], + // not much we can do when it won't fit + "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ": ` + abcdefghijklmnopqr + stuvwxyz + ABCDEFGHIJKLMNOPQR + STUVWXYZ +`[1:], + } { + var buf bytes.Buffer + snap.PrintDescr(&buf, k, 20) + c.Check(buf.String(), check.Equals, v, check.Commentf("%q", k)) + } +} diff --git a/cmd/snap/cmd_interface.go b/cmd/snap/cmd_interface.go new file mode 100644 index 00000000..e5a5270f --- /dev/null +++ b/cmd/snap/cmd_interface.go @@ -0,0 +1,190 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + "io" + "sort" + "text/tabwriter" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdInterface struct { + clientMixin + ShowAttrs bool `long:"attrs"` + ShowAll bool `long:"all"` + Positionals struct { + Interface interfaceName `skip-help:"true"` + } `positional-args:"true"` +} + +var shortInterfaceHelp = i18n.G("Show details of snap interfaces") +var longInterfaceHelp = i18n.G(` +The interface command shows details of snap interfaces. + +If no interface name is provided, a list of interface names with at least +one connection is shown, or a list of all interfaces if --all is provided. +`) + +func init() { + addCommand("interface", shortInterfaceHelp, longInterfaceHelp, func() flags.Commander { + return &cmdInterface{} + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "attrs": i18n.G("Show interface attributes"), + // TRANSLATORS: This should not start with a lowercase letter. + "all": i18n.G("Include unused interfaces"), + }, []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Show details of a specific interface"), + }}) +} + +func (x *cmdInterface) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if x.Positionals.Interface != "" { + // Show one interface in detail. + name := string(x.Positionals.Interface) + ifaces, err := x.client.Interfaces(&client.InterfaceOptions{ + Names: []string{name}, + Doc: true, + Plugs: true, + Slots: true, + }) + if err != nil { + return err + } + if len(ifaces) == 0 { + return fmt.Errorf(i18n.G("no such interface")) + } + x.showOneInterface(ifaces[0]) + } else { + // Show an overview of available interfaces. + ifaces, err := x.client.Interfaces(&client.InterfaceOptions{ + Connected: !x.ShowAll, + }) + if err != nil { + return err + } + if len(ifaces) == 0 { + if x.ShowAll { + return fmt.Errorf(i18n.G("no interfaces found")) + } + return fmt.Errorf(i18n.G("no interfaces currently connected")) + } + x.showManyInterfaces(ifaces) + } + return nil +} + +func (x *cmdInterface) showOneInterface(iface *client.Interface) { + w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) + defer w.Flush() + + fmt.Fprintf(w, "name:\t%s\n", iface.Name) + if iface.Summary != "" { + fmt.Fprintf(w, "summary:\t%s\n", iface.Summary) + } + if iface.DocURL != "" { + fmt.Fprintf(w, "documentation:\t%s\n", iface.DocURL) + } + if len(iface.Plugs) > 0 { + fmt.Fprintf(w, "plugs:\n") + for _, plug := range iface.Plugs { + var labelPart string + if plug.Label != "" { + labelPart = fmt.Sprintf(" (%s)", plug.Label) + } + if plug.Name == iface.Name { + fmt.Fprintf(w, " - %s%s", plug.Snap, labelPart) + } else { + fmt.Fprintf(w, ` - %s:%s%s`, plug.Snap, plug.Name, labelPart) + } + // Print a colon which will make the snap:plug element a key-value + // yaml object so that we can write the attributes. + if len(plug.Attrs) > 0 && x.ShowAttrs { + fmt.Fprintf(w, ":\n") + x.showAttrs(w, plug.Attrs, " ") + } else { + fmt.Fprintf(w, "\n") + } + } + } + if len(iface.Slots) > 0 { + fmt.Fprintf(w, "slots:\n") + for _, slot := range iface.Slots { + var labelPart string + if slot.Label != "" { + labelPart = fmt.Sprintf(" (%s)", slot.Label) + } + if slot.Name == iface.Name { + fmt.Fprintf(w, " - %s%s", slot.Snap, labelPart) + } else { + fmt.Fprintf(w, ` - %s:%s%s`, slot.Snap, slot.Name, labelPart) + } + // Print a colon which will make the snap:slot element a key-value + // yaml object so that we can write the attributes. + if len(slot.Attrs) > 0 && x.ShowAttrs { + fmt.Fprintf(w, ":\n") + x.showAttrs(w, slot.Attrs, " ") + } else { + fmt.Fprintf(w, "\n") + } + } + } +} + +func (x *cmdInterface) showManyInterfaces(infos []*client.Interface) { + w := tabWriter() + defer w.Flush() + fmt.Fprintln(w, i18n.G("Name\tSummary")) + for _, iface := range infos { + fmt.Fprintf(w, "%s\t%s\n", iface.Name, iface.Summary) + } +} + +func (x *cmdInterface) showAttrs(w io.Writer, attrs map[string]interface{}, indent string) { + if len(attrs) == 0 { + return + } + names := make([]string, 0, len(attrs)) + for name := range attrs { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + value := attrs[name] + switch value.(type) { + case string, bool, json.Number: + fmt.Fprintf(w, "%s %s:\t%v\n", indent, name, value) + } + } +} diff --git a/cmd/snap/cmd_interface_test.go b/cmd/snap/cmd_interface_test.go new file mode 100644 index 00000000..b5e98d55 --- /dev/null +++ b/cmd/snap/cmd_interface_test.go @@ -0,0 +1,287 @@ +// -*- 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" + "os" + + "github.com/jessevdk/go-flags" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestInterfaceHelp(c *C) { + msg := `Usage: + snap.test interface [interface-OPTIONS] [] + +The interface command shows details of snap interfaces. + +If no interface name is provided, a list of interface names with at least +one connection is shown, or a list of all interfaces if --all is provided. + +[interface command options] + --attrs Show interface attributes + --all Include unused interfaces + +[interface command arguments] + : Show details of a specific interface +` + s.testSubCommandHelp(c, "interface", msg) +} + +func (s *SnapSuite) TestInterfaceListEmpty(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") + c.Check(r.URL.RawQuery, Equals, "select=connected") + 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.Interface{}, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"interface"}) + c.Assert(err, ErrorMatches, "no interfaces currently connected") + c.Assert(rest, DeepEquals, []string{"interface"}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceListAllEmpty(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") + c.Check(r.URL.RawQuery, Equals, "select=all") + 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.Interface{}, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"interface", "--all"}) + c.Assert(err, ErrorMatches, "no interfaces found") + c.Assert(rest, DeepEquals, []string{"--all"}) // XXX: feels like a bug in go-flags. + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceList(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") + c.Check(r.URL.RawQuery, Equals, "select=connected") + 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.Interface{{ + Name: "network", + Summary: "allows access to the network", + }, { + Name: "network-bind", + Summary: "allows providing services on the network", + }}, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"interface"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Name Summary\n" + + "network allows access to the network\n" + + "network-bind allows providing services on the network\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceListAll(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") + c.Check(r.URL.RawQuery, Equals, "select=all") + 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.Interface{{ + Name: "network", + Summary: "allows access to the network", + }, { + Name: "network-bind", + Summary: "allows providing services on the network", + }, { + Name: "unused", + Summary: "just an unused interface, nothing to see here", + }}, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"interface", "--all"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Name Summary\n" + + "network allows access to the network\n" + + "network-bind allows providing services on the network\n" + + "unused just an unused interface, nothing to see here\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceDetails(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") + c.Check(r.URL.RawQuery, Equals, "doc=true&names=network&plugs=true&select=all&slots=true") + 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.Interface{{ + Name: "network", + Summary: "allows access to the network", + DocURL: "http://example.org/about-the-network-interface", + Plugs: []client.Plug{ + {Snap: "deepin-music", Name: "network"}, + {Snap: "http", Name: "network"}, + }, + Slots: []client.Slot{{Snap: "system", Name: "network"}}, + }}, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"interface", "network"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "name: network\n" + + "summary: allows access to the network\n" + + "documentation: http://example.org/about-the-network-interface\n" + + "plugs:\n" + + " - deepin-music\n" + + " - http\n" + + "slots:\n" + + " - system\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceDetailsAndAttrs(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") + c.Check(r.URL.RawQuery, Equals, "doc=true&names=serial-port&plugs=true&select=all&slots=true") + 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.Interface{{ + Name: "serial-port", + Summary: "allows providing or using a specific serial port", + Plugs: []client.Plug{ + {Snap: "minicom", Name: "serial-port"}, + }, + Slots: []client.Slot{{ + Snap: "gizmo-gadget", + Name: "debug-serial-port", + Label: "serial port for debugging", + Attrs: map[string]interface{}{ + "header": "pin-array", + "location": "internal", + "path": "/dev/ttyS0", + "number": 1, + }, + }}, + }}, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"interface", "--attrs", "serial-port"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "name: serial-port\n" + + "summary: allows providing or using a specific serial port\n" + + "plugs:\n" + + " - minicom\n" + + "slots:\n" + + " - gizmo-gadget:debug-serial-port (serial port for debugging):\n" + + " header: pin-array\n" + + " location: internal\n" + + " number: 1\n" + + " path: /dev/ttyS0\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceCompletion(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.RawQuery, Equals, "select=all") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []*client.Interface{{ + Name: "network", + Summary: "allows access to the network", + }, { + Name: "network-bind", + Summary: "allows providing services on the network", + }}, + }) + }) + os.Setenv("GO_FLAGS_COMPLETION", "verbose") + defer os.Unsetenv("GO_FLAGS_COMPLETION") + + expected := []flags.Completion{} + parser := Parser(Client()) + parser.CompletionHandler = func(obtained []flags.Completion) { + c.Check(obtained, DeepEquals, expected) + } + + expected = []flags.Completion{ + {Item: "network", Description: "allows access to the network"}, + {Item: "network-bind", Description: "allows providing services on the network"}, + } + _, err := parser.ParseArgs([]string{"interface", ""}) + c.Assert(err, IsNil) + + expected = []flags.Completion{ + {Item: "network-bind", Description: "allows providing services on the network"}, + } + _, err = parser.ParseArgs([]string{"interface", "network-"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{} + _, err = parser.ParseArgs([]string{"interface", "bogus"}) + c.Assert(err, IsNil) + + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_interfaces.go b/cmd/snap/cmd_interfaces.go new file mode 100644 index 00000000..363e4af2 --- /dev/null +++ b/cmd/snap/cmd_interfaces.go @@ -0,0 +1,168 @@ +// -*- 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 { + clientMixin + Interface string `short:"i"` + Positionals struct { + Query interfacesSlotOrPlugSpec `skip-help:"true"` + } `positional-args:"true"` +} + +var shortInterfacesHelp = i18n.G("List interfaces' slots and plugs") +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{ + // TRANSLATORS: This should not start with a lowercase letter. + "i": i18n.G("Constrain listing to specific interfaces"), + }, []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(":"), + // TRANSLATORS: This should not start with a lowercase letter. + 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 := x.client.Connections() + if err != nil { + return err + } + if len(ifaces.Plugs) == 0 && len(ifaces.Slots) == 0 { + return fmt.Errorf(i18n.G("no interfaces found")) + } + w := tabWriter() + defer w.Flush() + fmt.Fprintln(w, i18n.G("Slot\tPlug")) + + wantedSnap := x.Positionals.Query.Snap + + for _, slot := range ifaces.Slots { + if wantedSnap != "" { + var ok bool + if wantedSnap == slot.Snap { + ok = true + } + // Normally snap nicknames are handled internally in the snapd + // layer. This specific command is an exception as it does + // client-side filtering. As a special case, when the user asked + // for the snap "core" but we see the "system" nickname or the + // "snapd" snap, treat that as a match. + // + // The system nickname was returned in 2.35. + // The snapd snap is returned by 2.36+ if snapd snap is installed + // and is the host for implicit interfaces. + if (wantedSnap == "core" || wantedSnap == "snapd" || wantedSnap == "system") && (slot.Snap == "core" || slot.Snap == "snapd" || slot.Snap == "system") { + ok = true + } + + for i := 0; i < len(slot.Connections) && !ok; i++ { + if wantedSnap == slot.Connections[i].Snap { + ok = true + } + } + if !ok { + continue + } + } + if x.Positionals.Query.Name != "" && x.Positionals.Query.Name != slot.Name { + continue + } + if x.Interface != "" && slot.Interface != x.Interface { + continue + } + // There are two special snaps, the "core" and "snapd" snaps are + // abbreviated to an empty snap name. The "system" snap name is still + // here in case we talk to older snapd for some reason. + if slot.Snap == "core" || slot.Snap == "snapd" || slot.Snap == "system" { + 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 wantedSnap != "" { + if wantedSnap != 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 nil +} diff --git a/cmd/snap/cmd_interfaces_test.go b/cmd/snap/cmd_interfaces_test.go new file mode 100644 index 00000000..a7110ca0 --- /dev/null +++ b/cmd/snap/cmd_interfaces_test.go @@ -0,0 +1,674 @@ +// -*- 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" + "os" + + "github.com/jessevdk/go-flags" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestConnectionsZeroSlotsOnePlug(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.Connections{ + Plugs: []client.Plug{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }) + }) + rest, err := Parser(Client()).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) TestConnectionsZeroPlugsOneSlot(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.Connections{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + }, + }, + }, + }) + }) + rest, err := Parser(Client()).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) TestConnectionsOneSlotOnePlug(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.Connections{ + 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(Client()).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(Client()).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(Client()).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) TestConnectionsTwoPlugs(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.Connections{ + 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(Client()).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) TestConnectionsPlugsWithCommonName(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.Connections{ + 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(Client()).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) TestConnectionsOsSnapSlots(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.Connections{ + Slots: []client.Slot{ + { + Snap: "system", + 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: "system", + Name: "network-listening", + }, + }, + }, + { + Snap: "time-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.SlotRef{ + { + Snap: "system", + Name: "network-listening", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser(Client()).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) TestConnectionsTwoSlotsAndFiltering(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.Connections{ + 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(Client()).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) TestConnectionsOfSpecificSnap(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.Connections{ + 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(Client()).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) TestConnectionsOfSystemNicknameSnap(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.Connections{ + Slots: []client.Slot{ + { + Snap: "system", + Name: "core-support", + Interface: "some-iface", + Connections: []client.PlugRef{{Snap: "core", Name: "core-support-plug"}}, + }, { + Snap: "foo", + Name: "foo-slot", + Interface: "foo-slot-iface", + }, + }, + Plugs: []client.Plug{ + { + Snap: "core", + Name: "core-support-plug", + Interface: "some-iface", + Connections: []client.SlotRef{{Snap: "system", Name: "core-support"}}, + }, + }, + }, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces", "system"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + ":core-support core:core-support-plug\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") + + s.ResetStdStreams() + + // when called with system nickname we get the same output + rest, err = Parser(Client()).ParseArgs([]string{"interfaces", "system"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdoutSystem := "" + + "Slot Plug\n" + + ":core-support core:core-support-plug\n" + c.Assert(s.Stdout(), Equals, expectedStdoutSystem) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsOfSpecificSnapAndSlot(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.Connections{ + 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(Client()).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) TestConnectionsNothingAtAll(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.Connections{}, + }) + }) + rest, err := Parser(Client()).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, "") +} + +func (s *SnapSuite) TestConnectionsOfSpecificType(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.Connections{ + 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(Client()).ParseArgs([]string{"interfaces", "-i", "bool-file"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "cheese:photo-trigger -\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) TestConnectionsCompletion(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Assert(r.Method, Equals, "GET") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": fortestingConnectionList, + }) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + os.Setenv("GO_FLAGS_COMPLETION", "verbose") + defer os.Unsetenv("GO_FLAGS_COMPLETION") + + expected := []flags.Completion{} + parser := Parser(Client()) + parser.CompletionHandler = func(obtained []flags.Completion) { + c.Check(obtained, DeepEquals, expected) + } + + expected = []flags.Completion{{Item: "canonical-pi2:"}, {Item: "core:"}, {Item: "keyboard-lights:"}, {Item: "paste-daemon:"}, {Item: "potato:"}, {Item: "wake-up-alarm:"}} + _, err := parser.ParseArgs([]string{"interfaces", ""}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "paste-daemon:network-listening", Description: "plug"}} + _, err = parser.ParseArgs([]string{"interfaces", "pa"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "wake-up-alarm:toggle", Description: "slot"}} + _, err = parser.ParseArgs([]string{"interfaces", "wa"}) + c.Assert(err, IsNil) + + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsCoreNicknamedSystem(c *C) { + s.checkConnectionsSystemCoreRemapping(c, "core", "system") +} + +func (s *SnapSuite) TestConnectionsSnapdNicknamedSystem(c *C) { + s.checkConnectionsSystemCoreRemapping(c, "snapd", "system") +} + +func (s *SnapSuite) TestConnectionsSnapdNicknamedCore(c *C) { + s.checkConnectionsSystemCoreRemapping(c, "snapd", "core") +} + +func (s *SnapSuite) TestConnectionsCoreSnap(c *C) { + s.checkConnectionsSystemCoreRemapping(c, "core", "core") +} + +func (s *SnapSuite) checkConnectionsSystemCoreRemapping(c *C, apiSnapName, cliSnapName string) { + 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.Connections{ + Slots: []client.Slot{ + { + Snap: apiSnapName, + Name: "network", + }, + }, + }, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"interfaces", cliSnapName}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + ":network -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + 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..29e24f33 --- /dev/null +++ b/cmd/snap/cmd_keys.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" + + "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(` +The keys command lists cryptographic keys that can be used for signing +assertions. +`), + func() flags.Commander { + return &cmdKeys{} + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "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 outputJSON(keys []Key) error { + obj, err := json.Marshal(keys) + if err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", obj) + return nil +} + +func outputText(keys []Key) error { + if len(keys) == 0 { + fmt.Fprintf(Stderr, "No keys registered, see `snapcraft create-key`\n") + return nil + } + + w := tabWriter() + defer w.Flush() + + fmt.Fprintln(w, i18n.G("Name\tSHA3-384")) + for _, key := range keys { + fmt.Fprintf(w, "%s\t%s\n", key.Name, key.Sha3_384) + } + return nil +} + +func (x *cmdKeys) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + keys := []Key{} + + manager := asserts.NewGPGKeypairManager() + collect := func(privk asserts.PrivateKey, fpr string, uid string) error { + key := Key{ + Name: uid, + Sha3_384: privk.PublicKey().ID(), + } + keys = append(keys, key) + return nil + } + err := manager.Walk(collect) + if err != nil { + return err + } + if x.JSON { + return outputJSON(keys) + } + + return outputText(keys) +} diff --git a/cmd/snap/cmd_keys_test.go b/cmd/snap/cmd_keys_test.go new file mode 100644 index 00000000..00dab5f6 --- /dev/null +++ b/cmd/snap/cmd_keys_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 main_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +type SnapKeysSuite struct { + BaseSnapSuite + + GnupgCmd string + tempdir 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) { + if testing.Short() && s.GnupgCmd == "/usr/bin/gpg2" { + c.Skip("gpg2 does not do short tests") + } + s.BaseSnapSuite.SetUpTest(c) + + s.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(s.tempdir, fileName), data, 0644) + c.Assert(err, IsNil) + } + fakePinentryFn := filepath.Join(s.tempdir, "pinentry-fake") + err := ioutil.WriteFile(fakePinentryFn, fakePinentryData, 0755) + c.Assert(err, IsNil) + gpgAgentConfFn := filepath.Join(s.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", s.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(snap.Client()).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) TestKeysEmptyNoHeader(c *C) { + // simulate empty keys + err := os.RemoveAll(s.tempdir) + c.Assert(err, IsNil) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"keys"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "No keys registered, see `snapcraft create-key`\n") +} + +func (s *SnapKeysSuite) TestKeysJSON(c *C) { + rest, err := snap.Parser(snap.Client()).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(snap.Client()).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..17454c9c --- /dev/null +++ b/cmd/snap/cmd_known.go @@ -0,0 +1,128 @@ +// -*- 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 { + clientMixin + KnownOptions struct { + // XXX: how to get a list of assert types for completion? + AssertTypeName assertTypeName `required:"true"` + HeaderFilters []string `required:"0"` + } `positional-args:"true" required:"true"` + + Remote bool `long:"remote"` +} + +var shortKnownHelp = i18n.G("Show 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{ + { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Assertion type name"), + }, { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G("
"), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Constrain listing to those matching header=value"), + }, + }) +} + +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, err := asserts.PrimaryKeyFromHeaders(at, headers) + if err != nil { + return nil, fmt.Errorf("cannot query remote assertion: %v", err) + } + + 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(string(x.KnownOptions.AssertTypeName), headers) + } else { + assertions, err = x.client.Known(string(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..471ec659 --- /dev/null +++ b/cmd/snap/cmd_known_test.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 main_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + + "github.com/jessevdk/go-flags" + "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, _ := url.Parse(server.URL) + cfg.AssertionsBaseURL = serverURL + return store.New(cfg, auth) + }) + defer restorer() + + n := 0 + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.URL.Path, check.Matches, ".*/assertions/.*") // sanity check request + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/api/v1/snaps/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(snap.Client()).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(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical"}) + c.Assert(err, check.ErrorMatches, `cannot query remote assertion: must provide primary key: model`) +} + +func (s *SnapSuite) TestAssertTypeNameCompletion(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/assertions") + fmt.Fprintln(w, `{"type": "sync", "result": { "types": [ "account", "... more stuff ...", "validation" ] } }`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + + c.Check(snap.AssertTypeNameCompletion("v"), check.DeepEquals, []flags.Completion{{Item: "validation"}}) +} diff --git a/cmd/snap/cmd_list.go b/cmd/snap/cmd_list.go new file mode 100644 index 00000000..cb97a0a7 --- /dev/null +++ b/cmd/snap/cmd_list.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 main + +import ( + "errors" + "fmt" + "sort" + "strings" + "text/tabwriter" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/strutil" +) + +var shortListHelp = i18n.G("List installed snaps") +var longListHelp = i18n.G(` +The list command displays a summary of snaps installed in the current system. + +A green check mark (given color and unicode support) after a publisher name +indicates that the publisher has been verified. +`) + +type cmdList struct { + clientMixin + Positional struct { + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` + + All bool `long:"all"` + colorMixin +} + +func init() { + addCommand("list", shortListHelp, longListHelp, func() flags.Commander { return &cmdList{} }, + colorDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "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] } + +var ErrNoMatchingSnaps = errors.New(i18n.G("no matching snaps installed")) + +// snapd will give us and we want +// "" (local snap) "-" +// risk risk +// track track (not yet returned by snapd) +// track/stable track +// track/risk track/risk +// risk/branch risk/… +// track/risk/branch track/risk/… +func fmtChannel(ch string) string { + if ch == "" { + // "" -> "-" (local snap) + return "-" + } + idx := strings.IndexByte(ch, '/') + if idx < 0 { + // risk -> risk + return ch + } + first, rest := ch[:idx], ch[idx+1:] + if rest == "stable" && first != "" { + // track/stable -> track + return first + } + if idx2 := strings.IndexByte(rest, '/'); idx2 >= 0 { + // track/risk/branch -> track/risk/… + return ch[:idx2+idx+2] + "…" + } + // so it's foo/bar -> either risk/branch, or track/risk. + if strutil.ListContains(channelRisks, first) { + // risk/branch -> risk/… + return first + "/…" + } + // track/risk -> track/risk + return ch +} + +func (x *cmdList) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + names := installedSnapNames(x.Positional.Snaps) + snaps, err := x.client.List(names, &client.ListOptions{All: x.All}) + if err != nil { + if err == client.ErrNoSnapsInstalled { + if len(names) == 0 { + fmt.Fprintln(Stderr, i18n.G("No snaps are installed yet. Try 'snap install hello-world'.")) + return nil + } else { + return ErrNoMatchingSnaps + } + } + return err + } else if len(snaps) == 0 { + return ErrNoMatchingSnaps + } + sort.Sort(snapsByName(snaps)) + + esc := x.getEscapes() + w := tabWriter() + + // TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces) + fmt.Fprintf(w, i18n.G("Name\tVersion\tRev\tTracking\tPublisher%s\tNotes\n"), fillerPublisher(esc)) + + for _, snap := range snaps { + // doing it this way because otherwise it's a sea of %s\t%s\t%s + line := []string{ + snap.Name, + snap.Version, + snap.Revision.String(), + fmtChannel(snap.TrackingChannel), + shortPublisher(esc, snap.Publisher), + NotesFromLocal(snap).String(), + } + fmt.Fprintln(w, strings.Join(line, "\t")) + } + w.Flush() + + 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..98312c8a --- /dev/null +++ b/cmd/snap/cmd_list_test.go @@ -0,0 +1,249 @@ +// -*- 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 list [list-OPTIONS] [...] + +The list command displays a summary of snaps installed in the current system. + +A green check mark (given color and unicode support) after a publisher name +indicates that the publisher has been verified. + +[list command options] + --all Show all revisions + --color=[auto|never|always] Use a little bit of color to highlight + some things. (default: auto) + --unicode=[auto|never|always] Use a little bit of Unicode to improve + legibility. (default: auto) +` + s.testSubCommandHelp(c, "list", msg) +} + +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", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17, "tracking-channel": "potatoes"}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Publisher +Notes +foo +4.2 +17 +potatoes +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", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17, "tracking-channel": "stable"}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"list", "--all"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Publisher +Notes +foo +4.2 +17 +stable +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(snap.Client()).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++ + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"list", "quux"}) + c.Assert(err, check.ErrorMatches, `no matching snaps installed`) +} + +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") + c.Check(r.URL.Query().Get("snaps"), check.Equals, "quux") + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + _, err := snap.Parser(snap.Client()).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().Get("snaps"), check.Equals, "foo") + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17, "tracking-channel": "1.10/stable/fix1234"}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"list", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Publisher +Notes +foo +4.2 +17 +1\.10/stable/… +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", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "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(snap.Client()).ParseArgs([]string{"list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?ms)^Name +Version +Rev +Tracking +Publisher +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, "") +} + +func (s *SnapSuite) TestFormatChannel(c *check.C) { + type tableT struct { + channel string + expected string + } + for _, t := range []tableT{ + {"", "-"}, + {"stable", "stable"}, + {"edge", "edge"}, + {"foo/stable", "foo"}, + {"foo/edge", "foo/edge"}, + {"foo", "foo"}, + {"foo/stable/bar", "foo/stable/…"}, + {"foo/edge/bar", "foo/edge/…"}, + {"stable/bar", "stable/…"}, + {"edge/bar", "edge/…"}, + } { + c.Check(snap.FormatChannel(t.channel), check.Equals, t.expected, check.Commentf(t.channel)) + } + + // and some SISO tests just to check it doesn't panic nor return empty string + // (the former would break scripts) + for _, ch := range []string{ + "", + "\x00", + "/", + "//", + "///", + "////", + "a/", + "/b", + "a//b", + "/stable", + "/edge", + } { + c.Check(snap.FormatChannel(ch), check.Not(check.Equals), "", check.Commentf(ch)) + } +} diff --git a/cmd/snap/cmd_login.go b/cmd/snap/cmd_login.go new file mode 100644 index 00000000..707a81f7 --- /dev/null +++ b/cmd/snap/cmd_login.go @@ -0,0 +1,135 @@ +// -*- 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 { + clientMixin + Positional struct { + Email string + } `positional-args:"yes"` +} + +var shortLoginHelp = i18n.G("Authenticate to snapd and the store") + +var longLoginHelp = i18n.G(` +The login command authenticates the user to 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. + +It's not necessary to log in to interact with snapd. Doing so, however, enables +purchasing of snaps using 'snap buy', as well as some some developer-oriented +features as detailed in the help for the find, install and refresh commands. + +An account can be set up at https://login.ubuntu.com +`) + +func init() { + addCommand("login", + shortLoginHelp, + longLoginHelp, + func() flags.Commander { + return &cmdLogin{} + }, nil, []argDesc{{ + // TRANSLATORS: This is a noun, and it needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter (unless it's "login.ubuntu.com") + desc: i18n.G("The login.ubuntu.com email to login as"), + }}) +} + +func requestLoginWith2faRetry(cli *client.Client, 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: "), + } + + 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(cli *client.Client, 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(cli, email, strings.TrimSpace(string(password))) +} + +func (x *cmdLogin) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + //TRANSLATORS: after the "... at" follows a URL in the next line + fmt.Fprint(Stdout, i18n.G("Personal information is handled as per our privacy notice at\n")) + fmt.Fprint(Stdout, "https://www.ubuntu.com/legal/dataprivacy/snap-store\n\n") + + 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(x.client, 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..a4f3e1e4 --- /dev/null +++ b/cmd/snap/cmd_login_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 ( + "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(snap.Client()).ParseArgs([]string{"login", "foo@example.com"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, `Personal information is handled as per our privacy notice at +https://www.ubuntu.com/legal/dataprivacy/snap-store + +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(snap.Client()).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, `Personal information is handled as per our privacy notice at +https://www.ubuntu.com/legal/dataprivacy/snap-store + +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..05f80faf --- /dev/null +++ b/cmd/snap/cmd_logout.go @@ -0,0 +1,53 @@ +// -*- 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 { + clientMixin +} + +var shortLogoutHelp = i18n.G("Log out of snapd and the store") + +var longLogoutHelp = i18n.G(` +The logout command logs the current user out of snapd and 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 cmd.client.Logout() +} diff --git a/cmd/snap/cmd_managed.go b/cmd/snap/cmd_managed.go new file mode 100644 index 00000000..3491d87a --- /dev/null +++ b/cmd/snap/cmd_managed.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 ( + "fmt" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortIsManagedHelp = i18n.G("Print whether the system is managed") +var longIsManagedHelp = i18n.G(` +The managed command will print true or false informing whether +snapd has registered users. +`) + +type cmdIsManaged struct { + clientMixin +} + +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 := cmd.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..140375f1 --- /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(snap.Client()).ParseArgs([]string{"managed"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, fmt.Sprintf("%v\n", managed)) + } +} diff --git a/cmd/snap/cmd_pack.go b/cmd/snap/cmd_pack.go new file mode 100644 index 00000000..a874eeaf --- /dev/null +++ b/cmd/snap/cmd_pack.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 main + +import ( + "fmt" + "path/filepath" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/pack" +) + +type packCmd struct { + CheckSkeleton bool `long:"check-skeleton"` + Filename string `long:"filename"` + Positional struct { + SnapDir string `positional-arg-name:""` + TargetDir string `positional-arg-name:""` + } `positional-args:"yes"` +} + +var shortPackHelp = i18n.G("Pack the given directory as a snap") +var longPackHelp = i18n.G(` +The pack command packs the given snap-dir as a snap and writes the result to +target-dir. If target-dir is omitted, the result is written to current +directory. If both source-dir and target-dir are omitted, the pack command packs +the current directory. + +The default file name for a snap can be derived entirely from its snap.yaml, but +in some situations it's simpler for a script to feed the filename in. In those +cases, --filename can be given to override the default. If this filename is +not absolute it will be taken as relative to target-dir. + +When used with --check-skeleton, pack only checks whether snap-dir contains +valid snap metadata and raises an error otherwise. Application commands listed +in snap metadata file, but appearing with incorrect permission bits result in an +error. Commands that are missing from snap-dir are listed in diagnostic +messages. +`) + +func init() { + cmd := addCommand("pack", + shortPackHelp, + longPackHelp, + func() flags.Commander { + return &packCmd{} + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "check-skeleton": i18n.G("Validate snap-dir metadata only"), + // TRANSLATORS: This should not start with a lowercase letter. + "filename": i18n.G("Output to this filename"), + }, nil) + cmd.extra = func(cmd *flags.Command) { + // TRANSLATORS: this describes the default filename for a snap, e.g. core_16-2.35.2_amd64.snap + cmd.FindOptionByLongName("filename").DefaultMask = i18n.G("__.snap") + } +} + +func (x *packCmd) Execute([]string) error { + if x.Positional.TargetDir != "" && x.Filename != "" && filepath.IsAbs(x.Filename) { + return fmt.Errorf(i18n.G("you can't specify an absolute filename while also specifying target dir.")) + } + + if x.Positional.SnapDir == "" { + x.Positional.SnapDir = "." + } + if x.Positional.TargetDir == "" { + x.Positional.TargetDir = "." + } + + if x.CheckSkeleton { + err := pack.CheckSkeleton(x.Positional.SnapDir) + if err == snap.ErrMissingPaths { + return nil + } + return err + } + + snapPath, err := pack.Snap(x.Positional.SnapDir, x.Positional.TargetDir, x.Filename) + if err != nil { + // TRANSLATORS: the %q is the snap-dir (the first positional + // argument to the command); the %v is an error + return fmt.Errorf(i18n.G("cannot pack %q: %v"), x.Positional.SnapDir, err) + + } + // TRANSLATORS: %s is the path to the built snap file + fmt.Fprintf(Stdout, i18n.G("built: %s\n"), snapPath) + return nil +} diff --git a/cmd/snap/cmd_pack_test.go b/cmd/snap/cmd_pack_test.go new file mode 100644 index 00000000..c1aef464 --- /dev/null +++ b/cmd/snap/cmd_pack_test.go @@ -0,0 +1,103 @@ +package main_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + "gopkg.in/check.v1" + + snaprun "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/logger" +) + +const packSnapYaml = `name: hello +version: 1.0.1 +apps: + app: + command: bin/hello +` + +func makeSnapDirForPack(c *check.C, snapYaml string) string { + tempdir := c.MkDir() + c.Assert(os.Chmod(tempdir, 0755), check.IsNil) + + // use meta/snap.yaml + metaDir := filepath.Join(tempdir, "meta") + err := os.Mkdir(metaDir, 0755) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(metaDir, "snap.yaml"), []byte(snapYaml), 0644) + c.Assert(err, check.IsNil) + + return tempdir +} + +func (s *SnapSuite) TestPackCheckSkeletonNoAppFiles(c *check.C) { + _, r := logger.MockLogger() + defer r() + + snapDir := makeSnapDirForPack(c, packSnapYaml) + + // check-skeleton does not fail due to missing files + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) TestPackCheckSkeletonBadMeta(c *check.C) { + // no snap name + snapYaml := ` +version: foobar +apps: +` + snapDir := makeSnapDirForPack(c, snapYaml) + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir}) + c.Assert(err, check.ErrorMatches, `cannot validate snap "": snap name cannot be empty`) +} + +func (s *SnapSuite) TestPackCheckSkeletonConflictingCommonID(c *check.C) { + // conflicting common-id + snapYaml := `name: foo +version: foobar +apps: + foo: + common-id: org.foo.foo + bar: + common-id: org.foo.foo +` + snapDir := makeSnapDirForPack(c, snapYaml) + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir}) + c.Assert(err, check.ErrorMatches, `cannot validate snap "foo": application ("bar" common-id "org.foo.foo" must be unique, already used by application "foo"|"foo" common-id "org.foo.foo" must be unique, already used by application "bar")`) +} + +func (s *SnapSuite) TestPackPacksFailsForMissingPaths(c *check.C) { + _, r := logger.MockLogger() + defer r() + + snapDir := makeSnapDirForPack(c, packSnapYaml) + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", snapDir, snapDir}) + c.Assert(err, check.ErrorMatches, ".* snap is unusable due to missing files") +} + +func (s *SnapSuite) TestPackPacksASnap(c *check.C) { + snapDir := makeSnapDirForPack(c, packSnapYaml) + + const helloBinContent = `#!/bin/sh +printf "hello world" +` + // an example binary + binDir := filepath.Join(snapDir, "bin") + err := os.Mkdir(binDir, 0755) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(binDir, "hello"), []byte(helloBinContent), 0755) + c.Assert(err, check.IsNil) + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", snapDir, snapDir}) + c.Assert(err, check.IsNil) + + matches, err := filepath.Glob(snapDir + "/hello*.snap") + c.Assert(err, check.IsNil) + c.Assert(matches, check.HasLen, 1) +} diff --git a/cmd/snap/cmd_paths.go b/cmd/snap/cmd_paths.go new file mode 100644 index 00000000..9578bd0a --- /dev/null +++ b/cmd/snap/cmd_paths.go @@ -0,0 +1,62 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" +) + +var pathsHelp = i18n.G("Print system paths") +var longPathsHelp = i18n.G(` +The paths command prints the list of paths detected and used by snapd. +`) + +type cmdPaths struct{} + +func init() { + addDebugCommand("paths", pathsHelp, longPathsHelp, func() flags.Commander { + return &cmdPaths{} + }, nil, nil) +} + +func (cmd cmdPaths) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + // TODO: include paths reported by snap-confine + for _, p := range []struct { + name string + path string + }{ + {"SNAPD_MOUNT", dirs.SnapMountDir}, + {"SNAPD_BIN", dirs.SnapBinariesDir}, + {"SNAPD_LIBEXEC", dirs.DistroLibExecDir}, + } { + fmt.Fprintf(Stdout, "%s=%s\n", p.name, p.path) + } + + return nil +} diff --git a/cmd/snap/cmd_paths_test.go b/cmd/snap/cmd_paths_test.go new file mode 100644 index 00000000..0eb9c430 --- /dev/null +++ b/cmd/snap/cmd_paths_test.go @@ -0,0 +1,90 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/release" +) + +func (s *SnapSuite) TestPathsUbuntu(c *C) { + restore := release.MockReleaseInfo(&release.OS{ID: "ubuntu"}) + defer restore() + defer dirs.SetRootDir("/") + + dirs.SetRootDir("/") + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "paths"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "SNAPD_MOUNT=/snap\n"+ + "SNAPD_BIN=/snap/bin\n"+ + "SNAPD_LIBEXEC=/usr/lib/snapd\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestPathsFedora(c *C) { + restore := release.MockReleaseInfo(&release.OS{ID: "fedora"}) + defer restore() + defer dirs.SetRootDir("/") + + dirs.SetRootDir("/") + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "paths"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "SNAPD_MOUNT=/var/lib/snapd/snap\n"+ + "SNAPD_BIN=/var/lib/snapd/snap/bin\n"+ + "SNAPD_LIBEXEC=/usr/libexec/snapd\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestPathsArch(c *C) { + defer dirs.SetRootDir("/") + + // old /etc/os-release contents + restore := release.MockReleaseInfo(&release.OS{ID: "arch", IDLike: []string{"archlinux"}}) + defer restore() + + dirs.SetRootDir("/") + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "paths"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "SNAPD_MOUNT=/var/lib/snapd/snap\n"+ + "SNAPD_BIN=/var/lib/snapd/snap/bin\n"+ + "SNAPD_LIBEXEC=/usr/lib/snapd\n") + c.Assert(s.Stderr(), Equals, "") + + s.ResetStdStreams() + + // new contents, as set by filesystem-2018.12-1 + restore = release.MockReleaseInfo(&release.OS{ID: "archlinux"}) + defer restore() + + dirs.SetRootDir("/") + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"debug", "paths"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "SNAPD_MOUNT=/var/lib/snapd/snap\n"+ + "SNAPD_BIN=/var/lib/snapd/snap/bin\n"+ + "SNAPD_LIBEXEC=/usr/lib/snapd\n") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_prefer.go b/cmd/snap/cmd_prefer.go new file mode 100644 index 00000000..370c853a --- /dev/null +++ b/cmd/snap/cmd_prefer.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 main + +import ( + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdPrefer struct { + waitMixin + Positionals struct { + Snap installedSnapName `required:"yes"` + } `positional-args:"true"` +} + +var shortPreferHelp = i18n.G("Enable aliases from a snap, disabling any conflicting aliases") +var longPreferHelp = i18n.G(` +The prefer command enables all aliases of the given snap in preference +to conflicting aliases of other snaps whose aliases will be disabled +(or removed, for manual ones). +`) + +func init() { + addCommand("prefer", shortPreferHelp, longPreferHelp, func() flags.Commander { + return &cmdPrefer{} + }, waitDescs, []argDesc{ + {name: ""}, + }) +} + +func (x *cmdPrefer) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + id, err := x.client.Prefer(string(x.Positionals.Snap)) + if err != nil { + return err + } + chg, err := x.wait(id) + if err != nil { + if err == noWait { + return nil + } + return err + } + + return showAliasChanges(chg) +} diff --git a/cmd/snap/cmd_prefer_test.go b/cmd/snap/cmd_prefer_test.go new file mode 100644 index 00000000..b47cb5c4 --- /dev/null +++ b/cmd/snap/cmd_prefer_test.go @@ -0,0 +1,71 @@ +// -*- 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) TestPreferHelp(c *C) { + msg := `Usage: + snap.test prefer [prefer-OPTIONS] [] + +The prefer command enables all aliases of the given snap in preference +to conflicting aliases of other snaps whose aliases will be disabled +(or removed, for manual ones). + +[prefer command options] + --no-wait Do not wait for the operation to finish but just print the + change id. +` + s.testSubCommandHelp(c, "prefer", msg) +} + +func (s *SnapSuite) TestPrefer(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": "prefer", + "snap": "some-snap", + }) + 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", "data": {"aliases-added": [{"alias": "alias1", "snap": "some-snap", "app": "cmd1"}]}}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser(Client()).ParseArgs([]string{"prefer", "some-snap"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, ""+ + "Added:\n"+ + " - some-snap.cmd1 as alias1\n", + ) + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_prepare_image.go b/cmd/snap/cmd_prepare_image.go new file mode 100644 index 00000000..b9636afa --- /dev/null +++ b/cmd/snap/cmd_prepare_image.go @@ -0,0 +1,82 @@ +// -*- 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" default:"stable"` +} + +func init() { + cmd := addCommand("prepare-image", + i18n.G("Prepare a core device image"), + i18n.G(` +The prepare-image command performs some of the steps necessary for creating +core device images. +`), + func() flags.Commander { + return &cmdPrepareImage{} + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "extra-snaps": i18n.G("Extra snaps to be installed"), + // TRANSLATORS: This should not start with a lowercase letter. + "channel": i18n.G("The channel to use"), + }, []argDesc{ + { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("The model assertion name"), + }, { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + 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_repair_repairs.go b/cmd/snap/cmd_repair_repairs.go new file mode 100644 index 00000000..f14636c5 --- /dev/null +++ b/cmd/snap/cmd_repair_repairs.go @@ -0,0 +1,93 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/release" +) + +func runSnapRepair(cmdStr string, args []string) error { + // do not even try to run snap-repair on classic, some distros + // may not even package it + if release.OnClassic { + return fmt.Errorf(i18n.G("repairs are not available on a classic system")) + } + + snapRepairPath := filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "snap-repair") + args = append([]string{cmdStr}, args...) + cmd := exec.Command(snapRepairPath, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +type cmdShowRepair struct { + Positional struct { + Repair []string `positional-arg-name:""` + } `positional-args:"yes"` +} + +var shortRepairHelp = i18n.G("Show specific repairs") +var longRepairHelp = i18n.G(` +The repair command shows the details about one or multiple repairs. +`) + +func init() { + cmd := addCommand("repair", shortRepairHelp, longRepairHelp, func() flags.Commander { + return &cmdShowRepair{} + }, nil, nil) + if release.OnClassic { + cmd.hidden = true + } +} + +func (x *cmdShowRepair) Execute(args []string) error { + return runSnapRepair("show", x.Positional.Repair) +} + +type cmdListRepairs struct{} + +var shortRepairsHelp = i18n.G("Lists all repairs") +var longRepairsHelp = i18n.G(` +The repairs command lists all processed repairs for this device. +`) + +func init() { + cmd := addCommand("repairs", shortRepairsHelp, longRepairsHelp, func() flags.Commander { + return &cmdListRepairs{} + }, nil, nil) + if release.OnClassic { + cmd.hidden = true + } +} + +func (x *cmdListRepairs) Execute(args []string) error { + return runSnapRepair("list", args) +} diff --git a/cmd/snap/cmd_repair_repairs_test.go b/cmd/snap/cmd_repair_repairs_test.go new file mode 100644 index 00000000..39adf711 --- /dev/null +++ b/cmd/snap/cmd_repair_repairs_test.go @@ -0,0 +1,67 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main_test + +import ( + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/testutil" +) + +func mockSnapRepair(c *C) *testutil.MockCmd { + coreLibExecDir := filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir) + err := os.MkdirAll(coreLibExecDir, 0755) + c.Assert(err, IsNil) + return testutil.MockCommand(c, filepath.Join(coreLibExecDir, "snap-repair"), "") +} + +func (s *SnapSuite) TestSnapShowRepair(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + mockSnapRepair := mockSnapRepair(c) + defer mockSnapRepair.Restore() + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"repair", "canonical-1"}) + c.Assert(err, IsNil) + c.Check(mockSnapRepair.Calls(), DeepEquals, [][]string{ + {"snap-repair", "show", "canonical-1"}, + }) +} + +func (s *SnapSuite) TestSnapListRepairs(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + mockSnapRepair := mockSnapRepair(c) + defer mockSnapRepair.Restore() + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"repairs"}) + c.Assert(err, IsNil) + c.Check(mockSnapRepair.Calls(), DeepEquals, [][]string{ + {"snap-repair", "list"}, + }) +} diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go new file mode 100644 index 00000000..1aaf8e9a --- /dev/null +++ b/cmd/snap/cmd_run.go @@ -0,0 +1,943 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2018 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" + "io" + "io/ioutil" + "os" + "os/exec" + "os/user" + "path/filepath" + "regexp" + "strconv" + "strings" + "syscall" + "time" + + "github.com/godbus/dbus" + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/osutil/strace" + "github.com/snapcore/snapd/selinux" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapenv" + "github.com/snapcore/snapd/strutil/shlex" + "github.com/snapcore/snapd/timeutil" + "github.com/snapcore/snapd/x11" +) + +var ( + syscallExec = syscall.Exec + userCurrent = user.Current + osGetenv = os.Getenv + timeNow = time.Now + selinuxIsEnabled = selinux.IsEnabled + selinuxVerifyPathContext = selinux.VerifyPathContext + selinuxRestoreContext = selinux.RestoreContext +) + +type cmdRun struct { + clientMixin + Command string `long:"command" hidden:"yes"` + HookName string `long:"hook" hidden:"yes"` + Revision string `short:"r" default:"unset" hidden:"yes"` + Shell bool `long:"shell" ` + + // This options is both a selector (use or don't use strace) and it + // can also carry extra options for strace. This is why there is + // "default" and "optional-value" to distinguish this. + Strace string `long:"strace" optional:"true" optional-value:"with-strace" default:"no-strace" default-mask:"-"` + Gdb bool `long:"gdb"` + TraceExec bool `long:"trace-exec"` + + // not a real option, used to check if cmdRun is initialized by + // the parser + ParserRan int `long:"parser-ran" default:"1" hidden:"yes"` + Timer string `long:"timer" hidden:"yes"` +} + +func init() { + addCommand("run", + i18n.G("Run the given snap command"), + i18n.G(` +The run command executes the given snap command with the right confinement +and environment. +`), + func() flags.Commander { + return &cmdRun{} + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "command": i18n.G("Alternative command to run"), + // TRANSLATORS: This should not start with a lowercase letter. + "hook": i18n.G("Hook to run"), + // TRANSLATORS: This should not start with a lowercase letter. + "r": i18n.G("Use a specific snap revision when running hook"), + // TRANSLATORS: This should not start with a lowercase letter. + "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), + // TRANSLATORS: This should not start with a lowercase letter. + "strace": i18n.G("Run the command under strace (useful for debugging). Extra strace options can be specified as well here. Pass --raw to strace early snap helpers."), + // TRANSLATORS: This should not start with a lowercase letter. + "gdb": i18n.G("Run the command with gdb"), + // TRANSLATORS: This should not start with a lowercase letter. + "timer": i18n.G("Run as a timer service with given schedule"), + // TRANSLATORS: This should not start with a lowercase letter. + "trace-exec": i18n.G("Display exec calls timing data"), + "parser-ran": "", + }, nil) +} + +func maybeWaitForSecurityProfileRegeneration(cli *client.Client) error { + // check if the security profiles key has changed, if so, we need + // to wait for snapd to re-generate all profiles + mismatch, err := interfaces.SystemKeyMismatch() + if err == nil && !mismatch { + return nil + } + // something went wrong with the system-key compare, try to + // reach snapd before continuing + if err != nil { + logger.Debugf("SystemKeyMismatch returned an error: %v", err) + } + + // We have a mismatch, try to connect to snapd, once we can + // connect we just continue because that usually means that + // a new snapd is ready and has generated profiles. + // + // There is a corner case if an upgrade leaves the old snapd + // running and we connect to the old snapd. Handling this + // correctly is tricky because our "snap run" pipeline may + // depend on profiles written by the new snapd. So for now we + // just continue and hope for the best. The real fix for this + // is to fix the packaging so that snapd is stopped, upgraded + // and started. + // + // connect timeout for client is 5s on each try, so 12*5s = 60s + timeout := 12 + if timeoutEnv := os.Getenv("SNAPD_DEBUG_SYSTEM_KEY_RETRY"); timeoutEnv != "" { + if i, err := strconv.Atoi(timeoutEnv); err == nil { + timeout = i + } + } + + for i := 0; i < timeout; i++ { + if _, err := cli.SysInfo(); err == nil { + return nil + } + // sleep a litte bit for good measure + time.Sleep(1 * time.Second) + } + + return fmt.Errorf("timeout waiting for snap system profiles to get updated") +} + +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 + optionsSet := 0 + for _, param := range []string{x.HookName, x.Command, x.Timer} { + if param != "" { + optionsSet++ + } + } + if optionsSet > 1 { + return fmt.Errorf("you can only use one of --hook, --command, and --timer") + } + + if x.Revision != "unset" && x.Revision != "" && x.HookName == "" { + return fmt.Errorf(i18n.G("-r can only be used with --hook")) + } + if x.HookName != "" && 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.HookName, strings.Join(args, " ")) + } + + if err := maybeWaitForSecurityProfileRegeneration(x.client); err != nil { + return err + } + + // Now actually handle the dispatching + if x.HookName != "" { + return x.snapRunHook(snapApp) + } + + if x.Command == "complete" { + snapApp, args = antialias(snapApp, args) + } + + if x.Timer != "" { + return x.snapRunTimer(snapApp, x.Timer, args) + } + + return x.snapRunApp(snapApp, args) +} + +// antialias changes snapApp and args if snapApp is actually an alias +// for something else. If not, or if the args aren't what's expected +// for completion, it returns them unchanged. +func antialias(snapApp string, args []string) (string, []string) { + if len(args) < 7 { + // NOTE if len(args) < 7, Something is Wrong (at least WRT complete.sh and etelpmoc.sh) + return snapApp, args + } + + actualApp, err := resolveApp(snapApp) + if err != nil || actualApp == snapApp { + // no alias! woop. + return snapApp, args + } + + compPoint, err := strconv.Atoi(args[2]) + if err != nil { + // args[2] is not COMP_POINT + return snapApp, args + } + + if compPoint <= len(snapApp) { + // COMP_POINT is inside $0 + return snapApp, args + } + + if compPoint > len(args[5]) { + // COMP_POINT is bigger than $# + return snapApp, args + } + + if args[6] != snapApp { + // args[6] is not COMP_WORDS[0] + return snapApp, args + } + + // it _should_ be COMP_LINE followed by one of + // COMP_WORDBREAKS, but that's hard to do + re, err := regexp.Compile(`^` + regexp.QuoteMeta(snapApp) + `\b`) + if err != nil || !re.MatchString(args[5]) { + // (weird regexp error, or) args[5] is not COMP_LINE + return snapApp, args + } + + argsOut := make([]string, len(args)) + copy(argsOut, args) + + argsOut[2] = strconv.Itoa(compPoint - len(snapApp) + len(actualApp)) + argsOut[5] = re.ReplaceAllLiteralString(args[5], actualApp) + argsOut[6] = actualApp + + return actualApp, argsOut +} + +func getSnapInfo(snapName string, revision snap.Revision) (info *snap.Info, err error) { + if revision.Unset() { + info, err = snap.ReadCurrentInfo(snapName) + } else { + info, err = snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: revision, + }) + } + + return info, err +} + +func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error { + // 'current' symlink for user data (SNAP_USER_DATA) + userData := info.UserDataDir(usr.HomeDir) + wantedSymlinkValue := filepath.Base(userData) + currentActiveSymlink := filepath.Join(userData, "..", "current") + + var err error + var currentSymlinkValue string + for i := 0; i < 5; i++ { + currentSymlinkValue, err = os.Readlink(currentActiveSymlink) + // Failure other than non-existing symlink is fatal + if err != nil && !os.IsNotExist(err) { + // TRANSLATORS: %v the error message + return fmt.Errorf(i18n.G("cannot read symlink: %v"), err) + } + + if currentSymlinkValue == wantedSymlinkValue { + break + } + + if err == nil { + // We may be racing with other instances of snap-run that try to do the same thing + // If the symlink is already removed then we can ignore this error. + err = os.Remove(currentActiveSymlink) + if err != nil && !os.IsNotExist(err) { + // abort with error + break + } + } + + err = os.Symlink(wantedSymlinkValue, currentActiveSymlink) + // Error other than symlink already exists will abort and be propagated + if err == nil || !os.IsExist(err) { + break + } + // If we arrived here it means the symlink couldn't be created because it got created + // in the meantime by another instance, so we will try again. + } + if err != nil { + return fmt.Errorf(i18n.G("cannot update the 'current' symlink of %q: %v"), currentActiveSymlink, err) + } + return 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 + instanceUserData := info.UserDataDir(usr.HomeDir) + instanceCommonUserData := info.UserCommonDataDir(usr.HomeDir) + createDirs := []string{instanceUserData, instanceCommonUserData} + if info.InstanceKey != "" { + // parallel instance snaps get additional mapping in their mount + // namespace, namely /home/joe/snap/foo_bar -> + // /home/joe/snap/foo, make sure that the mount point exists and + // is owned by the user + snapUserDir := snap.UserSnapDir(usr.HomeDir, info.SnapName()) + createDirs = append(createDirs, snapUserDir) + } + for _, d := range createDirs { + 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) + } + } + + if err := createOrUpdateUserDataSymlink(info, usr); err != nil { + return err + } + + return maybeRestoreSecurityContext(usr) +} + +// maybeRestoreSecurityContext attempts to restore security context of ~/snap on +// systems where it's applicable +func maybeRestoreSecurityContext(usr *user.User) error { + snapUserHome := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) + enabled, err := selinuxIsEnabled() + if err != nil { + return fmt.Errorf("cannot determine SELinux status: %v", err) + } + if !enabled { + logger.Debugf("SELinux not enabled") + return nil + } + + match, err := selinuxVerifyPathContext(snapUserHome) + if err != nil { + return fmt.Errorf("failed to verify SELinux context of %v: %v", snapUserHome, err) + } + if match { + return nil + } + logger.Noticef("restoring default SELinux context of %v", snapUserHome) + + if err := selinuxRestoreContext(snapUserHome, selinux.RestoreMode{Recursive: true}); err != nil { + return fmt.Errorf("cannot restore SELinux context of %v: %v", snapUserHome, err) + } + return nil +} + +func (x *cmdRun) useStrace() bool { + return x.ParserRan == 1 && x.Strace != "no-strace" +} + +func (x *cmdRun) straceOpts() (opts []string, raw bool, err error) { + if x.Strace == "with-strace" { + return nil, false, nil + } + + split, err := shlex.Split(x.Strace) + if err != nil { + return nil, false, err + } + + opts = make([]string, 0, len(split)) + for _, opt := range split { + if opt == "--raw" { + raw = true + continue + } + opts = append(opts, opt) + } + return opts, raw, nil +} + +func (x *cmdRun) snapRunApp(snapApp 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 x.runSnapConfine(info, app.SecurityTag(), snapApp, "", args) +} + +func (x *cmdRun) snapRunHook(snapName string) error { + revision, err := snap.ParseRevision(x.Revision) + if err != nil { + return err + } + + info, err := getSnapInfo(snapName, revision) + if err != nil { + return err + } + + hook := info.Hooks[x.HookName] + if hook == nil { + return fmt.Errorf(i18n.G("cannot find hook %q in %q"), x.HookName, snapName) + } + + return x.runSnapConfine(info, hook.SecurityTag(), snapName, hook.Name, nil) +} + +func (x *cmdRun) snapRunTimer(snapApp, timer string, args []string) error { + schedule, err := timeutil.ParseSchedule(timer) + if err != nil { + return fmt.Errorf("invalid timer format: %v", err) + } + + now := timeNow() + if !timeutil.Includes(schedule, now) { + fmt.Fprintf(Stderr, "%s: attempted to run %q timer outside of scheduled time %q\n", now.Format(time.RFC3339), snapApp, timer) + return nil + } + + return x.snapRunApp(snapApp, args) +} + +var osReadlink = os.Readlink + +func isReexeced() bool { + exe, err := osReadlink("/proc/self/exe") + if err != nil { + logger.Noticef("cannot read /proc/self/exe: %v", err) + return false + } + return strings.HasPrefix(exe, dirs.SnapMountDir) +} + +func migrateXauthority(info *snap.Info) (string, error) { + u, err := userCurrent() + if err != nil { + return "", fmt.Errorf(i18n.G("cannot get the current user: %s"), err) + } + + // If our target directory (XDG_RUNTIME_DIR) doesn't exist we + // don't attempt to create it. + baseTargetDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid) + if !osutil.FileExists(baseTargetDir) { + return "", nil + } + + xauthPath := osGetenv("XAUTHORITY") + if len(xauthPath) == 0 || !osutil.FileExists(xauthPath) { + // Nothing to do for us. Most likely running outside of any + // graphical X11 session. + return "", nil + } + + fin, err := os.Open(xauthPath) + if err != nil { + return "", err + } + defer fin.Close() + + // Abs() also calls Clean(); see https://golang.org/pkg/path/filepath/#Abs + xauthPathAbs, err := filepath.Abs(fin.Name()) + if err != nil { + return "", nil + } + + // Remove all symlinks from path + xauthPathCan, err := filepath.EvalSymlinks(xauthPathAbs) + if err != nil { + return "", nil + } + + // Ensure the XAUTHORITY env is not abused by checking that + // it point to exactly the file we just opened (no symlinks, + // no funny "../.." etc) + if fin.Name() != xauthPathCan { + logger.Noticef("WARNING: XAUTHORITY environment value is not a clean path: %q", xauthPathCan) + return "", nil + } + + // Only do the migration from /tmp since the real /tmp is not visible for snaps + if !strings.HasPrefix(fin.Name(), "/tmp/") { + return "", nil + } + + // We are performing a Stat() here to make sure that the user can't + // steal another user's Xauthority file. Note that while Stat() uses + // fstat() on the file descriptor created during Open(), the file might + // have changed ownership between the Open() and the Stat(). That's ok + // because we aren't trying to block access that the user already has: + // if the user has the privileges to chown another user's Xauthority + // file, we won't block that since the user can just steal it without + // having to use snap run. This code is just to ensure that a user who + // doesn't have those privileges can't steal the file via snap run + // (also note that the (potentially untrusted) snap isn't running yet). + fi, err := fin.Stat() + if err != nil { + return "", err + } + sys := fi.Sys() + if sys == nil { + return "", fmt.Errorf(i18n.G("cannot validate owner of file %s"), fin.Name()) + } + // cheap comparison as the current uid is only available as a string + // but it is better to convert the uid from the stat result to a + // string than a string into a number. + if fmt.Sprintf("%d", sys.(*syscall.Stat_t).Uid) != u.Uid { + return "", fmt.Errorf(i18n.G("Xauthority file isn't owned by the current user %s"), u.Uid) + } + + targetPath := filepath.Join(baseTargetDir, ".Xauthority") + + // Only validate Xauthority file again when both files don't match + // otherwise we can continue using the existing Xauthority file. + // This is ok to do here because we aren't trying to protect against + // the user changing the Xauthority file in XDG_RUNTIME_DIR outside + // of snapd. + if osutil.FileExists(targetPath) { + var fout *os.File + if fout, err = os.Open(targetPath); err != nil { + return "", err + } + if osutil.StreamsEqual(fin, fout) { + fout.Close() + return targetPath, nil + } + + fout.Close() + if err := os.Remove(targetPath); err != nil { + return "", err + } + + // Ensure we're validating the Xauthority file from the beginning + if _, err := fin.Seek(int64(os.SEEK_SET), 0); err != nil { + return "", err + } + } + + // To guard against setting XAUTHORITY to non-xauth files, check + // that we have a valid Xauthority. Specifically, the file must be + // parseable as an Xauthority file and not be empty. + if err := x11.ValidateXauthority(fin); err != nil { + return "", err + } + + // Read data from the beginning of the file + if _, err = fin.Seek(int64(os.SEEK_SET), 0); err != nil { + return "", err + } + + fout, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return "", err + } + defer fout.Close() + + // Read and write validated Xauthority file to its right location + if _, err = io.Copy(fout, fin); err != nil { + if err := os.Remove(targetPath); err != nil { + logger.Noticef("WARNING: cannot remove file at %s: %s", targetPath, err) + } + return "", fmt.Errorf(i18n.G("cannot write new Xauthority file at %s: %s"), targetPath, err) + } + + return targetPath, nil +} + +func activateXdgDocumentPortal(info *snap.Info, snapApp, hook string) error { + // Don't do anything for apps or hooks that don't plug the + // desktop interface + // + // NOTE: This check is imperfect because we don't really know + // if the interface is connected or not but this is an + // acceptable compromise for not having to communicate with + // snapd in snap run. In a typical desktop session the + // document portal can be in use by many applications, not + // just by snaps, so this is at most, pre-emptively using some + // extra memory. + var plugs map[string]*snap.PlugInfo + if hook != "" { + plugs = info.Hooks[hook].Plugs + } else { + _, appName := snap.SplitSnapApp(snapApp) + plugs = info.Apps[appName].Plugs + } + plugsDesktop := false + for _, plug := range plugs { + if plug.Interface == "desktop" { + plugsDesktop = true + break + } + } + if !plugsDesktop { + return nil + } + + u, err := userCurrent() + if err != nil { + return fmt.Errorf(i18n.G("cannot get the current user: %s"), err) + } + xdgRuntimeDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid) + + // If $XDG_RUNTIME_DIR/doc appears to be a mount point, assume + // that the document portal is up and running. + expectedMountPoint := filepath.Join(xdgRuntimeDir, "doc") + if mounted, err := osutil.IsMounted(expectedMountPoint); err != nil { + logger.Noticef("Could not check document portal mount state: %s", err) + } else if mounted { + return nil + } + + // If there is no session bus, our job is done. We check this + // manually to avoid dbus.SessionBus() auto-launching a new + // bus. + busAddress := osGetenv("DBUS_SESSION_BUS_ADDRESS") + if len(busAddress) == 0 { + return nil + } + + // We've previously tried to start the document portal and + // were told the service is unknown: don't bother connecting + // to the session bus again. + // + // As the file is in $XDG_RUNTIME_DIR, it will be cleared over + // full logout/login or reboot cycles. + portalsUnavailableFile := filepath.Join(xdgRuntimeDir, ".portals-unavailable") + if osutil.FileExists(portalsUnavailableFile) { + return nil + } + + conn, err := dbus.SessionBus() + if err != nil { + return err + } + + portal := conn.Object("org.freedesktop.portal.Documents", + "/org/freedesktop/portal/documents") + var mountPoint []byte + if err := portal.Call("org.freedesktop.portal.Documents.GetMountPoint", 0).Store(&mountPoint); err != nil { + // It is not considered an error if + // xdg-document-portal is not available on the system. + if dbusErr, ok := err.(dbus.Error); ok && dbusErr.Name == "org.freedesktop.DBus.Error.ServiceUnknown" { + // We ignore errors here: if writing the file + // fails, we'll just try connecting to D-Bus + // again next time. + if err = ioutil.WriteFile(portalsUnavailableFile, []byte(""), 0644); err != nil { + logger.Noticef("WARNING: cannot write file at %s: %s", portalsUnavailableFile, err) + } + return nil + } + return err + } + + // Sanity check to make sure the document portal is exposed + // where we think it is. + actualMountPoint := strings.TrimRight(string(mountPoint), "\x00") + if actualMountPoint != expectedMountPoint { + return fmt.Errorf(i18n.G("Expected portal at %#v, got %#v"), expectedMountPoint, actualMountPoint) + } + return nil +} + +func (x *cmdRun) runCmdUnderGdb(origCmd, env []string) error { + env = append(env, "SNAP_CONFINE_RUN_UNDER_GDB=1") + + cmd := []string{"sudo", "-E", "gdb", "-ex=run", "-ex=catch exec", "-ex=continue", "--args"} + cmd = append(cmd, origCmd...) + + gcmd := exec.Command(cmd[0], cmd[1:]...) + gcmd.Stdin = os.Stdin + gcmd.Stdout = os.Stdout + gcmd.Stderr = os.Stderr + gcmd.Env = env + return gcmd.Run() +} + +func (x *cmdRun) runCmdWithTraceExec(origCmd, env []string) error { + // setup private tmp dir with strace fifo + straceTmp, err := ioutil.TempDir("", "exec-trace") + if err != nil { + return err + } + defer os.RemoveAll(straceTmp) + straceLog := filepath.Join(straceTmp, "strace.fifo") + if err := syscall.Mkfifo(straceLog, 0640); err != nil { + return err + } + // ensure we have one writer on the fifo so that if strace fails + // nothing blocks + fw, err := os.OpenFile(straceLog, os.O_RDWR, 0640) + if err != nil { + return err + } + defer fw.Close() + + // read strace data from fifo async + var slg *strace.ExecveTiming + var straceErr error + doneCh := make(chan bool, 1) + go func() { + // FIXME: make this configurable? + nSlowest := 10 + slg, straceErr = strace.TraceExecveTimings(straceLog, nSlowest) + close(doneCh) + }() + + cmd, err := strace.TraceExecCommand(straceLog, origCmd...) + if err != nil { + return err + } + // run + cmd.Env = env + cmd.Stdin = Stdin + cmd.Stdout = Stdout + cmd.Stderr = Stderr + err = cmd.Run() + // ensure we close the fifo here so that the strace.TraceExecCommand() + // helper gets a EOF from the fifo (i.e. all writers must be closed + // for this) + fw.Close() + + // wait for strace reader + <-doneCh + if straceErr == nil { + slg.Display(Stderr) + } else { + logger.Noticef("cannot extract runtime data: %v", straceErr) + } + return err +} + +func (x *cmdRun) runCmdUnderStrace(origCmd, env []string) error { + extraStraceOpts, raw, err := x.straceOpts() + if err != nil { + return err + } + cmd, err := strace.Command(extraStraceOpts, origCmd...) + if err != nil { + return err + } + + // run with filter + cmd.Env = env + cmd.Stdin = Stdin + cmd.Stdout = Stdout + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + filterDone := make(chan bool, 1) + go func() { + defer func() { filterDone <- true }() + + if raw { + // Passing --strace='--raw' disables the filtering of + // early strace output. This is useful when tracking + // down issues with snap helpers such as snap-confine, + // snap-exec ... + io.Copy(Stderr, stderr) + return + } + + r := bufio.NewReader(stderr) + + // The first thing from strace if things work is + // "exeve(" - show everything until we see this to + // not swallow real strace errors. + for { + s, err := r.ReadString('\n') + if err != nil { + break + } + if strings.Contains(s, "execve(") { + break + } + fmt.Fprint(Stderr, s) + } + + // The last thing that snap-exec does is to + // execve() something inside the snap dir so + // we know that from that point on the output + // will be interessting to the user. + // + // We need check both /snap (which is where snaps + // are located inside the mount namespace) and the + // distro snap mount dir (which is different on e.g. + // fedora/arch) to fully work with classic snaps. + needle1 := fmt.Sprintf(`execve("%s`, dirs.SnapMountDir) + needle2 := `execve("/snap` + for { + s, err := r.ReadString('\n') + if err != nil { + if err != io.EOF { + fmt.Fprintf(Stderr, "cannot read strace output: %s\n", err) + } + break + } + // Ensure we catch the execve but *not* the + // exec into + // /snap/core/current/usr/lib/snapd/snap-confine + // which is just `snap run` using the core version + // snap-confine. + if (strings.Contains(s, needle1) || strings.Contains(s, needle2)) && !strings.Contains(s, "usr/lib/snapd/snap-confine") { + fmt.Fprint(Stderr, s) + break + } + } + io.Copy(Stderr, r) + }() + if err := cmd.Start(); err != nil { + return err + } + <-filterDone + err = cmd.Wait() + return err +} + +func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook string, args []string) error { + snapConfine := filepath.Join(dirs.DistroLibExecDir, "snap-confine") + // if we re-exec, we must run the snap-confine from the core/snapd snap + // as well, if they get out of sync, havoc will happen + if isReexeced() { + // exe is something like /snap/{snapd,core}/123/usr/bin/snap + exe, err := osReadlink("/proc/self/exe") + if err != nil { + return err + } + // snapBase will be "/snap/{core,snapd}/$rev/" because + // the snap binary is always at $root/usr/bin/snap + snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", "..")) + // Run snap-confine from the core/snapd snap. That + // will work because snap-confine on the core/snapd snap is + // mostly statically linked (except libudev and libc) + snapConfine = filepath.Join(snapBase, dirs.CoreLibExecDir, "snap-confine") + } + + if !osutil.FileExists(snapConfine) { + if hook != "" { + logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.InstanceName()) + return nil + } + return fmt.Errorf(i18n.G("missing snap-confine: try updating your core/snapd package")) + } + + if err := createUserDataDirs(info); err != nil { + logger.Noticef("WARNING: cannot create user data directory: %s", err) + } + + xauthPath, err := migrateXauthority(info) + if err != nil { + logger.Noticef("WARNING: cannot copy user Xauthority file: %s", err) + } + + if err := activateXdgDocumentPortal(info, snapApp, hook); err != nil { + logger.Noticef("WARNING: cannot start document portal: %s", err) + } + + cmd := []string{snapConfine} + if info.NeedsClassic() { + cmd = append(cmd, "--classic") + } + if info.Base != "" { + cmd = append(cmd, "--base", info.Base) + } + cmd = append(cmd, securityTag) + + // when under confinement, snap-exec is run from 'core' snap rootfs + snapExecPath := filepath.Join(dirs.CoreLibExecDir, "snap-exec") + + if info.NeedsClassic() { + // running with classic confinement, carefully pick snap-exec we + // are going to use + if isReexeced() { + // same rule as when choosing the location of snap-confine + snapExecPath = filepath.Join(dirs.SnapMountDir, "core/current", + dirs.CoreLibExecDir, "snap-exec") + } else { + // there is no mount namespace where 'core' is the + // rootfs, hence we need to use distro's snap-exec + snapExecPath = filepath.Join(dirs.DistroLibExecDir, "snap-exec") + } + } + cmd = append(cmd, snapExecPath) + + if x.Shell { + cmd = append(cmd, "--command=shell") + } + if x.Gdb { + cmd = append(cmd, "--command=gdb") + } + if x.Command != "" { + cmd = append(cmd, "--command="+x.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...) + + extraEnv := make(map[string]string) + if len(xauthPath) > 0 { + extraEnv["XAUTHORITY"] = xauthPath + } + env := snapenv.ExecEnv(info, extraEnv) + + if x.TraceExec { + return x.runCmdWithTraceExec(cmd, env) + } else if x.Gdb { + return x.runCmdUnderGdb(cmd, env) + } else if x.useStrace() { + return x.runCmdUnderStrace(cmd, env) + } else { + return syscallExec(cmd[0], cmd, env) + } +} diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go new file mode 100644 index 00000000..beb7b2e2 --- /dev/null +++ b/cmd/snap/cmd_run_test.go @@ -0,0 +1,1111 @@ +// -*- 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 ( + "errors" + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + "time" + + "gopkg.in/check.v1" + + snaprun "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/selinux" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/x11" +) + +var mockYaml = []byte(`name: snapname +version: 1.0 +apps: + app: + command: run-app +hooks: + configure: +`) + +func (s *SnapSuite) TestInvalidParameters(c *check.C) { + invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "--", "snap-name"} + _, err := snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*") + + invalidParameters = []string{"run", "--hook=configure", "--timer=10:00-12:00", "--", "snap-name"} + _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*") + + invalidParameters = []string{"run", "--command=command-name", "--timer=10:00-12:00", "--", "snap-name"} + _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*") + + invalidParameters = []string{"run", "-r=1", "--command=command-name", "--", "snap-name"} + _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") + + invalidParameters = []string{"run", "-r=1", "--", "snap-name"} + _, err = snaprun.Parser(snaprun.Client()).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(snaprun.Client()).ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*") +} + +func (s *SnapSuite) TestSnapRunWhenMissingConfine(c *check.C) { + _, r := logger.MockLogger() + defer r() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // redirect exec + var execs [][]string + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execs = append(execs, args) + return nil + }) + defer restorer() + + // and run it! + // a regular run will fail + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, `.* your core/snapd package`) + // a hook run will not fail + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname"}) + c.Assert(err, check.IsNil) + + // but nothing is run ever + c.Check(execs, check.IsNil) +} + +func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // 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(snaprun.Client()).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.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunClassicAppIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // 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(snaprun.Client()).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.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "--classic", + "snap.snapname.app", + filepath.Join(dirs.DistroLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") + +} + +func (s *SnapSuite) TestSnapRunClassicAppIntegrationReexeced(c *check.C) { + mountedCorePath := filepath.Join(dirs.SnapMountDir, "core/current") + mountedCoreLibExecPath := filepath.Join(mountedCorePath, dirs.CoreLibExecDir) + + defer mockSnapConfine(mountedCoreLibExecPath)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + restore := snaprun.MockOsReadlink(func(name string) (string, error) { + // pretend 'snap' is reexeced from 'core' + return filepath.Join(mountedCorePath, "usr/bin/snap"), nil + }) + defer restore() + + execArgs := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArgs = args + return nil + }) + defer restorer() + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(mountedCoreLibExecPath, "snap-confine"), "--classic", + "snap.snapname.app", + filepath.Join(mountedCoreLibExecPath, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) +} + +func (s *SnapSuite) TestSnapRunAppWithCommandIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &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() + + // and run it! + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--command=my-command", "--", "snapname.app", "arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "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) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, check.IsNil) + info.SideInfo.Revision = snap.R(42) + info.InstanceKey = "foo" + + 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_foo/42")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/common")), check.Equals, true) + // mount point for snap instance mapping has been created + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname")), check.Equals, true) + // and it's empty inside + m, err := filepath.Glob(filepath.Join(fakeHome, "/snap/snapname/*")) + c.Assert(err, check.IsNil) + c.Assert(m, check.HasLen, 0) +} + +func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &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 from the active revision + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &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() + + // Specifically pass "unset" which would use the active version. + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=unset", "--", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + // Create both revisions 41 and 42 + snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(41), + }) + snaptest.MockSnap(c, string(mockYaml), &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(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=41", "--", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41") +} + +func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) { + // Only create revision 42 + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // 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(snaprun.Client()).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(snaprun.Client()).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) { + // Only create revision 42 + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // redirect exec + called := false + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + called = true + return nil + }) + defer restorer() + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=missing-hook", "--", "snapname"}) + 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(snaprun.Client()).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(snaprun.Client()).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(snaprun.Client()).ParseArgs([]string{"run", "--", "not-there"}) + c.Assert(err, check.ErrorMatches, fmt.Sprintf("cannot find current revision for snap not-there: readlink %s/not-there/current: no such file or directory", dirs.SnapMountDir)) +} + +func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // 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(snaprun.Client()).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") +} + +func (s *SnapSuite) TestSnapRunIsReexeced(c *check.C) { + var osReadlinkResult string + restore := snaprun.MockOsReadlink(func(name string) (string, error) { + return osReadlinkResult, nil + }) + defer restore() + + for _, t := range []struct { + readlink string + expected bool + }{ + {filepath.Join(dirs.SnapMountDir, dirs.CoreLibExecDir, "snapd"), true}, + {filepath.Join(dirs.DistroLibExecDir, "snapd"), false}, + } { + osReadlinkResult = t.readlink + c.Check(snaprun.IsReexeced(), check.Equals, t.expected) + } +} + +func (s *SnapSuite) TestSnapRunAppIntegrationFromCore(c *check.C) { + defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // pretend to be running from core + restorer := snaprun.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil + }) + defer restorer() + + // 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(snaprun.Client()).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.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunAppIntegrationFromSnapd(c *check.C) { + defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "snapd", "222", dirs.CoreLibExecDir))() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // pretend to be running from snapd + restorer := snaprun.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.SnapMountDir, "snapd/222/usr/bin/snap"), nil + }) + defer restorer() + + // 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(snaprun.Client()).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.SnapMountDir, "/snapd/222", dirs.CoreLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.SnapMountDir, "/snapd/222", dirs.CoreLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunXauthorityMigration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + u, err := user.Current() + c.Assert(err, check.IsNil) + + // Ensure XDG_RUNTIME_DIR exists for the user we're testing with + err = os.MkdirAll(filepath.Join(dirs.XdgRuntimeDirBase, u.Uid), 0700) + c.Assert(err, check.IsNil) + + // mock installed snap; happily this also gives us a directory + // below /tmp which the Xauthority migration expects. + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // 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() + + xauthPath, err := x11.MockXauthority(2) + c.Assert(err, check.IsNil) + defer os.Remove(xauthPath) + + defer snaprun.MockGetEnv(func(name string) string { + if name == "XAUTHORITY" { + return xauthPath + } + return "" + })() + + // and run it! + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app"}) + + expectedXauthPath := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid, ".Xauthority") + c.Check(execEnv, testutil.Contains, fmt.Sprintf("XAUTHORITY=%s", expectedXauthPath)) + + info, err := os.Stat(expectedXauthPath) + c.Assert(err, check.IsNil) + c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0600)) + + err = x11.ValidateXauthorityFile(expectedXauthPath) + c.Assert(err, check.IsNil) +} + +// build the args for a hypothetical completer +func mkCompArgs(compPoint string, argv ...string) []string { + out := []string{ + "99", // COMP_TYPE + "99", // COMP_KEY + "", // COMP_POINT + "2", // COMP_CWORD + " ", // COMP_WORDBREAKS + } + out[2] = compPoint + out = append(out, strings.Join(argv, " ")) + out = append(out, argv...) + return out +} + +func (s *SnapSuite) TestAntialiasHappy(c *check.C) { + c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil) + + inArgs := mkCompArgs("10", "alias", "alias", "bo-alias") + + // first not so happy because no alias symlink + app, outArgs := snaprun.Antialias("alias", inArgs) + c.Check(app, check.Equals, "alias") + c.Check(outArgs, check.DeepEquals, inArgs) + + c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil) + + // now really happy + app, outArgs = snaprun.Antialias("alias", inArgs) + c.Check(app, check.Equals, "an-app") + c.Check(outArgs, check.DeepEquals, []string{ + "99", // COMP_TYPE (no change) + "99", // COMP_KEY (no change) + "11", // COMP_POINT (+1 because "an-app" is one longer than "alias") + "2", // COMP_CWORD (no change) + " ", // COMP_WORDBREAKS (no change) + "an-app alias bo-alias", // COMP_LINE (argv[0] changed) + "an-app", // argv (arv[0] changed) + "alias", + "bo-alias", + }) +} + +func (s *SnapSuite) TestAntialiasBailsIfUnhappy(c *check.C) { + // alias exists but args are somehow wonky + c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil) + c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil) + + // weird1 has COMP_LINE not start with COMP_WORDS[0], argv[0] equal to COMP_WORDS[0] + weird1 := mkCompArgs("6", "alias", "") + weird1[5] = "xxxxx " + // weird2 has COMP_LINE not start with COMP_WORDS[0], argv[0] equal to the first word in COMP_LINE + weird2 := mkCompArgs("6", "xxxxx", "") + weird2[5] = "alias " + + for desc, inArgs := range map[string][]string{ + "nil args": nil, + "too-short args": {"alias"}, + "COMP_POINT not a number": mkCompArgs("hello", "alias"), + "COMP_POINT is inside argv[0]": mkCompArgs("2", "alias", ""), + "COMP_POINT is outside argv": mkCompArgs("99", "alias", ""), + "COMP_WORDS[0] is not argv[0]": mkCompArgs("10", "not-alias", ""), + "mismatch between argv[0], COMP_LINE and COMP_WORDS, #1": weird1, + "mismatch between argv[0], COMP_LINE and COMP_WORDS, #2": weird2, + } { + // antialias leaves args alone if it's too short + app, outArgs := snaprun.Antialias("alias", inArgs) + c.Check(app, check.Equals, "alias", check.Commentf(desc)) + c.Check(outArgs, check.DeepEquals, inArgs, check.Commentf(desc)) + } +} + +func (s *SnapSuite) TestSnapRunAppWithStraceIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // pretend we have sudo and simulate some useful output that would + // normally come from strace + sudoCmd := testutil.MockCommand(c, "sudo", fmt.Sprintf(` +echo "stdout output 1" +>&2 echo 'execve("/path/to/snap-confine")' +>&2 echo "snap-confine/snap-exec strace stuff" +>&2 echo "getuid() = 1000" +>&2 echo 'execve("%s/snapName/x2/bin/foo")' +>&2 echo "interessting strace output" +>&2 echo "and more" +echo "stdout output 2" +`, dirs.SnapMountDir)) + defer sudoCmd.Restore() + + // pretend we have strace + straceCmd := testutil.MockCommand(c, "strace", "") + defer straceCmd.Restore() + + user, err := user.Current() + c.Assert(err, check.IsNil) + + // and run it under strace + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--strace", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{ + { + "sudo", "-E", + filepath.Join(straceCmd.BinDir(), "strace"), + "-u", user.Username, + "-f", + "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2", + }, + }) + c.Check(s.Stdout(), check.Equals, "stdout output 1\nstdout output 2\n") + c.Check(s.Stderr(), check.Equals, fmt.Sprintf("execve(%q)\ninteressting strace output\nand more\n", filepath.Join(dirs.SnapMountDir, "snapName/x2/bin/foo"))) + + s.ResetStdStreams() + sudoCmd.ForgetCalls() + + // try again without filtering + rest, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--strace=--raw", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{ + { + "sudo", "-E", + filepath.Join(straceCmd.BinDir(), "strace"), + "-u", user.Username, + "-f", + "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2", + }, + }) + c.Check(s.Stdout(), check.Equals, "stdout output 1\nstdout output 2\n") + expectedFullFmt := `execve("/path/to/snap-confine") +snap-confine/snap-exec strace stuff +getuid() = 1000 +execve("%s/snapName/x2/bin/foo") +interessting strace output +and more +` + c.Check(s.Stderr(), check.Equals, fmt.Sprintf(expectedFullFmt, dirs.SnapMountDir)) +} + +func (s *SnapSuite) TestSnapRunAppWithStraceOptions(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // pretend we have sudo + sudoCmd := testutil.MockCommand(c, "sudo", "") + defer sudoCmd.Restore() + + // pretend we have strace + straceCmd := testutil.MockCommand(c, "strace", "") + defer straceCmd.Restore() + + user, err := user.Current() + c.Assert(err, check.IsNil) + + // and run it under strace + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--strace=-tt --raw -o "file with spaces"`, "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{ + { + "sudo", "-E", + filepath.Join(straceCmd.BinDir(), "strace"), + "-u", user.Username, + "-f", + "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", + "-tt", + "-o", + "file with spaces", + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2", + }, + }) +} + +func (s *SnapSuite) TestSnapRunShellIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // 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(snaprun.Client()).ParseArgs([]string{"run", "--shell", "--", "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.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--command=shell", "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunAppTimer(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execCalled := false + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execCalled = true + return nil + }) + defer restorer() + + fakeNow := time.Date(2018, 02, 12, 9, 55, 0, 0, time.Local) + restorer = snaprun.MockTimeNow(func() time.Time { + // Monday Feb 12, 9:55 + return fakeNow + }) + defer restorer() + + // pretend we are outside of timer range + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--timer="mon,10:00~12:00,,fri,13:00"`, "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Assert(execCalled, check.Equals, false) + + c.Check(s.Stderr(), check.Equals, fmt.Sprintf(`%s: attempted to run "snapname.app" timer outside of scheduled time "mon,10:00~12:00,,fri,13:00" +`, fakeNow.Format(time.RFC3339))) + s.ResetStdStreams() + + restorer = snaprun.MockTimeNow(func() time.Time { + // Monday Feb 12, 10:20 + return time.Date(2018, 02, 12, 10, 20, 0, 0, time.Local) + }) + defer restorer() + + // and run it under strace + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--timer="mon,10:00~12:00,,fri,13:00"`, "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(execCalled, check.Equals, true) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) +} + +func (s *SnapSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("1"), + }) + + // pretend we have sudo + sudoCmd := testutil.MockCommand(c, "sudo", "echo unhappy; exit 12") + defer sudoCmd.Restore() + + // pretend we have strace + straceCmd := testutil.MockCommand(c, "strace", "") + defer straceCmd.Restore() + + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--trace-exec", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, "exit status 12") + c.Assert(rest, check.DeepEquals, []string{"--", "snapname.app", "--arg1", "arg2"}) + c.Check(s.Stdout(), check.Equals, "unhappy\n") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) { + logbuf, restorer := logger.MockLogger() + defer restorer() + + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + fakeHome := c.MkDir() + restorer = snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + // redirect exec + execCalled := 0 + restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error { + execCalled++ + return nil + }) + defer restorer() + + verifyCalls := 0 + restoreCalls := 0 + isEnabledCalls := 0 + enabled := false + verify := true + + snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir) + + restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) { + c.Check(what, check.Equals, snapUserDir) + verifyCalls++ + return verify, nil + }) + defer restorer() + + restorer = snaprun.MockSELinuxRestoreContext(func(what string, mode selinux.RestoreMode) error { + c.Check(mode, check.Equals, selinux.RestoreMode{Recursive: true}) + c.Check(what, check.Equals, snapUserDir) + restoreCalls++ + return nil + }) + defer restorer() + + restorer = snaprun.MockSELinuxIsEnabled(func() (bool, error) { + isEnabledCalls++ + return enabled, nil + }) + defer restorer() + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 1) + c.Check(isEnabledCalls, check.Equals, 1) + c.Check(verifyCalls, check.Equals, 0) + c.Check(restoreCalls, check.Equals, 0) + + // pretend SELinux is on + enabled = true + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 2) + c.Check(isEnabledCalls, check.Equals, 2) + c.Check(verifyCalls, check.Equals, 1) + c.Check(restoreCalls, check.Equals, 0) + + // pretend the context does not match + verify = false + + logbuf.Reset() + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 3) + c.Check(isEnabledCalls, check.Equals, 3) + c.Check(verifyCalls, check.Equals, 2) + c.Check(restoreCalls, check.Equals, 1) + + // and we let the user know what we're doing + c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("restoring default SELinux context of %s", snapUserDir)) +} + +func (s *SnapSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) { + logbuf, restorer := logger.MockLogger() + defer restorer() + + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + fakeHome := c.MkDir() + restorer = snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + // redirect exec + execCalled := 0 + restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error { + execCalled++ + return nil + }) + defer restorer() + + verifyCalls := 0 + restoreCalls := 0 + isEnabledCalls := 0 + enabledErr := errors.New("enabled failed") + verifyErr := errors.New("verify failed") + restoreErr := errors.New("restore failed") + + snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir) + + restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) { + c.Check(what, check.Equals, snapUserDir) + verifyCalls++ + return false, verifyErr + }) + defer restorer() + + restorer = snaprun.MockSELinuxRestoreContext(func(what string, mode selinux.RestoreMode) error { + c.Check(mode, check.Equals, selinux.RestoreMode{Recursive: true}) + c.Check(what, check.Equals, snapUserDir) + restoreCalls++ + return restoreErr + }) + defer restorer() + + restorer = snaprun.MockSELinuxIsEnabled(func() (bool, error) { + isEnabledCalls++ + return enabledErr == nil, enabledErr + }) + defer restorer() + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + // these errors are only logged, but we still run the snap + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 1) + c.Check(logbuf.String(), testutil.Contains, "cannot determine SELinux status: enabled failed") + c.Check(isEnabledCalls, check.Equals, 1) + c.Check(verifyCalls, check.Equals, 0) + c.Check(restoreCalls, check.Equals, 0) + // pretend selinux is on + enabledErr = nil + + logbuf.Reset() + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 2) + c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("failed to verify SELinux context of %s: verify failed", snapUserDir)) + c.Check(isEnabledCalls, check.Equals, 2) + c.Check(verifyCalls, check.Equals, 1) + c.Check(restoreCalls, check.Equals, 0) + + // pretend the context does not match + verifyErr = nil + + logbuf.Reset() + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 3) + c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("cannot restore SELinux context of %s: restore failed", snapUserDir)) + c.Check(isEnabledCalls, check.Equals, 3) + c.Check(verifyCalls, check.Equals, 2) + c.Check(restoreCalls, check.Equals, 1) +} diff --git a/cmd/snap/cmd_sandbox_features.go b/cmd/snap/cmd_sandbox_features.go new file mode 100644 index 00000000..385a2c09 --- /dev/null +++ b/cmd/snap/cmd_sandbox_features.go @@ -0,0 +1,88 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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/i18n" +) + +var shortSandboxFeaturesHelp = i18n.G("Print sandbox features available on the system") +var longSandboxFeaturesHelp = i18n.G(` +The sandbox command prints tags describing features of individual sandbox +components used by snapd on a given system. +`) + +type cmdSandboxFeatures struct { + clientMixin + Required []string `long:"required" arg-name:""` +} + +func init() { + addDebugCommand("sandbox-features", shortSandboxFeaturesHelp, longSandboxFeaturesHelp, func() flags.Commander { + return &cmdSandboxFeatures{} + }, map[string]string{ + "required": i18n.G("Ensure that given backend:feature is available"), + }, nil) +} + +func (cmd cmdSandboxFeatures) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + sysInfo, err := cmd.client.SysInfo() + if err != nil { + return err + } + + sandboxFeatures := sysInfo.SandboxFeatures + + if len(cmd.Required) > 0 { + avail := make(map[string]bool) + for backend := range sandboxFeatures { + for _, feature := range sandboxFeatures[backend] { + avail[fmt.Sprintf("%s:%s", backend, feature)] = true + } + } + for _, required := range cmd.Required { + if !avail[required] { + return fmt.Errorf("sandbox feature not available: %q", required) + } + } + } else { + backends := make([]string, 0, len(sandboxFeatures)) + for backend := range sandboxFeatures { + backends = append(backends, backend) + } + sort.Strings(backends) + w := tabWriter() + defer w.Flush() + for _, backend := range backends { + fmt.Fprintf(w, "%s:\t%s\n", backend, strings.Join(sandboxFeatures[backend], " ")) + } + } + return nil +} diff --git a/cmd/snap/cmd_sandbox_features_test.go b/cmd/snap/cmd_sandbox_features_test.go new file mode 100644 index 00000000..765ab41a --- /dev/null +++ b/cmd/snap/cmd_sandbox_features_test.go @@ -0,0 +1,61 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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) TestSandboxFeatures(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"sandbox-features": {"apparmor": ["a", "b", "c"], "selinux": ["1", "2", "3"]}}}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "sandbox-features"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "apparmor: a b c\n"+ + "selinux: 1 2 3\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestSandboxFeaturesRequired(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"sandbox-features": {"apparmor": ["a", "b", "c"], "selinux": ["1", "2", "3"]}}}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "sandbox-features", "--required=apparmor:a", "--required=selinux:2"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestSandboxFeaturesRequiredButMissing(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"sandbox-features": {"apparmor": ["a", "b", "c"], "selinux": ["1", "2", "3"]}}}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "sandbox-features", "--required=magic:thing"}) + c.Assert(err, ErrorMatches, `sandbox feature not available: "magic:thing"`) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_services.go b/cmd/snap/cmd_services.go new file mode 100644 index 00000000..6abb5695 --- /dev/null +++ b/cmd/snap/cmd_services.go @@ -0,0 +1,264 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-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 . + * + */ + +package main + +import ( + "fmt" + "strconv" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/i18n" +) + +type svcStatus struct { + clientMixin + Positional struct { + ServiceNames []serviceName + } `positional-args:"yes"` +} + +type svcLogs struct { + clientMixin + N string `short:"n" default:"10"` + Follow bool `short:"f"` + Positional struct { + ServiceNames []serviceName `required:"1"` + } `positional-args:"yes" required:"yes"` +} + +var ( + shortServicesHelp = i18n.G("Query the status of services") + longServicesHelp = i18n.G(` +The services command lists information about the services specified, or about +the services in all currently installed snaps. +`) + shortLogsHelp = i18n.G("Retrieve logs for services") + longLogsHelp = i18n.G(` +The logs command fetches logs of the given services and displays them in +chronological order. +`) + shortStartHelp = i18n.G("Start services") + longStartHelp = i18n.G(` +The start command starts, and optionally enables, the given services. +`) + shortStopHelp = i18n.G("Stop services") + longStopHelp = i18n.G(` +The stop command stops, and optionally disables, the given services. +`) + shortRestartHelp = i18n.G("Restart services") + longRestartHelp = i18n.G(` +The restart command restarts the given services. + +If the --reload option is given, for each service whose app has a reload +command, a reload is performed instead of a restart. +`) +) + +func init() { + argdescs := []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("A service specification, which can be just a snap name (for all services in the snap), or . for a single service."), + }} + addCommand("services", shortServicesHelp, longServicesHelp, func() flags.Commander { return &svcStatus{} }, nil, argdescs) + addCommand("logs", shortLogsHelp, longLogsHelp, func() flags.Commander { return &svcLogs{} }, + map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "n": i18n.G("Show only the given number of lines, or 'all'."), + // TRANSLATORS: This should not start with a lowercase letter. + "f": i18n.G("Wait for new lines and print them as they come in."), + }, argdescs) + + addCommand("start", shortStartHelp, longStartHelp, func() flags.Commander { return &svcStart{} }, + waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "enable": i18n.G("As well as starting the service now, arrange for it to be started on boot."), + }), argdescs) + addCommand("stop", shortStopHelp, longStopHelp, func() flags.Commander { return &svcStop{} }, + waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "disable": i18n.G("As well as stopping the service now, arrange for it to no longer be started on boot."), + }), argdescs) + addCommand("restart", shortRestartHelp, longRestartHelp, func() flags.Commander { return &svcRestart{} }, + waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "reload": i18n.G("If the service has a reload command, use it instead of restarting."), + }), argdescs) +} + +func svcNames(s []serviceName) []string { + svcNames := make([]string, len(s)) + for i, svcName := range s { + svcNames[i] = string(svcName) + } + return svcNames +} + +func (s *svcStatus) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + services, err := s.client.Apps(svcNames(s.Positional.ServiceNames), client.AppOptions{Service: true}) + if err != nil { + return err + } + + if len(services) == 0 { + fmt.Fprintln(Stderr, i18n.G("There are no services provided by installed snaps.")) + return nil + } + + w := tabWriter() + defer w.Flush() + + fmt.Fprintln(w, i18n.G("Service\tStartup\tCurrent\tNotes")) + + for _, svc := range services { + startup := i18n.G("disabled") + if svc.Enabled { + startup = i18n.G("enabled") + } + current := i18n.G("inactive") + if svc.Active { + current = i18n.G("active") + } + fmt.Fprintf(w, "%s.%s\t%s\t%s\t%s\n", svc.Snap, svc.Name, startup, current, cmd.ClientAppInfoNotes(svc)) + } + + return nil +} + +func (s *svcLogs) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + sN := -1 + if s.N != "all" { + n, err := strconv.ParseInt(s.N, 0, 32) + if n < 0 || err != nil { + return fmt.Errorf(i18n.G("invalid argument for flag ‘-n’: expected a non-negative integer argument, or “all”.")) + } + sN = int(n) + } + + logs, err := s.client.Logs(svcNames(s.Positional.ServiceNames), client.LogOptions{N: sN, Follow: s.Follow}) + if err != nil { + return err + } + + for log := range logs { + fmt.Fprintln(Stdout, log) + } + + return nil +} + +type svcStart struct { + waitMixin + Positional struct { + ServiceNames []serviceName `required:"1"` + } `positional-args:"yes" required:"yes"` + Enable bool `long:"enable"` +} + +func (s *svcStart) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + names := svcNames(s.Positional.ServiceNames) + changeID, err := s.client.Start(names, client.StartOptions{Enable: s.Enable}) + if err != nil { + return err + } + if _, err := s.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + fmt.Fprintf(Stdout, i18n.G("Started.\n")) + + return nil +} + +type svcStop struct { + waitMixin + Positional struct { + ServiceNames []serviceName `required:"1"` + } `positional-args:"yes" required:"yes"` + Disable bool `long:"disable"` +} + +func (s *svcStop) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + names := svcNames(s.Positional.ServiceNames) + changeID, err := s.client.Stop(names, client.StopOptions{Disable: s.Disable}) + if err != nil { + return err + } + if _, err := s.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + fmt.Fprintf(Stdout, i18n.G("Stopped.\n")) + + return nil +} + +type svcRestart struct { + waitMixin + Positional struct { + ServiceNames []serviceName `required:"1"` + } `positional-args:"yes" required:"yes"` + Reload bool `long:"reload"` +} + +func (s *svcRestart) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + names := svcNames(s.Positional.ServiceNames) + changeID, err := s.client.Restart(names, client.RestartOptions{Reload: s.Reload}) + if err != nil { + return err + } + if _, err := s.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + fmt.Fprintf(Stdout, i18n.G("Restarted.\n")) + + return nil +} diff --git a/cmd/snap/cmd_services_test.go b/cmd/snap/cmd_services_test.go new file mode 100644 index 00000000..bd7355f1 --- /dev/null +++ b/cmd/snap/cmd_services_test.go @@ -0,0 +1,254 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-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 . + * + */ + +package main_test + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +type appOpSuite struct { + BaseSnapSuite + + restoreAll func() +} + +var _ = check.Suite(&appOpSuite{}) + +func (s *appOpSuite) 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() + } +} + +func (s *appOpSuite) TearDownTest(c *check.C) { + s.restoreAll() + s.BaseSnapSuite.TearDownTest(c) +} + +func (s *appOpSuite) expectedBody(op string, names []string, extra []string) map[string]interface{} { + inames := make([]interface{}, len(names)) + for i, name := range names { + inames[i] = name + } + expectedBody := map[string]interface{}{ + "action": op, + "names": inames, + } + for _, x := range extra { + expectedBody[x] = true + } + return expectedBody +} + +func (s *appOpSuite) args(op string, names []string, extra []string, noWait bool) []string { + args := []string{op} + if noWait { + args = append(args, "--no-wait") + } + for _, x := range extra { + args = append(args, "--"+x) + } + args = append(args, names...) + return args +} + +func (s *appOpSuite) testOpNoArgs(c *check.C, op string) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{op}) + c.Assert(err, check.ErrorMatches, `.* required argument .* not provided`) +} + +func (s *appOpSuite) testOpErrorResponse(c *check.C, op string, names []string, extra []string, noWait bool) { + n := 0 + s.RedirectClientToTestServer(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/apps") + c.Check(r.URL.Query(), check.HasLen, 0) + c.Check(DecodedRequestBody(c, r), check.DeepEquals, s.expectedBody(op, names, extra)) + w.WriteHeader(400) + fmt.Fprintln(w, `{"type": "error", "result": {"message": "error"}, "status-code": 400}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + + _, err := snap.Parser(snap.Client()).ParseArgs(s.args(op, names, extra, noWait)) + c.Assert(err, check.ErrorMatches, "error") + c.Check(n, check.Equals, 1) +} + +func (s *appOpSuite) testOp(c *check.C, op, summary string, names []string, extra []string, noWait bool) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/apps") + c.Check(r.URL.Query(), check.HasLen, 0) + c.Check(DecodedRequestBody(c, r), check.DeepEquals, s.expectedBody(op, names, extra)) + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(202) + 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"}}`) + default: + c.Fatalf("expected to get 2 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs(s.args(op, names, extra, noWait)) + c.Assert(err, check.IsNil) + c.Assert(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + expectedN := 3 + if noWait { + summary = "42" + expectedN = 1 + } + c.Check(s.Stdout(), check.Equals, summary+"\n") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, expectedN) +} + +func (s *appOpSuite) TestAppOps(c *check.C) { + extras := []string{"enable", "disable", "reload"} + summaries := []string{"Started.", "Stopped.", "Restarted."} + for i, op := range []string{"start", "stop", "restart"} { + s.testOpNoArgs(c, op) + for _, extra := range [][]string{nil, {extras[i]}} { + for _, noWait := range []bool{false, true} { + for _, names := range [][]string{ + {"foo"}, + {"foo", "bar"}, + {"foo", "bar.baz"}, + } { + s.testOpErrorResponse(c, op, names, extra, noWait) + s.testOp(c, op, summaries[i], names, extra, noWait) + s.stdout.Reset() + } + } + } + } +} + +func (s *appOpSuite) TestAppStatus(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/apps") + c.Check(r.URL.Query(), check.HasLen, 1) + c.Check(r.URL.Query().Get("select"), check.Equals, "service") + c.Check(r.Method, check.Equals, "GET") + w.WriteHeader(200) + enc := json.NewEncoder(w) + enc.Encode(map[string]interface{}{ + "type": "sync", + "result": []map[string]interface{}{ + {"snap": "foo", "name": "bar", "daemon": "oneshot", + "active": false, "enabled": true, + "activators": []map[string]interface{}{ + {"name": "bar", "type": "timer", "active": true, "enabled": true}, + }, + }, {"snap": "foo", "name": "baz", "daemon": "oneshot", + "active": false, "enabled": true, + "activators": []map[string]interface{}{ + {"name": "baz-sock1", "type": "socket", "active": true, "enabled": true}, + {"name": "baz-sock2", "type": "socket", "active": false, "enabled": true}, + }, + }, {"snap": "foo", "name": "zed", + "active": true, "enabled": true, + }, + }, + "status": "OK", + "status-code": 200, + }) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"services"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, `Service Startup Current Notes +foo.bar enabled inactive timer-activated +foo.baz enabled inactive socket-activated +foo.zed enabled active - +`) + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} + +func (s *appOpSuite) TestAppStatusNoServices(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/apps") + c.Check(r.URL.Query(), check.HasLen, 1) + c.Check(r.URL.Query().Get("select"), check.Equals, "service") + c.Check(r.Method, check.Equals, "GET") + w.WriteHeader(200) + enc := json.NewEncoder(w) + enc.Encode(map[string]interface{}{ + "type": "sync", + "result": []map[string]interface{}{}, + "status": "OK", + "status-code": 200, + }) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"services"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.HasLen, 0) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "There are no services provided by installed snaps.\n") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} diff --git a/cmd/snap/cmd_set.go b/cmd/snap/cmd_set.go new file mode 100644 index 00000000..d3afac4b --- /dev/null +++ b/cmd/snap/cmd_set.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 ( + "fmt" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/jsonutil" +) + +var shortSetHelp = i18n.G("Change 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 { + waitMixin + Positional struct { + Snap installedSnapName + ConfValues []string `required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func init() { + addCommand("set", shortSetHelp, longSetHelp, func() flags.Commander { return &cmdSet{} }, waitDescs, []argDesc{ + { + name: "", + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("The snap to configure (e.g. hello-world)"), + }, { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + 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{} + if err := jsonutil.DecodeWithNumber(strings.NewReader(parts[1]), &value); err != nil { + // Not valid JSON-- just save the string as-is. + patchValues[parts[0]] = parts[1] + } else { + patchValues[parts[0]] = value + } + } + + snapName := string(x.Positional.Snap) + id, err := x.client.SetConf(snapName, patchValues) + if err != nil { + return err + } + + if _, err := x.wait(id); err != nil { + if err == noWait { + return nil + } + return err + } + + return nil +} diff --git a/cmd/snap/cmd_set_test.go b/cmd/snap/cmd_set_test.go new file mode 100644 index 00000000..39eb9f5e --- /dev/null +++ b/cmd/snap/cmd_set_test.go @@ -0,0 +1,117 @@ +// -*- 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" + + snapset "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" +) + +var validApplyYaml = []byte(`name: snapname +version: 1.0 +hooks: + configure: +`) + +func (s *SnapSuite) TestInvalidSetParameters(c *check.C) { + invalidParameters := []string{"set", "snap-name", "key", "value"} + _, err := snapset.Parser(snapset.Client()).ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*invalid configuration:.*(want key=value).*") +} + +func (s *SnapSuite) TestSnapSetIntegrationString(c *check.C) { + // mock installed snap + snaptest.MockSnap(c, string(validApplyYaml), &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(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=value"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) TestSnapSetIntegrationNumber(c *check.C) { + // mock installed snap + snaptest.MockSnap(c, string(validApplyYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockSetConfigServer(c, json.Number("1.2")) + + // Set a config value for the active snap + _, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=1.2"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) TestSnapSetIntegrationBigInt(c *check.C) { + snaptest.MockSnap(c, string(validApplyYaml), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockSetConfigServer(c, json.Number("1234567890")) + + // Set a config value for the active snap + _, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=1234567890"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) TestSnapSetIntegrationJson(c *check.C) { + // mock installed snap + snaptest.MockSnap(c, string(validApplyYaml), &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(snapset.Client()).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_sign.go b/cmd/snap/cmd_sign.go new file mode 100644 index 00000000..4abc89bb --- /dev/null +++ b/cmd/snap/cmd_sign.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 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(` +The sign command signs 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{ + // TRANSLATORS: This should not start with a lowercase letter. + "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..9fb76068 --- /dev/null +++ b/cmd/snap/cmd_sign_build.go @@ -0,0 +1,124 @@ +// -*- 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 a snap-build assertion") +var longSignBuildHelp = i18n.G(` +The sign-build command creates a snap-build assertion for the provided +snap file. +`) + +func init() { + cmd := addCommand("sign-build", + shortSignBuildHelp, + longSignBuildHelp, + func() flags.Commander { + return &cmdSignBuild{} + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "developer-id": i18n.G("Identifier of the signer"), + // TRANSLATORS: This should not start with a lowercase letter. + "snap-id": i18n.G("Identifier of the snap package associated with the build"), + // TRANSLATORS: This should not start with a lowercase letter. + "k": i18n.G("Name of the GnuPG key to use (defaults to 'default' as key name)"), + // TRANSLATORS: This should not start with a lowercase letter. + "grade": i18n.G("Grade states the build quality of the snap (defaults to 'stable')"), + }, []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + 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..51f2276e --- /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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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..22273a2d --- /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(snap.Client()).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(snap.Client()).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..bf96af4b --- /dev/null +++ b/cmd/snap/cmd_snap_op.go @@ -0,0 +1,981 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-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 . + * + */ + +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" + "unicode/utf8" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" +) + +var ( + shortInstallHelp = i18n.G("Install snaps on the system") + shortRemoveHelp = i18n.G("Remove snaps from the system") + shortRefreshHelp = i18n.G("Refresh snaps in the system") + shortTryHelp = i18n.G("Test an unpacked snap in the system") + shortEnableHelp = i18n.G("Enable a snap in the system") + shortDisableHelp = i18n.G("Disable a snap in the system") +) + +var longInstallHelp = i18n.G(` +The install command installs the named snaps on the system. + +To install multiple instances of the same snap, append an underscore and a +unique identifier (for each instance) to a snap's name. + +With no further options, the snaps are installed tracking the stable channel, +with strict security confinement. + +Revision choice via the --revision override requires the the user to +have developer access to the snap, either directly or through the +store's collaboration feature, and to be logged in (see 'snap help login'). + +Note a later refresh will typically undo a revision override, taking the snap +back to the current revision of the channel it's tracking. + +Use --name to set the instance name when installing from snap file. +`) + +var longRemoveHelp = i18n.G(` +The remove command removes the named snap instance 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 updates the specified snaps, or all snaps in the system if +none are specified. + +With no further options, the snaps are refreshed to the current revision of the +channel they're tracking, preserving their confinement options. + +Revision choice via the --revision override requires the the user to +have developer access to the snap, either directly or through the +store's collaboration feature, and to be logged in (see 'snap help login'). + +Note a later refresh will typically undo a revision override. +`) + +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 { + waitMixin + + 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 := string(x.Positional.Snaps[0]) + + changeID, err := x.client.Remove(name, opts) + if err != nil { + msg, err := errorToCmdMessage(name, err, opts) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) + return nil + } + + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + if opts.Revision != "" { + fmt.Fprintf(Stdout, i18n.G("%s (revision %s) removed\n"), name, opts.Revision) + } else { + fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name) + } + return nil +} + +func (x *cmdRemove) removeMany(opts *client.SnapOptions) error { + names := installedSnapNames(x.Positional.Snaps) + changeID, err := x.client.RemoveMany(names, opts) + if err != nil { + return err + } + + chg, err := x.wait(changeID) + if err != nil { + if err == noWait { + return 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{ + // TRANSLATORS: This should not start with a lowercase letter. + "channel": i18n.G("Use this channel instead of stable"), + // TRANSLATORS: This should not start with a lowercase letter. + "beta": i18n.G("Install from the beta channel"), + // TRANSLATORS: This should not start with a lowercase letter. + "edge": i18n.G("Install from the edge channel"), + // TRANSLATORS: This should not start with a lowercase letter. + "candidate": i18n.G("Install from the candidate channel"), + // TRANSLATORS: This should not start with a lowercase letter. + "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 + } + + if !strings.Contains(mx.Channel, "/") && mx.Channel != "" && mx.Channel != "edge" && mx.Channel != "beta" && mx.Channel != "candidate" && mx.Channel != "stable" { + // shortcut to jump to a different track, e.g. + // snap install foo --channel=3.4 # implies 3.4/stable + mx.Channel += "/stable" + } + + return nil +} + +// show what has been done +func showDone(cli *client.Client, names []string, op string, opts *client.SnapOptions, esc *escapes) error { + 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 opts != nil && opts.Classic && snap.Confinement != client.ClassicConfinement { + // requested classic but the snap is not classic + head := i18n.G("Warning:") + // TRANSLATORS: the arg is a snap name (e.g. "some-snap") + warn := fill(fmt.Sprintf(i18n.G("flag --classic ignored for strictly confined snap %s"), snap.Name), utf8.RuneCountInString(head)+1) // +1 for the space + fmt.Fprint(Stderr, head, " ", warn, "\n\n") + } + + if snap.Publisher != nil { + // TRANSLATORS: the args are a snap name optionally followed by a channel, then a version, then the developer name (e.g. "some-snap (beta) 1.3 from Alice installed") + fmt.Fprintf(Stdout, i18n.G("%s%s %s from %s installed\n"), snap.Name, channelStr, snap.Version, longPublisher(esc, snap.Publisher)) + } else { + // TRANSLATORS: the args are a snap name optionally followed by a channel, then a version (e.g. "some-snap (beta) 1.3 installed") + fmt.Fprintf(Stdout, i18n.G("%s%s %s installed\n"), snap.Name, channelStr, snap.Version) + } + case "refresh": + if snap.Publisher != nil { + // TRANSLATORS: the args are a snap name optionally followed by a channel, then a version, then the developer name (e.g. "some-snap (beta) 1.3 from Alice refreshed") + fmt.Fprintf(Stdout, i18n.G("%s%s %s from %s refreshed\n"), snap.Name, channelStr, snap.Version, longPublisher(esc, snap.Publisher)) + } else { + // TRANSLATORS: the args are a snap name optionally followed by a channel, then a version (e.g. "some-snap (beta) 1.3 refreshed") + fmt.Fprintf(Stdout, i18n.G("%s%s %s refreshed\n"), snap.Name, channelStr, snap.Version) + } + case "revert": + // TRANSLATORS: first %s is a snap name, second %s is a revision + fmt.Fprintf(Stdout, i18n.G("%s reverted to %s\n"), snap.Name, snap.Version) + default: + fmt.Fprintf(Stdout, "internal error: unknown op %q", op) + } + if snap.TrackingChannel != snap.Channel && snap.Channel != "" { + // TRANSLATORS: first %s is a channel name, following %s is a snap name, last %s is a channel name again. + fmt.Fprintf(Stdout, i18n.G("Channel %s for %s is closed; temporarily forwarding to %s.\n"), snap.TrackingChannel, snap.Name, snap.Channel) + } + } + + 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{ + // TRANSLATORS: This should not start with a lowercase letter. + "classic": i18n.G("Put snap in classic mode and disable security confinement"), + // TRANSLATORS: This should not start with a lowercase letter. + "devmode": i18n.G("Put snap in development mode and disable security confinement"), + // TRANSLATORS: This should not start with a lowercase letter. + "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 || mx.Classic +} + +func (mx modeMixin) setModes(opts *client.SnapOptions) { + opts.DevMode = mx.DevMode + opts.JailMode = mx.JailMode + opts.Classic = mx.Classic +} + +type cmdInstall struct { + colorMixin + waitMixin + + 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"` + + Unaliased bool `long:"unaliased"` + + Name string `long:"name"` + + Positional struct { + Snaps []remoteSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *cmdInstall) installOne(nameOrPath, desiredName string, opts *client.SnapOptions) error { + var err error + var changeID string + var snapName string + var path string + + if strings.Contains(nameOrPath, "/") || strings.HasSuffix(nameOrPath, ".snap") || strings.Contains(nameOrPath, ".snap.") { + path = nameOrPath + changeID, err = x.client.InstallPath(path, x.Name, opts) + } else { + snapName = nameOrPath + if desiredName != "" { + return errors.New(i18n.G("cannot use explicit name when installing from store")) + } + changeID, err = x.client.Install(snapName, opts) + } + if err != nil { + msg, err := errorToCmdMessage(nameOrPath, err, opts) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) + return nil + } + + chg, err := x.wait(changeID) + if err != nil { + if err == noWait { + return nil + } + return err + } + + // extract the snapName from the change, important for sideloaded + if path != "" { + if err := chg.Get("snap-name", &snapName); err != nil { + return fmt.Errorf("cannot extract the snap-name from local file %q: %s", nameOrPath, err) + } + } + + return showDone(x.client, []string{snapName}, "install", opts, x.getEscapes()) +} + +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") + } + } + + changeID, err := x.client.InstallMany(names, opts) + if err != nil { + var snapName string + if err, ok := err.(*client.Error); ok { + snapName, _ = err.Value.(string) + } + msg, err := errorToCmdMessage(snapName, err, opts) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) + return nil + } + + chg, err := x.wait(changeID) + if err != nil { + if err == noWait { + return 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(x.client, installed, "install", opts, x.getEscapes()); 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, + Revision: x.Revision, + Dangerous: dangerous, + Unaliased: x.Unaliased, + } + x.setModes(opts) + + names := remoteSnapNames(x.Positional.Snaps) + if len(names) == 0 { + return errors.New(i18n.G("cannot install zero snaps")) + } + for _, name := range names { + if len(name) == 0 { + return errors.New(i18n.G("cannot install snap with empty name")) + } + } + + if len(names) == 1 { + return x.installOne(names[0], x.Name, 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.Name != "" { + return errors.New(i18n.G("cannot use instance name when installing multiple snaps")) + } + return x.installMany(names, nil) +} + +type cmdRefresh struct { + colorMixin + timeMixin + waitMixin + channelMixin + modeMixin + + Amend bool `long:"amend"` + Revision string `long:"revision"` + List bool `long:"list"` + Time bool `long:"time"` + IgnoreValidation bool `long:"ignore-validation"` + Positional struct { + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` +} + +func (x *cmdRefresh) refreshMany(snaps []string, opts *client.SnapOptions) error { + changeID, err := x.client.RefreshMany(snaps, opts) + if err != nil { + return err + } + + chg, err := x.wait(changeID) + if err != nil { + if err == noWait { + return 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(x.client, refreshed, "refresh", opts, x.getEscapes()) + } + + fmt.Fprintln(Stderr, i18n.G("All snaps up to date.")) + + return nil +} + +func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error { + changeID, err := x.client.Refresh(name, opts) + if err != nil { + msg, err := errorToCmdMessage(name, err, opts) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) + return nil + } + + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + return showDone(x.client, []string{name}, "refresh", opts, x.getEscapes()) +} + +func parseSysinfoTime(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return time.Time{} + } + return t +} + +func (x *cmdRefresh) showRefreshTimes() error { + sysinfo, err := x.client.SysInfo() + if err != nil { + return err + } + + if sysinfo.Refresh.Timer != "" { + fmt.Fprintf(Stdout, "timer: %s\n", sysinfo.Refresh.Timer) + } else if sysinfo.Refresh.Schedule != "" { + fmt.Fprintf(Stdout, "schedule: %s\n", sysinfo.Refresh.Schedule) + } else { + return errors.New("internal error: both refresh.timer and refresh.schedule are empty") + } + last := parseSysinfoTime(sysinfo.Refresh.Last) + hold := parseSysinfoTime(sysinfo.Refresh.Hold) + next := parseSysinfoTime(sysinfo.Refresh.Next) + + if !last.IsZero() { + fmt.Fprintf(Stdout, "last: %s\n", x.fmtTime(last)) + } else { + fmt.Fprintf(Stdout, "last: n/a\n") + } + if !hold.IsZero() { + fmt.Fprintf(Stdout, "hold: %s\n", x.fmtTime(hold)) + } + // only show "next" if its after "hold" to not confuse users + if !next.IsZero() { + // Snapstate checks for holdTime.After(limitTime) so we need + // to check for before or equal here to be fully correct. + if next.Before(hold) || next.Equal(hold) { + fmt.Fprintf(Stdout, "next: %s (but held)\n", x.fmtTime(next)) + } else { + fmt.Fprintf(Stdout, "next: %s\n", x.fmtTime(next)) + } + } else { + fmt.Fprintf(Stdout, "next: n/a\n") + } + return nil +} + +func (x *cmdRefresh) listRefresh() error { + snaps, _, err := x.client.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)) + + esc := x.getEscapes() + w := tabWriter() + defer w.Flush() + + // TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces) + fmt.Fprintf(w, i18n.G("Name\tVersion\tRev\tPublisher%s\tNotes\n"), fillerPublisher(esc)) + for _, snap := range snaps { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Revision, shortPublisher(esc, snap.Publisher), 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.Time { + if x.asksForMode() || x.asksForChannel() { + return errors.New(i18n.G("--time does not take mode or channel flags")) + } + return x.showRefreshTimes() + } + + if x.List { + if len(x.Positional.Snaps) > 0 || x.asksForMode() || x.asksForChannel() { + return errors.New(i18n.G("--list does not accept additional arguments")) + } + + return x.listRefresh() + } + + if len(x.Positional.Snaps) == 0 && os.Getenv("SNAP_REFRESH_FROM_TIMER") == "1" { + fmt.Fprintf(Stdout, "Ignoring `snap refresh` from the systemd timer") + return nil + } + + names := installedSnapNames(x.Positional.Snaps) + if len(names) == 1 { + opts := &client.SnapOptions{ + Amend: x.Amend, + Channel: x.Channel, + IgnoreValidation: x.IgnoreValidation, + Revision: x.Revision, + } + x.setModes(opts) + return x.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 x.refreshMany(names, nil) +} + +type cmdTry struct { + waitMixin + + modeMixin + Positional struct { + SnapDir string `positional-arg-name:""` + } `positional-args:"yes"` +} + +func hasSnapcraftYaml() bool { + for _, loc := range []string{ + "snap/snapcraft.yaml", + "snapcraft.yaml", + ".snapcraft.yaml", + } { + if osutil.FileExists(loc) { + return true + } + } + + return false +} + +func (x *cmdTry) Execute([]string) error { + if err := x.validateMode(); err != nil { + return err + } + name := x.Positional.SnapDir + opts := &client.SnapOptions{} + x.setModes(opts) + + if name == "" { + if hasSnapcraftYaml() && 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 := x.client.Try(path, opts) + if err != nil { + msg, err := errorToCmdMessage(name, err, opts) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) + return nil + } + + chg, err := x.wait(changeID) + if err != nil { + if err == noWait { + return 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 := x.client.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 { + waitMixin + + Positional struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *cmdEnable) Execute([]string) error { + name := string(x.Positional.Snap) + opts := &client.SnapOptions{} + changeID, err := x.client.Enable(name, opts) + if err != nil { + return err + } + + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + fmt.Fprintf(Stdout, i18n.G("%s enabled\n"), name) + return nil +} + +type cmdDisable struct { + waitMixin + + Positional struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *cmdDisable) Execute([]string) error { + name := string(x.Positional.Snap) + opts := &client.SnapOptions{} + changeID, err := x.client.Disable(name, opts) + if err != nil { + return err + } + + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + fmt.Fprintf(Stdout, i18n.G("%s disabled\n"), name) + return nil +} + +type cmdRevert struct { + waitMixin + + modeMixin + Revision string `long:"revision"` + Positional struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"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 + } + + name := string(x.Positional.Snap) + opts := &client.SnapOptions{Revision: x.Revision} + x.setModes(opts) + changeID, err := x.client.Revert(name, opts) + if err != nil { + return err + } + + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + return showDone(x.client, []string{name}, "revert", nil, nil) +} + +var shortSwitchHelp = i18n.G("Switches snap to a different channel") +var longSwitchHelp = i18n.G(` +The switch command switches the given snap to a different channel without +doing a refresh. +`) + +type cmdSwitch struct { + waitMixin + channelMixin + + Positional struct { + Snap installedSnapName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func (x cmdSwitch) Execute(args []string) error { + if err := x.setChannelFromCommandline(); err != nil { + return err + } + if x.Channel == "" { + return fmt.Errorf("missing --channel= parameter") + } + + name := string(x.Positional.Snap) + channel := string(x.Channel) + opts := &client.SnapOptions{ + Channel: channel, + } + changeID, err := x.client.Switch(name, opts) + if err != nil { + return err + } + + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + fmt.Fprintf(Stdout, i18n.G("%q switched to the %q channel\n"), name, channel) + return nil +} + +func init() { + addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} }, + waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "revision": i18n.G("Remove only the given revision"), + }), nil) + addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} }, + colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "revision": i18n.G("Install the given revision of a snap, to which you must have developer access"), + // TRANSLATORS: This should not start with a lowercase letter. + "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)"), + // TRANSLATORS: This should not start with a lowercase letter. + "force-dangerous": i18n.G("Alias for --dangerous (DEPRECATED)"), + // TRANSLATORS: This should not start with a lowercase letter. + "unaliased": i18n.G("Install the given snap without enabling its automatic aliases"), + // TRANSLATORS: This should not start with a lowercase letter. + "name": i18n.G("Install the snap file under the given instance name"), + }), nil) + addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} }, + colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(timeDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "amend": i18n.G("Allow refresh attempt on snap unknown to the store"), + // TRANSLATORS: This should not start with a lowercase letter. + "revision": i18n.G("Refresh to the given revision, to which you must have developer access"), + // TRANSLATORS: This should not start with a lowercase letter. + "list": i18n.G("Show the new versions of snaps that would be updated with the next refresh"), + // TRANSLATORS: This should not start with a lowercase letter. + "time": i18n.G("Show auto refresh information but do not perform a refresh"), + // TRANSLATORS: This should not start with a lowercase letter. + "ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"), + }), nil) + addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, waitDescs.also(modeDescs), nil) + addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, waitDescs, nil) + addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} }, waitDescs, nil) + addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, waitDescs.also(modeDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "revision": i18n.G("Revert to the given revision"), + }), nil) + addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, waitDescs.also(channelDescs), 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..e338b820 --- /dev/null +++ b/cmd/snap/cmd_snap_op_test.go @@ -0,0 +1,1772 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2018 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" + "mime" + "mime/multipart" + "net/http" + "net/http/httptest" + "path/filepath" + "regexp" + "strings" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/progress/progresstest" + "github.com/snapcore/snapd/testutil" + "os" +) + +type snapOpTestServer struct { + c *check.C + + checker func(r *http.Request) + n int + total int + channel string + confinement string + rebooting bool + snap string +} + +var _ = check.Suite(&SnapOpSuite{}) + +func (t *snapOpTestServer) handle(w http.ResponseWriter, r *http.Request) { + switch t.n { + case 0: + t.checker(r) + method := "POST" + if strings.HasSuffix(r.URL.Path, "/conf") { + method = "PUT" + } + t.c.Check(r.Method, check.Equals, method) + w.WriteHeader(202) + 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") + if !t.rebooting { + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + } else { + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}, "maintenance": {"kind": "system-restart", "message": "system is restarting"}}}`) + } + case 2: + t.c.Check(r.Method, check.Equals, "GET") + t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintf(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-name": "%s"}}}\n`, t.snap) + 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": "%s", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"%s", "confinement": "%s"}]}\n`, t.snap, t.channel, t.confinement) + 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, + snap: "foo", + } +} + +func (s *SnapOpSuite) TearDownTest(c *check.C) { + s.restoreAll() + s.BaseSnapSuite.TearDownTest(c) +} + +func (s *SnapOpSuite) TestWait(c *check.C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + 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() + + cli := snap.Client() + chg, err := snap.Wait(cli, "x") + c.Assert(chg, check.IsNil) + c.Assert(err, check.NotNil) + c.Check(meter.Labels, testutil.Contains, "Waiting for server to restart") +} + +func (s *SnapOpSuite) TestWaitRecovers(c *check.C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + 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"}}`) + }) + + cli := snap.Client() + chg, err := snap.Wait(cli, "x") + // we got the change + c.Assert(chg, check.NotNil) + c.Assert(err, check.IsNil) + + // but only after recovering + c.Check(meter.Labels, testutil.Contains, "Waiting for server to restart") +} + +func (s *SnapOpSuite) TestWaitRebooting(c *check.C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + restore := snap.MockMaxGoneTime(time.Millisecond) + defer restore() + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", +"result": { +"ready": false, +"status": "Doing", +"tasks": [{"kind": "bar", "summary": "...", "status": "Doing", "progress": {"done": 1, "total": 1}, "log": ["INFO: info"]}] +}, +"maintenance": {"kind": "system-restart", "message": "system is restarting"}}`) + }) + + cli := snap.Client() + chg, err := snap.Wait(cli, "x") + c.Assert(chg, check.IsNil) + c.Assert(err, check.DeepEquals, &client.Error{Kind: client.ErrorKindSystemRestart, Message: "system is restarting"}) + + // last available info is still displayed + c.Check(meter.Notices, testutil.Contains, "INFO: info") +} + +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": "candidate", + }) + s.srv.channel = "candidate" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "candidate", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(candidate\) 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) TestInstallFromTrack(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": "3.4/stable", + }) + s.srv.channel = "3.4/stable" + } + + s.RedirectClientToTestServer(s.srv.handle) + // snap install --channel=3.4 means 3.4/stable, this is what we test here + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/stable\) 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) TestInstallFromBranch(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": "3.4/hotfix-1", + }) + s.srv.channel = "3.4/hotfix-1" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4/hotfix-1", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/hotfix-1\) 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": "beta", + }) + s.srv.channel = "beta" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "beta", "--devmode", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 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.srv.confinement = "classic" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).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) TestInstallStrictWithClassicFlag(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.srv.confinement = "strict" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).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(), testutil.MatchesWrapped, `Warning:\s+flag --classic ignored for strictly confined snap foo.*`) + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallUnaliased(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", + "unaliased": true, + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--unaliased", "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) TestInstallSnapNotFound(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "snap not found", "value": "foo", "kind": "snap-not-found"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("error: %v\n", err), check.Equals, `error: snap "foo" not found +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailable(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision available as specified", "value": "foo", "kind": "snap-revision-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" not available as specified (see 'snap info foo') +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableOnChannel(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision available as specified", "value": "foo", "kind": "snap-revision-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=mytrack", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" not available on channel "mytrack/stable" (see 'snap info + foo') +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableAtRevision(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision available as specified", "value": "foo", "kind": "snap-revision-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--revision=2", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" revision 2 not available (see 'snap info foo') +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackOK(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "stable", + "releases": [{"architecture": "amd64", "channel": "beta"}, + {"architecture": "amd64", "channel": "edge"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on stable but is available to install on the + following channels: + + beta snap install --beta foo + edge snap install --edge foo + + Please be mindful pre-release channels may include features not + completely tested or implemented. Get more information with 'snap info + foo'. +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackOKPrerelOK(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "candidate", + "releases": [{"architecture": "amd64", "channel": "beta"}, + {"architecture": "amd64", "channel": "edge"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--candidate", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on candidate but is available to install on + the following channels: + + beta snap install --beta foo + edge snap install --edge foo + + Get more information with 'snap info foo'. +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackOther(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "stable", + "releases": [{"architecture": "amd64", "channel": "1.0/stable"}, + {"architecture": "amd64", "channel": "2.0/stable"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on latest/stable but is available to install + on the following tracks: + + 1.0/stable snap install --channel=1.0 foo + 2.0/stable snap install --channel=2.0 foo + + Please be mindful that different tracks may include different features. + Get more information with 'snap info foo'. +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackLatestStable(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "2.0/stable", + "releases": [{"architecture": "amd64", "channel": "stable"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=2.0/stable", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on 2.0/stable but is available to install on + the following tracks: + + latest/stable snap install --stable foo + + Please be mindful that different tracks may include different features. + Get more information with 'snap info foo'. +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelTrackAndRiskOther(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "2.0/stable", + "releases": [{"architecture": "amd64", "channel": "1.0/edge"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=2.0/stable", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on 2.0/stable but other tracks exist. + + Please be mindful that different tracks may include different features. + Get more information with 'snap info foo'. +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForArchitectureTrackAndRiskOK(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified architecture", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "arm64", + "channel": "stable", + "releases": [{"architecture": "amd64", "channel": "stable"}, + {"architecture": "s390x", "channel": "stable"}] +}, "kind": "snap-architecture-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on stable for this architecture (arm64) but + exists on other architectures (amd64, s390x). +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForArchitectureTrackAndRiskOther(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified architecture", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "arm64", + "channel": "1.0/stable", + "releases": [{"architecture": "amd64", "channel": "stable"}, + {"architecture": "s390x", "channel": "stable"}] +}, "kind": "snap-architecture-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=1.0/stable", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: snap "foo" is not available on this architecture (arm64) but exists on + other architectures (amd64, s390x). +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableInvalidChannel(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "a/b/c/d", + "releases": [{"architecture": "amd64", "channel": "stable"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=a/b/c/d", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: requested channel "a/b/c/d" is not valid (see 'snap info foo' for valid + ones) +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelNonExistingBranchOnMainChannel(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "stable/baz", + "releases": [{"architecture": "amd64", "channel": "stable"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=stable/baz", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: requested a non-existing branch on latest/stable for snap "foo": baz +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableForChannelNonExistingBranch(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { + "snap-name": "foo", + "action": "install", + "architecture": "amd64", + "channel": "stable/baz", + "releases": [{"architecture": "amd64", "channel": "edge"}] +}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=stable/baz", "foo"}) + c.Assert(err, check.NotNil) + c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` +error: requested a non-existing branch for snap "foo": latest/stable/baz +`) + + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func testForm(r *http.Request, c *check.C) *multipart.Form { + contentType := r.Header.Get("Content-Type") + mediaType, params, err := mime.ParseMediaType(contentType) + c.Assert(err, check.IsNil) + c.Assert(params["boundary"], check.Matches, ".{10,}") + c.Check(mediaType, check.Equals, "multipart/form-data") + + form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(1 << 20) + c.Assert(err, check.IsNil) + + return form +} + +func formFile(form *multipart.Form, c *check.C) (name, filename string, content []byte) { + c.Assert(form.File, check.HasLen, 1) + + for name, fheaders := range form.File { + c.Assert(fheaders, check.HasLen, 1) + body, err := fheaders[0].Open() + c.Assert(err, check.IsNil) + defer body.Close() + filename = fheaders[0].Filename + content, err = ioutil.ReadAll(body) + c.Assert(err, check.IsNil) + + return name, filename, content + } + + return "", "", nil +} + +func (s *SnapOpSuite) TestInstallPath(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + + form := testForm(r, c) + defer form.RemoveAll() + + c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) + c.Check(form.Value["devmode"], check.IsNil) + c.Check(form.Value["snap-path"], check.NotNil) + c.Check(form.Value, check.HasLen, 2) + + name, _, body := formFile(form, c) + c.Check(name, check.Equals, "snap") + c.Check(string(body), check.Equals, "snap-data") + } + + 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(snap.Client()).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") + form := testForm(r, c) + defer form.RemoveAll() + + c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) + c.Check(form.Value["devmode"], check.DeepEquals, []string{"true"}) + c.Check(form.Value["snap-path"], check.NotNil) + c.Check(form.Value, check.HasLen, 3) + + name, _, body := formFile(form, c) + c.Check(name, check.Equals, "snap") + c.Check(string(body), check.Equals, "snap-data") + } + + 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(snap.Client()).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") + form := testForm(r, c) + defer form.RemoveAll() + + c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) + c.Check(form.Value["classic"], check.DeepEquals, []string{"true"}) + c.Check(form.Value["snap-path"], check.NotNil) + c.Check(form.Value, check.HasLen, 3) + + name, _, body := formFile(form, c) + c.Check(name, check.Equals, "snap") + c.Check(string(body), check.Equals, "snap-data") + + s.srv.confinement = "classic" + } + + 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(snap.Client()).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") + form := testForm(r, c) + defer form.RemoveAll() + + c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) + c.Check(form.Value["dangerous"], check.DeepEquals, []string{"true"}) + c.Check(form.Value["snap-path"], check.NotNil) + c.Check(form.Value, check.HasLen, 3) + + name, _, body := formFile(form, c) + c.Check(name, check.Equals, "snap") + c.Check(string(body), check.Equals, "snap-data") + } + + 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(snap.Client()).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) TestInstallPathInstance(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + + form := testForm(r, c) + defer form.RemoveAll() + + c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) + c.Check(form.Value["name"], check.DeepEquals, []string{"foo_bar"}) + c.Check(form.Value["devmode"], check.IsNil) + c.Check(form.Value["snap-path"], check.NotNil) + c.Check(form.Value, check.HasLen, 3) + + name, _, body := formFile(form, c) + c.Check(name, check.Equals, "snap") + c.Check(string(body), check.Equals, "snap-data") + } + + snapBody := []byte("snap-data") + s.RedirectClientToTestServer(s.srv.handle) + // instance is named foo_bar + s.srv.snap = "foo_bar" + snapPath := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snapPath, snapBody, 0644) + c.Assert(err, check.IsNil) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", snapPath, "--name", "foo_bar"}) + c.Assert(rest, check.DeepEquals, []string{}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo_bar 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 *SnapSuite) TestInstallWithInstanceNoPath(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--name", "foo_bar", "some-snap"}) + c.Assert(err, check.ErrorMatches, "cannot use explicit name when installing from store") +} + +func (s *SnapSuite) TestInstallManyWithInstance(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--name", "foo_bar", "some-snap-1", "some-snap-2"}) + c.Assert(err, check.ErrorMatches, "cannot use instance name when installing multiple snaps") +} + +func (s *SnapOpSuite) TestRevertRunthrough(c *check.C) { + s.srv.total = 4 + s.srv.channel = "potato" + 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(snap.Client()).ParseArgs([]string{"revert", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + // tracking channel is "" in the test server + c.Check(s.Stdout(), check.Equals, `foo reverted to 1.0 +Channel for foo is closed; temporarily forwarding to potato. +`) + 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) runRevertTest(c *check.C, opts *client.SnapOptions) { + modes := []struct { + enabled bool + name string + }{ + {opts.DevMode, "devmode"}, + {opts.JailMode, "jailmode"}, + {opts.Classic, "classic"}, + } + + s.srv.checker = func(r *http.Request) { + + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + d := DecodedRequestBody(c, r) + + n := 1 + c.Check(d["action"], check.Equals, "revert") + + for _, mode := range modes { + if mode.enabled { + n++ + c.Check(d[mode.name], check.Equals, true) + } else { + c.Check(d[mode.name], check.IsNil) + } + } + c.Check(d, check.HasLen, n) + } + + s.RedirectClientToTestServer(s.srv.handle) + + cmd := []string{"revert", "foo"} + for _, mode := range modes { + if mode.enabled { + cmd = append(cmd, "--"+mode.name) + } + } + + rest, err := snap.Parser(snap.Client()).ParseArgs(cmd) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "foo reverted to 1.0\n") + 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) TestRevertNoMode(c *check.C) { + s.runRevertTest(c, &client.SnapOptions{}) +} + +func (s *SnapOpSuite) TestRevertDevMode(c *check.C) { + s.runRevertTest(c, &client.SnapOptions{DevMode: true}) +} + +func (s *SnapOpSuite) TestRevertJailMode(c *check.C) { + s.runRevertTest(c, &client.SnapOptions{JailMode: true}) +} + +func (s *SnapOpSuite) TestRevertClassic(c *check.C) { + s.runRevertTest(c, &client.SnapOptions{Classic: true}) +} + +func (s *SnapOpSuite) TestRevertMissingName(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"revert"}) + c.Assert(err, check.NotNil) + c.Assert(err, check.ErrorMatches, "the required argument `` was not provided") +} + +func (s *SnapSuite) TestRefreshListLessOptions(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Fatal("expected to get 0 requests") + }) + + for _, flag := range []string{"--beta", "--channel=potato", "--classic"} { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--list", flag}) + c.Assert(err, check.ErrorMatches, "--list does not accept additional arguments") + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--list", flag, "some-snap"}) + c.Assert(err, check.ErrorMatches, "--list does not accept additional arguments") + } +} + +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", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17,"summary":"some summary"}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Publisher +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) TestRefreshLegacyTime(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/system-info") + fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"schedule": "00:00-04:59/5:00-10:59/11:00-16:59/17:00-23:59", "last": "2017-04-25T17:35:00+02:00", "next": "2017-04-26T00:58:00+02:00"}}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time", "--abs-time"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `schedule: 00:00-04:59/5:00-10:59/11:00-16:59/17:00-23:59 +last: 2017-04-25T17:35:00+02:00 +next: 2017-04-26T00:58:00+02:00 +`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestRefreshTimer(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/system-info") + fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"timer": "0:00-24:00/4", "last": "2017-04-25T17:35:00+02:00", "next": "2017-04-26T00:58:00+02:00"}}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time", "--abs-time"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `timer: 0:00-24:00/4 +last: 2017-04-25T17:35:00+02:00 +next: 2017-04-26T00:58:00+02:00 +`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestRefreshHold(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/system-info") + fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"timer": "0:00-24:00/4", "last": "2017-04-25T17:35:00+02:00", "next": "2017-04-26T00:58:00+02:00", "hold": "2017-04-28T00:00:00+02:00"}}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time", "--abs-time"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `timer: 0:00-24:00/4 +last: 2017-04-25T17:35:00+02:00 +hold: 2017-04-28T00:00:00+02:00 +next: 2017-04-26T00:58:00+02:00 (but held) +`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestRefreshNoTimerNoSchedule(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/system-info") + fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"last": "2017-04-25T17:35:00+0200", "next": "2017-04-26T00:58:00+0200"}}}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--time"}) + c.Assert(err, check.ErrorMatches, `internal error: both refresh.timer and refresh.schedule are empty`) +} + +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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).ParseArgs([]string{"refresh", "--ignore-validation", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) TestRefreshOneRebooting(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/core") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + }) + } + s.srv.rebooting = true + + restore := mockArgs("snap", "refresh", "core") + defer restore() + + err := snap.RunMain() + c.Check(err, check.IsNil) + c.Check(s.Stderr(), check.Equals, "snapd is about to reboot the system\n") + +} + +func (s *SnapOpSuite) TestRefreshOneModeErr(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).ParseArgs([]string{"refresh", "--devmode"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) +} + +func (s *SnapOpSuite) TestRefreshOneAmend(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", + "amend": true, + }) + } + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--amend", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) runTryTest(c *check.C, opts *client.SnapOptions) { + // pass relative path to cmd + tryDir := "some-dir" + + modes := []struct { + enabled bool + name string + }{ + {opts.DevMode, "devmode"}, + {opts.JailMode, "jailmode"}, + {opts.Classic, "classic"}, + } + + 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") + form := testForm(r, c) + defer form.RemoveAll() + + c.Assert(form.Value["action"], check.HasLen, 1) + c.Assert(form.Value["snap-path"], check.HasLen, 1) + c.Check(form.File, check.HasLen, 0) + c.Check(form.Value["action"][0], check.Equals, "try") + c.Check(form.Value["snap-path"][0], check.Matches, regexp.QuoteMeta(fullTryDir)) + + for _, mode := range modes { + if mode.enabled { + c.Assert(form.Value[mode.name], check.HasLen, 1) + c.Check(form.Value[mode.name][0], check.Equals, "true") + } else { + c.Check(form.Value[mode.name], check.IsNil) + } + } + } + + s.RedirectClientToTestServer(s.srv.handle) + + cmd := []string{"try", tryDir} + for _, mode := range modes { + if mode.enabled { + cmd = append(cmd, "--"+mode.name) + } + } + + rest, err := snap.Parser(snap.Client()).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) TestTryNoMode(c *check.C) { + s.runTryTest(c, &client.SnapOptions{}) +} + +func (s *SnapOpSuite) TestTryDevMode(c *check.C) { + s.runTryTest(c, &client.SnapOptions{DevMode: true}) +} + +func (s *SnapOpSuite) TestTryJailMode(c *check.C) { + s.runTryTest(c, &client.SnapOptions{JailMode: true}) +} + +func (s *SnapOpSuite) TestTryClassic(c *check.C) { + s.runTryTest(c, &client.SnapOptions{Classic: true}) +} + +func (s *SnapOpSuite) TestTryNoSnapDirErrors(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(202) + fmt.Fprintln(w, ` +{ + "type": "error", + "result": { + "message":"error from server", + "kind":"snap-not-a-snap" + }, + "status-code": 400 +}`) + }) + + cmd := []string{"try", "/"} + _, err := snap.Parser(snap.Client()).ParseArgs(cmd) + c.Assert(err, testutil.EqualsWrapped, ` +"/" does not contain an unpacked snap. + +Try 'snapcraft prime' in your project directory, then 'snap try' again.`) +} + +func (s *SnapOpSuite) TestTryMissingOpt(c *check.C) { + oldArgs := os.Args + defer func() { + os.Args = oldArgs + }() + os.Args = []string{"snap", "try", "./"} + var kind string + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "POST", check.Commentf("%q", kind)) + w.WriteHeader(400) + fmt.Fprintf(w, ` +{ + "type": "error", + "result": { + "message":"error from server", + "value": "some-snap", + "kind": %q + }, + "status-code": 400 +}`, kind) + }) + + type table struct { + kind, expected string + } + + tests := []table{ + {"snap-needs-classic", "published using classic confinement"}, + {"snap-needs-devmode", "only meant for development"}, + } + + for _, test := range tests { + kind = test.kind + c.Check(snap.RunMain(), testutil.ContainsWrapped, test.expected, check.Commentf("%q", kind)) + } +} + +func (s *SnapOpSuite) TestInstallConfinedAsClassic(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(400) + fmt.Fprintf(w, `{ + "type": "error", + "result": { + "message":"error from server", + "value": "some-snap", + "kind": "snap-not-classic" + }, + "status-code": 400 +}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--classic", "some-snap"}) + c.Assert(err, check.ErrorMatches, `snap "some-snap" is not compatible with --classic`) +} + +func (s *SnapSuite) TestInstallChannelDuplicationError(c *check.C) { + _, err := snap.Parser(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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(snap.Client()).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) TestRemoveRevision(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", + "revision": "17", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "--revision=17", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(revision 17\) 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(snap.Client()).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(202) + 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(snap.Client()).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(snap.Client()).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(snap.Client()).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(202) + 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", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"stable"},{"name": "two", "status": "active", "version": "2.0", "developer": "baz", "publisher": {"id": "baz-id", "username": "baz", "display-name": "Baz", "validation": "unproven"}, "revision":42, "channel":"edge"}]}\n`) + + default: + c.Fatalf("expected to get %d requests, now on %d", total, n+1) + } + + n++ + }) + + rest, err := snap.Parser(snap.Client()).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) +} + +func (s *SnapOpSuite) TestInstallZeroEmpty(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install"}) + c.Assert(err, check.ErrorMatches, "cannot install zero snaps") + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"install", ""}) + c.Assert(err, check.ErrorMatches, "cannot install snap with empty name") + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"install", "", "bar"}) + c.Assert(err, check.ErrorMatches, "cannot install snap with empty name") +} + +func (s *SnapOpSuite) TestNoWait(c *check.C) { + s.srv.checker = func(r *http.Request) {} + + cmds := [][]string{ + {"remove", "--no-wait", "foo"}, + {"remove", "--no-wait", "foo", "bar"}, + {"install", "--no-wait", "foo"}, + {"install", "--no-wait", "foo", "bar"}, + {"revert", "--no-wait", "foo"}, + {"refresh", "--no-wait", "foo"}, + {"refresh", "--no-wait", "foo", "bar"}, + {"refresh", "--no-wait"}, + {"enable", "--no-wait", "foo"}, + {"disable", "--no-wait", "foo"}, + {"try", "--no-wait", "."}, + {"switch", "--no-wait", "--channel=foo", "bar"}, + // commands that use waitMixin from elsewhere + {"start", "--no-wait", "foo"}, + {"stop", "--no-wait", "foo"}, + {"restart", "--no-wait", "foo"}, + {"alias", "--no-wait", "foo", "bar"}, + {"unalias", "--no-wait", "foo"}, + {"prefer", "--no-wait", "foo"}, + {"set", "--no-wait", "foo", "bar=baz"}, + {"disconnect", "--no-wait", "foo:bar"}, + {"connect", "--no-wait", "foo:bar"}, + } + + s.RedirectClientToTestServer(s.srv.handle) + for _, cmd := range cmds { + rest, err := snap.Parser(snap.Client()).ParseArgs(cmd) + c.Assert(err, check.IsNil, check.Commentf("%v", cmd)) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, "(?sm)42\n") + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.srv.n, check.Equals, 1) + // reset + s.srv.n = 0 + s.stdout.Reset() + } +} + +func (s *SnapOpSuite) TestNoWaitImmediateError(c *check.C) { + + cmds := [][]string{ + {"remove", "--no-wait", "foo"}, + {"remove", "--no-wait", "foo", "bar"}, + {"install", "--no-wait", "foo"}, + {"install", "--no-wait", "foo", "bar"}, + {"revert", "--no-wait", "foo"}, + {"refresh", "--no-wait", "foo"}, + {"refresh", "--no-wait", "foo", "bar"}, + {"refresh", "--no-wait"}, + {"enable", "--no-wait", "foo"}, + {"disable", "--no-wait", "foo"}, + {"try", "--no-wait", "."}, + {"switch", "--no-wait", "--channel=foo", "bar"}, + // commands that use waitMixin from elsewhere + {"start", "--no-wait", "foo"}, + {"stop", "--no-wait", "foo"}, + {"restart", "--no-wait", "foo"}, + {"alias", "--no-wait", "foo", "bar"}, + {"unalias", "--no-wait", "foo"}, + {"prefer", "--no-wait", "foo"}, + {"set", "--no-wait", "foo", "bar=baz"}, + {"disconnect", "--no-wait", "foo:bar"}, + {"connect", "--no-wait", "foo:bar"}, + } + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "failure"}}`) + }) + + for _, cmd := range cmds { + _, err := snap.Parser(snap.Client()).ParseArgs(cmd) + c.Assert(err, check.ErrorMatches, "failure", check.Commentf("%v", cmd)) + } +} + +func (s *SnapOpSuite) TestWaitServerError(c *check.C) { + r := snap.MockMaxGoneTime(0) + defer r() + + cmds := [][]string{ + {"remove", "foo"}, + {"remove", "foo", "bar"}, + {"install", "foo"}, + {"install", "foo", "bar"}, + {"revert", "foo"}, + {"refresh", "foo"}, + {"refresh", "foo", "bar"}, + {"refresh"}, + {"enable", "foo"}, + {"disable", "foo"}, + {"try", "."}, + {"switch", "--channel=foo", "bar"}, + // commands that use waitMixin from elsewhere + {"start", "foo"}, + {"stop", "foo"}, + {"restart", "foo"}, + {"alias", "foo", "bar"}, + {"unalias", "foo"}, + {"prefer", "foo"}, + {"set", "foo", "bar=baz"}, + {"disconnect", "foo:bar"}, + {"connect", "foo:bar"}, + } + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + if n == 1 { + w.WriteHeader(202) + fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) + return + } + if n == 3 { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "unexpected request"}}`) + return + } + fmt.Fprintln(w, `{"type": "error", "result": {"message": "server error"}}`) + }) + + for _, cmd := range cmds { + _, err := snap.Parser(snap.Client()).ParseArgs(cmd) + c.Assert(err, check.ErrorMatches, "server error", check.Commentf("%v", cmd)) + // reset + n = 0 + } +} + +func (s *SnapOpSuite) TestSwitchHappy(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": "switch", + "channel": "beta", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--beta", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "beta" channel`) + 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) TestSwitchUnhappy(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch"}) + c.Assert(err, check.ErrorMatches, "the required argument `` was not provided") +} + +func (s *SnapOpSuite) TestSwitchAlsoUnhappy(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "foo"}) + c.Assert(err, check.ErrorMatches, `missing --channel= parameter`) +} + +func (s *SnapOpSuite) TestSnapOpNetworkTimeoutError(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(202) + w.Write([]byte(` +{ + "type": "error", + "result": { + "message":"Get https://api.snapcraft.io/api/v1/snaps/details/hello?channel=stable&fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha3_384%2Csummary%2Cdescription%2Cdeltas%2Cbinary_filesize%2Cdownload_url%2Cepoch%2Cicon_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Clicense%2Cbase%2Csupport_url%2Ccontact%2Ctitle%2Ccontent%2Cversion%2Corigin%2Cdeveloper_id%2Cprivate%2Cconfinement%2Cchannel_maps_list: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)", + "kind":"network-timeout" + }, + "status-code": 400 +} +`)) + + }) + + cmd := []string{"install", "hello"} + _, err := snap.Parser(snap.Client()).ParseArgs(cmd) + c.Assert(err, check.ErrorMatches, `unable to contact snap store`) +} diff --git a/cmd/snap/cmd_snapshot.go b/cmd/snap/cmd_snapshot.go new file mode 100644 index 00000000..ee003199 --- /dev/null +++ b/cmd/snap/cmd_snapshot.go @@ -0,0 +1,331 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/strutil/quantity" +) + +func fmtSize(size int64) string { + return quantity.FormatAmount(uint64(size), -1) +} + +var ( + shortSavedHelp = i18n.G("List currently stored snapshots") + shortSaveHelp = i18n.G("Save a snapshot of the current data") + shortForgetHelp = i18n.G("Delete a snapshot") + shortCheckHelp = i18n.G("Check a snapshot") + shortRestoreHelp = i18n.G("Restore a snapshot") +) + +var longSavedHelp = i18n.G(` +The saved command displays a list of snapshots that have been created +previously with the 'save' command. +`) +var longSaveHelp = i18n.G(` +The save command creates a snapshot of the current user, system and +configuration data for the given snaps. + +By default, this command saves the data of all snaps for all users. +Alternatively, you can specify the data of which snaps to save, or +for which users, or a combination of these. + +If a snap is included in a save operation, excluding its system and +configuration data from the snapshot is not currently possible. This +restriction may be lifted in the future. +`) +var longForgetHelp = i18n.G(` +The forget command deletes a snapshot. This operation can not be +undone. + +A snapshot contains archives for the user, system and configuration +data of each snap included in the snapshot. + +By default, this command forgets all the data in a snapshot. +Alternatively, you can specify the data of which snaps to forget. +`) +var longCheckHelp = i18n.G(` +The check-snapshot command verifies the user, system and configuration +data of the snaps included in the specified snapshot. + +The check operation runs the same data integrity verification that is +performed when a snapshot is restored. + +By default, this command checks all the data in a snapshot. +Alternatively, you can specify the data of which snaps to check, or +for which users, or a combination of these. + +If a snap is included in a check-snapshot operation, excluding its +system and configuration data from the check is not currently +possible. This restriction may be lifted in the future. +`) +var longRestoreHelp = i18n.G(` +The restore command replaces the current user, system and +configuration data of included snaps, with the corresponding data from +the specified snapshot. + +By default, this command restores all the data in a snapshot. +Alternatively, you can specify the data of which snaps to restore, or +for which users, or a combination of these. + +If a snap is included in a restore operation, excluding its system and +configuration data from the restore is not currently possible. This +restriction may be lifted in the future. +`) + +type savedCmd struct { + clientMixin + durationMixin + ID snapshotID `long:"id"` + Positional struct { + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` +} + +func (x *savedCmd) Execute([]string) error { + setID := uint64(x.ID) + snaps := installedSnapNames(x.Positional.Snaps) + list, err := x.client.SnapshotSets(setID, snaps) + if err != nil { + return err + } + if len(list) == 0 { + fmt.Fprintln(Stdout, i18n.G("No snapshots found.")) + return nil + } + w := tabWriter() + defer w.Flush() + + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + // TRANSLATORS: 'Set' as in group or bag of things + i18n.G("Set"), + "Snap", + // TRANSLATORS: 'Age' as in how old something is + i18n.G("Age"), + i18n.G("Version"), + // TRANSLATORS: 'Rev' is an abbreviation of 'Revision' + i18n.G("Rev"), + i18n.G("Size"), + // TRANSLATORS: 'Notes' as in 'Comments' + i18n.G("Notes")) + for _, sg := range list { + for _, sh := range sg.Snapshots { + note := "-" + if sh.Broken != "" { + note = "broken: " + sh.Broken + } + size := quantity.FormatAmount(uint64(sh.Size), -1) + "B" + fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, x.fmtDuration(sh.Time), sh.Version, sh.Revision, size, note) + } + } + return nil +} + +type saveCmd struct { + waitMixin + durationMixin + Users string `long:"users"` + Positional struct { + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` +} + +func (x *saveCmd) Execute([]string) error { + snaps := installedSnapNames(x.Positional.Snaps) + users := strutil.CommaSeparatedList(x.Users) + setID, changeID, err := x.client.SnapshotMany(snaps, users) + if err != nil { + return err + } + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + y := &savedCmd{ + clientMixin: x.clientMixin, + durationMixin: x.durationMixin, + ID: snapshotID(setID), + } + return y.Execute(nil) +} + +type forgetCmd struct { + waitMixin + Positional struct { + ID snapshotID `positional-arg-name:""` + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *forgetCmd) Execute([]string) error { + setID := uint64(x.Positional.ID) + snaps := installedSnapNames(x.Positional.Snaps) + changeID, err := x.client.ForgetSnapshots(setID, snaps) + if err != nil { + return err + } + _, err = x.wait(changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + if len(snaps) > 0 { + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + fmt.Fprintf(Stdout, i18n.NG("Snapshot #%d of snap %s forgotten.\n", "Snapshot #%d of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps)) + } else { + fmt.Fprintf(Stdout, i18n.G("Snapshot #%d forgotten.\n"), x.Positional.ID) + } + return nil +} + +type checkSnapshotCmd struct { + waitMixin + Users string `long:"users"` + Positional struct { + ID snapshotID `positional-arg-name:""` + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *checkSnapshotCmd) Execute([]string) error { + setID := uint64(x.Positional.ID) + snaps := installedSnapNames(x.Positional.Snaps) + users := strutil.CommaSeparatedList(x.Users) + changeID, err := x.client.CheckSnapshots(setID, snaps, users) + if err != nil { + return err + } + _, err = x.wait(changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + // TODO: also mention the home archives that were actually checked + if len(snaps) > 0 { + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + fmt.Fprintf(Stdout, i18n.G("Snapshot #%d of snaps %s verified successfully.\n"), + x.Positional.ID, strutil.Quoted(snaps)) + } else { + fmt.Fprintf(Stdout, i18n.G("Snapshot #%d verified successfully.\n"), x.Positional.ID) + } + return nil +} + +type restoreCmd struct { + waitMixin + Users string `long:"users"` + Positional struct { + ID snapshotID `positional-arg-name:""` + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *restoreCmd) Execute([]string) error { + setID := uint64(x.Positional.ID) + snaps := installedSnapNames(x.Positional.Snaps) + users := strutil.CommaSeparatedList(x.Users) + changeID, err := x.client.RestoreSnapshots(setID, snaps, users) + if err != nil { + return err + } + _, err = x.wait(changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + // TODO: also mention the home archives that were actually restored + if len(snaps) > 0 { + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d of snaps %s.\n"), + x.Positional.ID, strutil.Quoted(snaps)) + } else { + fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d.\n"), x.Positional.ID) + } + return nil +} + +func init() { + addCommand("saved", + shortSavedHelp, + longSavedHelp, + func() flags.Commander { + return &savedCmd{} + }, + durationDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "id": i18n.G("Show only a specific snapshot."), + }), + nil) + + addCommand("save", + shortSaveHelp, + longSaveHelp, + func() flags.Commander { + return &saveCmd{} + }, durationDescs.also(waitDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "users": i18n.G("Snapshot data of only specific users (comma-separated) (default: all users)"), + }), nil) + + addCommand("restore", + shortRestoreHelp, + longRestoreHelp, + func() flags.Commander { + return &restoreCmd{} + }, waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "users": i18n.G("Restore data of only specific users (comma-separated) (default: all users)"), + }), nil) + + addCommand("forget", + shortForgetHelp, + longForgetHelp, + func() flags.Commander { + return &forgetCmd{} + }, waitDescs, nil) + + addCommand("check-snapshot", + shortCheckHelp, + longCheckHelp, + func() flags.Commander { + return &checkSnapshotCmd{} + }, waitDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "users": i18n.G("Check data of only specific users (comma-separated) (default: all users)"), + }), nil) +} diff --git a/cmd/snap/cmd_unalias.go b/cmd/snap/cmd_unalias.go new file mode 100644 index 00000000..44309df7 --- /dev/null +++ b/cmd/snap/cmd_unalias.go @@ -0,0 +1,69 @@ +// -*- 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 { + waitMixin + Positionals struct { + AliasOrSnap aliasOrSnap `required:"yes"` + } `positional-args:"true"` +} + +var shortUnaliasHelp = i18n.G("Remove a manual alias, or the aliases for an entire snap") +var longUnaliasHelp = i18n.G(` +The unalias command removes a single alias if the provided argument is a manual +alias, or disables all aliases of a snap, including manual ones, if the +argument is a snap name. +`) + +func init() { + addCommand("unalias", shortUnaliasHelp, longUnaliasHelp, func() flags.Commander { + return &cmdUnalias{} + }, waitDescs.also(nil), []argDesc{ + // TRANSLATORS: This needs to begin with < and end with > + {name: i18n.G("")}, + }) +} + +func (x *cmdUnalias) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + id, err := x.client.Unalias(string(x.Positionals.AliasOrSnap)) + if err != nil { + return err + } + chg, err := x.wait(id) + if err != nil { + if err == noWait { + return nil + } + return err + } + + return showAliasChanges(chg) +} diff --git a/cmd/snap/cmd_unalias_test.go b/cmd/snap/cmd_unalias_test.go new file mode 100644 index 00000000..e6ec9976 --- /dev/null +++ b/cmd/snap/cmd_unalias_test.go @@ -0,0 +1,72 @@ +// -*- 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 unalias [unalias-OPTIONS] [] + +The unalias command removes a single alias if the provided argument is a manual +alias, or disables all aliases of a snap, including manual ones, if the +argument is a snap name. + +[unalias command options] + --no-wait Do not wait for the operation to finish but just + print the change id. +` + s.testSubCommandHelp(c, "unalias", msg) +} + +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": "alias1", + "alias": "alias1", + }) + 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", "data": {"aliases-removed": [{"alias": "alias1", "snap": "foo", "app": "foo"}]}}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser(Client()).ParseArgs([]string{"unalias", "alias1"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, ""+ + "Removed:\n"+ + " - foo as alias1\n", + ) + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_userd.go b/cmd/snap/cmd_userd.go new file mode 100644 index 00000000..2f1c98cf --- /dev/null +++ b/cmd/snap/cmd_userd.go @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/userd" +) + +type cmdUserd struct { + userd userd.Userd + + Autostart bool `long:"autostart"` +} + +var shortUserdHelp = i18n.G("Start the userd service") +var longUserdHelp = i18n.G(` +The userd command starts the snap user session service. +`) + +func init() { + cmd := addCommand("userd", + shortUserdHelp, + longUserdHelp, + func() flags.Commander { + return &cmdUserd{} + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "autostart": i18n.G("Autostart user applications"), + }, nil) + cmd.hidden = true +} + +func (x *cmdUserd) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if x.Autostart { + return x.runAutostart() + } + + if err := x.userd.Init(); err != nil { + return err + } + x.userd.Start() + + ch := make(chan os.Signal, 3) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) + select { + case sig := <-ch: + fmt.Fprintf(Stdout, "Exiting on %s.\n", sig) + case <-x.userd.Dying(): + // something called Stop() + } + + return x.userd.Stop() +} + +func (x *cmdUserd) runAutostart() error { + if err := userd.AutostartSessionApps(); err != nil { + return fmt.Errorf("autostart failed for the following apps:\n%v", err) + } + return nil +} diff --git a/cmd/snap/cmd_userd_test.go b/cmd/snap/cmd_userd_test.go new file mode 100644 index 00000000..7d9c0698 --- /dev/null +++ b/cmd/snap/cmd_userd_test.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 main_test + +import ( + "os" + "strings" + "syscall" + "time" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/testutil" +) + +type userdSuite struct { + BaseSnapSuite + testutil.DBusTest + + restoreLogger func() +} + +var _ = Suite(&userdSuite{}) + +func (s *userdSuite) SetUpTest(c *C) { + s.BaseSnapSuite.SetUpTest(c) + s.DBusTest.SetUpTest(c) + + _, s.restoreLogger = logger.MockLogger() +} + +func (s *userdSuite) TearDownTest(c *C) { + s.BaseSnapSuite.TearDownTest(c) + s.DBusTest.TearDownTest(c) + + s.restoreLogger() +} + +func (s *userdSuite) TestUserdBadCommandline(c *C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"userd", "extra-arg"}) + c.Assert(err, ErrorMatches, "too many arguments for command") +} + +func (s *userdSuite) TestUserdDBus(c *C) { + go func() { + myPid := os.Getpid() + defer func() { + me, err := os.FindProcess(myPid) + c.Assert(err, IsNil) + me.Signal(syscall.SIGUSR1) + }() + + names := map[string]bool{ + "io.snapcraft.Launcher": false, + "io.snapcraft.Settings": false, + } + for i := 0; i < 1000; i++ { + seenCount := 0 + for name, seen := range names { + if seen { + seenCount++ + continue + } + pid, err := testutil.DBusGetConnectionUnixProcessID(s.SessionBus, name) + c.Logf("name: %v pid: %v err: %v", name, pid, err) + if pid == myPid { + names[name] = true + seenCount++ + } + } + if seenCount == len(names) { + return + } + time.Sleep(10 * time.Millisecond) + } + c.Fatalf("not all names have appeared on the bus: %v", names) + }() + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"userd"}) + c.Assert(err, IsNil) + c.Check(rest, DeepEquals, []string{}) + c.Check(strings.ToLower(s.Stdout()), Equals, "exiting on user defined signal 1.\n") +} diff --git a/cmd/snap/cmd_version.go b/cmd/snap/cmd_version.go new file mode 100644 index 00000000..5b96c8c3 --- /dev/null +++ b/cmd/snap/cmd_version.go @@ -0,0 +1,74 @@ +// -*- 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/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/i18n" +) + +var shortVersionHelp = i18n.G("Show version details") +var longVersionHelp = i18n.G(` +The version command displays the versions of the running client, server, +and operating system. +`) + +type cmdVersion struct { + clientMixin +} + +func init() { + addCommand("version", shortVersionHelp, longVersionHelp, func() flags.Commander { return &cmdVersion{} }, nil, nil) +} + +func (cmd cmdVersion) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + printVersions(cmd.client) + return nil +} + +func printVersions(cli *client.Client) error { + sv := serverVersion(cli) + 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 { + if sv.OSVersionID == "" { + sv.OSVersionID = "-" + } + fmt.Fprintf(w, "%s\t%s\n", sv.OSID, sv.OSVersionID) + } + if sv.KernelVersion != "" { + fmt.Fprintf(w, "kernel\t%s\n", sv.KernelVersion) + } + w.Flush() + + return nil +} diff --git a/cmd/snap/cmd_version_linux.go b/cmd/snap/cmd_version_linux.go new file mode 100644 index 00000000..8c597b4e --- /dev/null +++ b/cmd/snap/cmd_version_linux.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 main + +import ( + "fmt" + "runtime" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +func serverVersion(cli *client.Client) *client.ServerVersion { + if release.OnWSL { + return &client.ServerVersion{ + Version: i18n.G("unavailable"), + Series: release.Series, + OSID: "Windows Subsystem for Linux", + OnClassic: true, + KernelVersion: fmt.Sprintf("%s (%s)", osutil.KernelVersion(), runtime.GOARCH), + } + } + sv, err := cli.ServerVersion() + + if err != nil { + sv = &client.ServerVersion{ + Version: i18n.G("unavailable"), + Series: "-", + OSID: "-", + OSVersionID: "-", + } + } + return sv +} diff --git a/cmd/snap/cmd_version_other.go b/cmd/snap/cmd_version_other.go new file mode 100644 index 00000000..2de3d0b6 --- /dev/null +++ b/cmd/snap/cmd_version_other.go @@ -0,0 +1,41 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build !linux + +/* + * 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" + "runtime" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +func serverVersion(*client.Client) *client.ServerVersion { + return &client.ServerVersion{ + Version: i18n.G("unavailable"), + Series: release.Series, + OSID: runtime.GOOS, + OnClassic: true, + KernelVersion: fmt.Sprintf("%s (%s)", osutil.KernelVersion(), runtime.GOARCH), + } +} diff --git a/cmd/snap/cmd_version_test.go b/cmd/snap/cmd_version_test.go new file mode 100644 index 00000000..47d781e0 --- /dev/null +++ b/cmd/snap/cmd_version_test.go @@ -0,0 +1,74 @@ +// -*- 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(snap.Client()).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(snap.Client()).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, "") +} + +func (s *SnapSuite) TestVersionCommandOnClassicNoOsVersion(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":"arch","version-id":""},"series":"56","version":"7.89"}}`) + }) + restore := mockArgs("snap", "version") + defer restore() + restore = mockVersion("4.56") + defer restore() + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"version"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\narch -\n") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_wait.go b/cmd/snap/cmd_wait.go new file mode 100644 index 00000000..1d6bbd53 --- /dev/null +++ b/cmd/snap/cmd_wait.go @@ -0,0 +1,155 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "math/rand" + "reflect" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type cmdWait struct { + clientMixin + Positional struct { + Snap installedSnapName `required:"yes"` + Key string + } `positional-args:"yes"` +} + +func init() { + addCommand("wait", + "Wait for configuration", + "The wait command waits until a configration becomes true.", + func() flags.Commander { + return &cmdWait{} + }, nil, []argDesc{ + { + name: "", + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("The snap for which configuration will be checked"), + }, { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Key of interest within the configuration"), + }, + }) +} + +var waitConfTimeout = 500 * time.Millisecond + +func isNoOption(err error) bool { + if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindConfigNoSuchOption { + return true + } + return false +} + +// trueishJSON takes an interface{} and returns true if the interface value +// looks "true". For strings thats if len(string) > 0 for numbers that +// they are != 0 and for maps/slices/arrays that they have elements. +// +// Note that *only* types that the json package decode with the +// "UseNumber()" options turned on are handled here. If this ever +// needs to becomes a generic "trueish" helper we need to resurrect +// the code in 306ba60edfba8d6501060c6f773235d8c994a319 (and add nil +// to it). +func trueishJSON(vi interface{}) (bool, error) { + switch v := vi.(type) { + // limited to the types that json unmarhal can produce + case nil: + return false, nil + case bool: + return v, nil + case json.Number: + if i, err := v.Int64(); err == nil { + return i != 0, nil + } + if f, err := v.Float64(); err == nil { + return f != 0.0, nil + } + case string: + return v != "", nil + } + // arrays/slices/maps + typ := reflect.TypeOf(vi) + switch typ.Kind() { + case reflect.Array, reflect.Slice, reflect.Map: + s := reflect.ValueOf(vi) + switch s.Kind() { + case reflect.Array, reflect.Slice, reflect.Map: + return s.Len() > 0, nil + } + } + + return false, fmt.Errorf("cannot test type %T for truth", vi) +} + +func (x *cmdWait) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapName := string(x.Positional.Snap) + confKey := x.Positional.Key + + // This is fine because not providing a confKey is unsupported so this + // won't interfere with supported uses of `snap wait`. + if snapName == "godot" && confKey == "" { + switch rand.Intn(10) { + case 0: + fmt.Fprintln(Stdout, `The tears of the world are a constant quantity. +For each one who begins to weep somewhere else another stops. +The same is true of the laugh.`) + case 1: + fmt.Fprintln(Stdout, "Nothing happens. Nobody comes, nobody goes. It's awful.") + default: + fmt.Fprintln(Stdout, `"Let's go." "We can't." "Why not?" "We're waiting for Godot."`) + } + return nil + } + if confKey == "" { + return fmt.Errorf("the required argument `` was not provided") + } + + for { + conf, err := x.client.Conf(snapName, []string{confKey}) + if err != nil && !isNoOption(err) { + return err + } + res, err := trueishJSON(conf[confKey]) + if err != nil { + return err + } + if res { + break + } + time.Sleep(waitConfTimeout) + } + + return nil +} diff --git a/cmd/snap/cmd_wait_test.go b/cmd/snap/cmd_wait_test.go new file mode 100644 index 00000000..33dcbac2 --- /dev/null +++ b/cmd/snap/cmd_wait_test.go @@ -0,0 +1,174 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "time" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestCmdWaitHappy(c *C) { + restore := snap.MockWaitConfTimeout(10 * time.Millisecond) + defer restore() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/snaps/system/conf") + + fmt.Fprintln(w, fmt.Sprintf(`{"type":"sync", "status-code": 200, "result": {"seed.loaded":%v}}`, n > 1)) + n++ + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"wait", "system", "seed.loaded"}) + c.Assert(err, IsNil) + + // ensure we retried a bit but make the check not overly precise + // because this will run in super busy build hosts that where a + // 10 millisecond sleep actually takes much longer until the kernel + // hands control back to the process + c.Check(n > 2, Equals, true) +} + +func (s *SnapSuite) TestCmdWaitMissingConfKey(c *C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"wait", "snapName"}) + c.Assert(err, ErrorMatches, "the required argument `` was not provided") + + c.Check(n, Equals, 0) +} + +func (s *SnapSuite) TestTrueishJSON(c *C) { + tests := []struct { + v interface{} + b bool + errStr string + }{ + // nil + {nil, false, ""}, + // bool + {true, true, ""}, + {false, false, ""}, + // string + {"a", true, ""}, + {"", false, ""}, + // json.Number + {json.Number("1"), true, ""}, + {json.Number("-1"), true, ""}, + {json.Number("0"), false, ""}, + {json.Number("1.0"), true, ""}, + {json.Number("-1.0"), true, ""}, + {json.Number("0.0"), false, ""}, + // slices + {[]interface{}{"a"}, true, ""}, + {[]interface{}{}, false, ""}, + {[]string{"a"}, true, ""}, + {[]string{}, false, ""}, + // arrays + {[2]interface{}{"a", "b"}, true, ""}, + {[0]interface{}{}, false, ""}, + {[2]string{"a", "b"}, true, ""}, + {[0]string{}, false, ""}, + // maps + {map[string]interface{}{"a": "a"}, true, ""}, + {map[string]interface{}{}, false, ""}, + {map[interface{}]interface{}{"a": "a"}, true, ""}, + {map[interface{}]interface{}{}, false, ""}, + // invalid + {int(1), false, "cannot test type int for truth"}, + } + for _, t := range tests { + res, err := snap.TrueishJSON(t.v) + if t.errStr == "" { + c.Check(err, IsNil) + } else { + c.Check(err, ErrorMatches, t.errStr) + } + c.Check(res, Equals, t.b, Commentf("unexpected result for %v (%T), did not get expected %v", t.v, t.v, t.b)) + } +} + +func (s *SnapSuite) TestCmdWaitIntegration(c *C) { + restore := snap.MockWaitConfTimeout(2 * time.Millisecond) + defer restore() + + var tests = []struct { + v string + willWait bool + }{ + // not-waiting + {"1.0", false}, + {"-1.0", false}, + {"0.1", false}, + {"-0.1", false}, + {"1", false}, + {"-1", false}, + {`"a"`, false}, + {`["a"]`, false}, + {`{"a":"b"}`, false}, + // waiting + {"0", true}, + {"0.0", true}, + {"{}", true}, + {"[]", true}, + {`""`, true}, + {"null", true}, + } + + testValueCh := make(chan string, 2) + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + testValue := <-testValueCh + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/snaps/system/conf") + + fmt.Fprintln(w, fmt.Sprintf(`{"type":"sync", "status-code": 200, "result": {"test.value":%v}}`, testValue)) + n++ + }) + + for _, t := range tests { + n = 0 + testValueCh <- t.v + if t.willWait { + // a "trueish" value to ensure wait does not wait forever + testValueCh <- "42" + } + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"wait", "system", "test.value"}) + c.Assert(err, IsNil) + if t.willWait { + // we waited once, then got a non-wait value + c.Check(n, Equals, 2) + } else { + // no waiting happened + c.Check(n, Equals, 1) + } + } +} diff --git a/cmd/snap/cmd_warnings.go b/cmd/snap/cmd_warnings.go new file mode 100644 index 00000000..89fec04c --- /dev/null +++ b/cmd/snap/cmd_warnings.go @@ -0,0 +1,224 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "os" + "path/filepath" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/strutil/quantity" +) + +type cmdWarnings struct { + clientMixin + timeMixin + All bool `long:"all"` + Verbose bool `long:"verbose"` +} + +type cmdOkay struct{ clientMixin } + +var shortWarningsHelp = i18n.G("List warnings") +var longWarningsHelp = i18n.G(` +The warnings command lists the warnings that have been reported to the system. + +Once warnings have been listed with 'snap warnings', 'snap okay' may be used to +silence them. A warning that's been silenced in this way will not be listed +again unless it happens again, _and_ a cooldown time has passed. + +Warnings expire automatically, and once expired they are forgotten. +`) + +var shortOkayHelp = i18n.G("Acknowledge warnings") +var longOkayHelp = i18n.G(` +The okay command acknowledges the warnings listed with 'snap warnings'. + +Once acknowledged a warning won't appear again unless it re-occurrs and +sufficient time has passed. +`) + +func init() { + addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, timeDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "all": i18n.G("Show all warnings"), + // TRANSLATORS: This should not start with a lowercase letter. + "verbose": i18n.G("Show more information"), + }), nil) + addCommand("okay", shortOkayHelp, longOkayHelp, func() flags.Commander { return &cmdOkay{} }, nil, nil) +} + +func (cmd *cmdWarnings) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + now := time.Now() + + warnings, err := cmd.client.Warnings(client.WarningsOptions{All: cmd.All}) + if err != nil { + return err + } + if len(warnings) == 0 { + if t, _ := lastWarningTimestamp(); t.IsZero() { + fmt.Fprintln(Stdout, i18n.G("No warnings.")) + } else { + fmt.Fprintln(Stdout, i18n.G("No further warnings.")) + } + return nil + } + + if err := writeWarningTimestamp(now); err != nil { + return err + } + + w := tabWriter() + if cmd.Verbose { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", + i18n.G("First occurrence"), + i18n.G("Last occurrence"), + i18n.G("Expires after"), + i18n.G("Acknowledged"), + i18n.G("Repeats after"), + i18n.G("Warning")) + for _, warning := range warnings { + lastShown := "-" + if !warning.LastShown.IsZero() { + lastShown = cmd.fmtTime(warning.LastShown) + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", + cmd.fmtTime(warning.FirstAdded), + cmd.fmtTime(warning.LastAdded), + quantity.FormatDuration(warning.ExpireAfter.Seconds()), + lastShown, + quantity.FormatDuration(warning.RepeatAfter.Seconds()), + warning.Message) + } + } else { + fmt.Fprintf(w, "%s\t%s\n", i18n.G("Last occurrence"), i18n.G("Warning")) + for _, warning := range warnings { + fmt.Fprintf(w, "%s\t%s\n", cmd.fmtTime(warning.LastAdded), warning.Message) + } + } + w.Flush() + + return nil +} + +func (cmd *cmdOkay) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + last, err := lastWarningTimestamp() + if err != nil { + return fmt.Errorf("no client-side warning timestamp found: %v", err) + } + + return cmd.client.Okay(last) +} + +const warnFileEnvKey = "SNAPD_LAST_WARNING_TIMESTAMP_FILENAME" + +func warnFilename(homedir string) string { + if fn := os.Getenv(warnFileEnvKey); fn != "" { + return fn + } + + return filepath.Join(dirs.GlobalRootDir, homedir, ".snap", "warnings.json") +} + +type clientWarningData struct { + Timestamp time.Time `json:"timestamp"` +} + +func writeWarningTimestamp(t time.Time) error { + user, err := osutil.RealUser() + if err != nil { + return err + } + uid, gid, err := osutil.UidGid(user) + if err != nil { + return err + } + + filename := warnFilename(user.HomeDir) + if err := osutil.MkdirAllChown(filepath.Dir(filename), 0700, uid, gid); err != nil { + return err + } + + aw, err := osutil.NewAtomicFile(filename, 0600, 0, uid, gid) + if err != nil { + return err + } + // Cancel once Committed is a NOP :-) + defer aw.Cancel() + + enc := json.NewEncoder(aw) + if err := enc.Encode(clientWarningData{Timestamp: t}); err != nil { + return err + } + + return aw.Commit() +} + +func lastWarningTimestamp() (time.Time, error) { + user, err := osutil.RealUser() + if err != nil { + return time.Time{}, fmt.Errorf("cannot determine real user: %v", err) + } + f, err := os.Open(warnFilename(user.HomeDir)) + if err != nil { + return time.Time{}, fmt.Errorf("cannot open timestamp file: %v", err) + + } + dec := json.NewDecoder(f) + var d clientWarningData + if err := dec.Decode(&d); err != nil { + return time.Time{}, fmt.Errorf("cannot decode timestamp file: %v", err) + } + if dec.More() { + return time.Time{}, fmt.Errorf("spurious extra data in timestamp file") + } + return d.Timestamp, nil +} + +func maybePresentWarnings(count int, timestamp time.Time) { + if count == 0 { + return + } + + if last, _ := lastWarningTimestamp(); !timestamp.After(last) { + return + } + + fmt.Fprintf(Stderr, + i18n.NG("WARNING: There is %d new warning. See 'snap warnings'.\n", + "WARNING: There are %d new warnings. See 'snap warnings'.\n", + count), + count) +} diff --git a/cmd/snap/cmd_warnings_test.go b/cmd/snap/cmd_warnings_test.go new file mode 100644 index 00000000..426fced0 --- /dev/null +++ b/cmd/snap/cmd_warnings_test.go @@ -0,0 +1,206 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "time" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +type warningSuite struct { + BaseSnapSuite +} + +var _ = check.Suite(&warningSuite{}) + +const twoWarnings = `{ + "result": [ + { + "expire-after": "672h0m0s", + "first-added": "2018-09-19T12:41:18.505007495Z", + "last-added": "2018-09-19T12:41:18.505007495Z", + "message": "hello world number one", + "repeat-after": "24h0m0s" + }, + { + "expire-after": "672h0m0s", + "first-added": "2018-09-19T12:44:19.680362867Z", + "last-added": "2018-09-19T12:44:19.680362867Z", + "message": "hello world number two", + "repeat-after": "24h0m0s" + } + ], + "status": "OK", + "status-code": 200, + "type": "sync" + }` + +func mkWarningsFakeHandler(c *check.C, body string) func(w http.ResponseWriter, r *http.Request) { + var called bool + return func(w http.ResponseWriter, r *http.Request) { + if called { + c.Fatalf("expected a single request") + } + called = true + c.Check(r.URL.Path, check.Equals, "/v2/warnings") + c.Check(r.URL.Query(), check.HasLen, 0) + + buf, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + c.Check(string(buf), check.Equals, "") + c.Check(r.Method, check.Equals, "GET") + w.WriteHeader(200) + fmt.Fprintln(w, body) + } +} + +func (s *warningSuite) TestNoWarningsEver(c *check.C) { + s.RedirectClientToTestServer(mkWarningsFakeHandler(c, `{"type": "sync", "status-code": 200, "result": []}`)) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, "No warnings.\n") +} + +func (s *warningSuite) TestNoFurtherWarnings(c *check.C) { + snap.WriteWarningTimestamp(time.Now()) + + s.RedirectClientToTestServer(mkWarningsFakeHandler(c, `{"type": "sync", "status-code": 200, "result": []}`)) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, "No further warnings.\n") +} + +func (s *warningSuite) TestWarnings(c *check.C) { + s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings)) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, ` +Last occurrence Warning +2018-09-19T12:41:18Z hello world number one +2018-09-19T12:44:19Z hello world number two +`[1:]) +} + +func (s *warningSuite) TestVerboseWarnings(c *check.C) { + s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings)) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--verbose"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, ` +First occurrence Last occurrence Expires after Acknowledged Repeats after Warning +2018-09-19T12:41:18Z 2018-09-19T12:41:18Z 28d0h - 1d00h hello world number one +2018-09-19T12:44:19Z 2018-09-19T12:44:19Z 28d0h - 1d00h hello world number two +`[1:]) +} + +func (s *warningSuite) TestOkay(c *check.C) { + t0 := time.Now() + snap.WriteWarningTimestamp(t0) + + var n int + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + if n != 1 { + c.Fatalf("expected 1 request, now on %d", n) + } + c.Check(r.URL.Path, check.Equals, "/v2/warnings") + c.Check(r.URL.Query(), check.HasLen, 0) + c.Assert(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{"action": "okay", "timestamp": t0.Format(time.RFC3339Nano)}) + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(200) + fmt.Fprintln(w, `{ + "status": "OK", + "status-code": 200, + "type": "sync" + }`) + }) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"okay"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, "") +} + +func (s *warningSuite) TestListWithWarnings(c *check.C) { + var called bool + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + if called { + c.Fatalf("expected a single request") + } + called = true + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(r.URL.Query(), check.HasLen, 0) + + buf, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + c.Check(string(buf), check.Equals, "") + c.Check(r.Method, check.Equals, "GET") + w.WriteHeader(200) + fmt.Fprintln(w, `{ + "result": [{}], + "status": "OK", + "status-code": 200, + "type": "sync", + "warning-count": 2, + "warning-timestamp": "2018-09-19T12:44:19.680362867Z" + }`) + }) + cli := snap.Client() + rest, err := snap.Parser(cli).ParseArgs([]string{"list"}) + c.Assert(err, check.IsNil) + + { + // TODO: I hope to get to refactor run() so we can + // call it from tests and not have to do this (whole + // block) by hand + + count, stamp := cli.WarningsSummary() + c.Check(count, check.Equals, 2) + c.Check(stamp, check.Equals, time.Date(2018, 9, 19, 12, 44, 19, 680362867, time.UTC)) + + snap.MaybePresentWarnings(count, stamp) + } + + c.Check(rest, check.HasLen, 0) + c.Check(s.Stdout(), check.Equals, ` +Name Version Rev Tracking Publisher Notes + unset - - disabled +`[1:]) + c.Check(s.Stderr(), check.Equals, "WARNING: There are 2 new warnings. See 'snap warnings'.\n") + +} diff --git a/cmd/snap/cmd_watch.go b/cmd/snap/cmd_watch.go new file mode 100644 index 00000000..57f4b2d9 --- /dev/null +++ b/cmd/snap/cmd_watch.go @@ -0,0 +1,61 @@ +// -*- 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{ changeIDMixin } + +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{} + }, changeIDMixinOptDesc, changeIDMixinArgDesc) +} + +func (x *cmdWatch) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + id, err := x.GetChangeID() + if err != nil { + if err == noChangeFoundOK { + return nil + } + return err + } + + // this is the only valid use of wait without a waitMixin (ie + // without --no-wait), so we fake it here. + wmx := &waitMixin{skipAbort: true} + wmx.client = x.client + _, err = wmx.wait(id) + + return err +} diff --git a/cmd/snap/cmd_watch_test.go b/cmd/snap/cmd_watch_test.go new file mode 100644 index 00000000..53c218ec --- /dev/null +++ b/cmd/snap/cmd_watch_test.go @@ -0,0 +1,154 @@ +// -*- 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" + "time" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/progress/progresstest" +) + +var fmtWatchChangeJSON = `{"type": "sync", "result": { + "id": "two", + "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) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + defer snap.MockMaxGoneTime(time.Millisecond)() + defer snap.MockPollTime(time.Millisecond)() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + case 1: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/two") + fmt.Fprintf(w, fmtWatchChangeJSON, 0, 100*1024) + case 2: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/two") + fmt.Fprintf(w, fmtWatchChangeJSON, 50*1024, 100*1024) + case 3: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/two") + fmt.Fprintln(w, `{"type": "sync", "result": {"id": "two", "ready": true, "status": "Done"}}`) + default: + c.Errorf("expected 3 queries, currently on %d", n) + } + }) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"watch", "two"}) + c.Assert(err, IsNil) + c.Assert(rest, HasLen, 0) + c.Check(n, Equals, 3) + c.Check(meter.Values, DeepEquals, []float64{51200}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestWatchLast(c *C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + defer snap.MockMaxGoneTime(time.Millisecond)() + defer snap.MockPollTime(time.Millisecond)() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + case 1: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes") + fmt.Fprintln(w, mockChangesJSON) + case 2: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/two") + fmt.Fprintf(w, fmtWatchChangeJSON, 0, 100*1024) + case 3: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/two") + fmt.Fprintf(w, fmtWatchChangeJSON, 50*1024, 100*1024) + case 4: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/two") + fmt.Fprintln(w, `{"type": "sync", "result": {"id": "two", "ready": true, "status": "Done"}}`) + default: + c.Errorf("expected 4 queries, currently on %d", n) + } + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"watch", "--last=install"}) + c.Assert(err, IsNil) + c.Assert(rest, HasLen, 0) + c.Check(n, Equals, 4) + c.Check(meter.Values, DeepEquals, []float64{51200}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestWatchLastQuestionmark(c *C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + restore := snap.MockMaxGoneTime(time.Millisecond) + defer restore() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + c.Check(r.Method, Equals, "GET") + c.Assert(r.URL.Path, Equals, "/v2/changes") + switch n { + case 1, 2: + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + case 3, 4: + fmt.Fprintln(w, mockChangesJSON) + default: + c.Errorf("expected 4 calls, now on %d", n) + } + }) + for i := 0; i < 2; i++ { + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"watch", "--last=foobar?"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, "") + c.Check(s.Stderr(), Equals, "") + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"watch", "--last=foobar"}) + if i == 0 { + c.Assert(err, ErrorMatches, `no changes found`) + } else { + c.Assert(err, ErrorMatches, `no changes of type "foobar" found`) + } + } + + c.Check(n, Equals, 4) +} diff --git a/cmd/snap/cmd_whoami.go b/cmd/snap/cmd_whoami.go new file mode 100644 index 00000000..21c7e866 --- /dev/null +++ b/cmd/snap/cmd_whoami.go @@ -0,0 +1,58 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortWhoAmIHelp = i18n.G("Show the email the user is logged in with") +var longWhoAmIHelp = i18n.G(` +The whoami command shows the email the user is logged in with. +`) + +type cmdWhoAmI struct { + clientMixin +} + +func init() { + addCommand("whoami", shortWhoAmIHelp, longWhoAmIHelp, func() flags.Commander { return &cmdWhoAmI{} }, nil, nil) +} + +func (cmd cmdWhoAmI) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + email, err := cmd.client.WhoAmI() + if err != nil { + return err + } + if email == "" { + // just printing nothing looks weird (as if something had gone wrong) + email = "-" + } + fmt.Fprintln(Stdout, i18n.G("email:"), email) + return nil +} diff --git a/cmd/snap/color.go b/cmd/snap/color.go new file mode 100644 index 00000000..e9b5ed03 --- /dev/null +++ b/cmd/snap/color.go @@ -0,0 +1,181 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "strings" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/snap" +) + +type colorMixin struct { + Color string `long:"color" default:"auto" choice:"auto" choice:"never" choice:"always"` + Unicode string `long:"unicode" default:"auto" choice:"auto" choice:"never" choice:"always"` // do we want this hidden? +} + +func (mx colorMixin) getEscapes() *escapes { + esc := colorTable(mx.Color) + if canUnicode(mx.Unicode) { + esc.dash = "–" // that's an en dash (so yaml is happy) + esc.uparrow = "↑" + esc.tick = "✓" + } else { + esc.dash = "--" // two dashes keeps yaml happy also + esc.uparrow = "^" + esc.tick = "*" + } + + return &esc +} + +func canUnicode(mode string) bool { + switch mode { + case "always": + return true + case "never": + return false + } + if !isStdoutTTY { + return false + } + var lang string + for _, k := range []string{"LC_MESSAGES", "LC_ALL", "LANG"} { + lang = os.Getenv(k) + if lang != "" { + break + } + } + if lang == "" { + return false + } + lang = strings.ToUpper(lang) + return strings.Contains(lang, "UTF-8") || strings.Contains(lang, "UTF8") +} + +var isStdoutTTY = terminal.IsTerminal(1) + +func colorTable(mode string) escapes { + switch mode { + case "always": + return color + case "never": + return noesc + } + if !isStdoutTTY { + return noesc + } + if _, ok := os.LookupEnv("NO_COLOR"); ok { + // from http://no-color.org/: + // command-line software which outputs text with ANSI color added should + // check for the presence of a NO_COLOR environment variable that, when + // present (regardless of its value), prevents the addition of ANSI color. + return mono // bold & dim is still ok + } + if term := os.Getenv("TERM"); term == "xterm-mono" || term == "linux-m" { + // these are often used to flag "I don't want to see color" more than "I can't do color" + // (if you can't *do* color, `color` and `mono` should produce the same results) + return mono + } + return color +} + +var colorDescs = mixinDescs{ + // TRANSLATORS: This should not start with a lowercase letter. + "color": i18n.G("Use a little bit of color to highlight some things."), + // TRANSLATORS: This should not start with a lowercase letter. + "unicode": i18n.G("Use a little bit of Unicode to improve legibility."), +} + +type escapes struct { + green string + end string + + tick, dash, uparrow string +} + +var ( + color = escapes{ + green: "\033[32m", + end: "\033[0m", + } + + mono = escapes{ + green: "\033[1m", + end: "\033[0m", + } + + noesc = escapes{} +) + +// fillerPublisher is used to add an no-op escape sequence to a header in a +// tabwriter table, so that things line up. +func fillerPublisher(esc *escapes) string { + return esc.green + esc.end +} + +// longPublisher returns a string that'll present the publisher of a snap to the +// terminal user: +// +// * if the publisher's username and display name match, it's just the display +// name; otherwise, it'll include the username in parentheses +// +// * if the publisher is verified, it'll include a green check mark; otherwise, +// it'll include a no-op escape sequence of the same length as the escape +// sequence used to make it green (this so that tabwriter gets things right). +func longPublisher(esc *escapes, storeAccount *snap.StoreAccount) string { + if storeAccount == nil { + return esc.dash + esc.green + esc.end + } + badge := "" + if storeAccount.Validation == "verified" { + badge = esc.tick + } + // NOTE this makes e.g. 'Potato' == 'potato', and 'Potato Team' == 'potato-team', + // but 'Potato Team' != 'potatoteam', 'Potato Inc.' != 'potato' (in fact 'Potato Inc.' != 'potato-inc') + if strings.EqualFold(strings.Replace(storeAccount.Username, "-", " ", -1), storeAccount.DisplayName) { + return storeAccount.DisplayName + esc.green + badge + esc.end + } + return fmt.Sprintf("%s (%s%s%s%s)", storeAccount.DisplayName, storeAccount.Username, esc.green, badge, esc.end) +} + +// shortPublisher returns a string that'll present the publisher of a snap to the +// terminal user: +// +// * it'll always be just the username +// +// * if the publisher is verified, it'll include a green check mark; otherwise, +// it'll include a no-op escape sequence of the same length as the escape +// sequence used to make it green (this so that tabwriter gets things right). +func shortPublisher(esc *escapes, storeAccount *snap.StoreAccount) string { + if storeAccount == nil { + return "-" + esc.green + esc.end + } + badge := "" + if storeAccount.Validation == "verified" { + badge = esc.tick + } + return storeAccount.Username + esc.green + badge + esc.end + +} diff --git a/cmd/snap/color_test.go b/cmd/snap/color_test.go new file mode 100644 index 00000000..68c64451 --- /dev/null +++ b/cmd/snap/color_test.go @@ -0,0 +1,196 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "runtime" + // "fmt" + // "net/http" + + "gopkg.in/check.v1" + + cmdsnap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/snap" +) + +func setEnviron(env map[string]string) func() { + old := make(map[string]string, len(env)) + ok := make(map[string]bool, len(env)) + + for k, v := range env { + old[k], ok[k] = os.LookupEnv(k) + if v != "" { + os.Setenv(k, v) + } else { + os.Unsetenv(k) + } + } + + return func() { + for k := range ok { + if ok[k] { + os.Setenv(k, old[k]) + } else { + os.Unsetenv(k) + } + } + } +} + +func (s *SnapSuite) TestCanUnicode(c *check.C) { + // setenv is per thread + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + type T struct { + lang, lcAll, lcMsg string + expected bool + } + + for _, t := range []T{ + {expected: false}, // all locale unset + {lang: "C", expected: false}, + {lang: "C", lcAll: "C", expected: false}, + {lang: "C", lcAll: "C", lcMsg: "C", expected: false}, + {lang: "C.UTF-8", lcAll: "C", lcMsg: "C", expected: false}, // LC_MESSAGES wins + {lang: "C.UTF-8", lcAll: "C.UTF-8", lcMsg: "C", expected: false}, + {lang: "C.UTF-8", lcAll: "C.UTF-8", lcMsg: "C.UTF-8", expected: true}, + {lang: "C.UTF-8", lcAll: "C", lcMsg: "C.UTF-8", expected: true}, + {lang: "C", lcAll: "C", lcMsg: "C.UTF-8", expected: true}, + {lang: "C", lcAll: "C.UTF-8", expected: true}, + {lang: "C.UTF-8", expected: true}, + {lang: "C.utf8", expected: true}, // deals with a bit of rando weirdness + } { + restore := setEnviron(map[string]string{"LANG": t.lang, "LC_ALL": t.lcAll, "LC_MESSAGES": t.lcMsg}) + c.Check(cmdsnap.CanUnicode("never"), check.Equals, false) + c.Check(cmdsnap.CanUnicode("always"), check.Equals, true) + restoreIsTTY := cmdsnap.MockIsStdoutTTY(true) + c.Check(cmdsnap.CanUnicode("auto"), check.Equals, t.expected) + cmdsnap.MockIsStdoutTTY(false) + c.Check(cmdsnap.CanUnicode("auto"), check.Equals, false) + restoreIsTTY() + restore() + } +} + +func (s *SnapSuite) TestColorTable(c *check.C) { + // setenv is per thread + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + type T struct { + isTTY bool + noColor, term string + expected interface{} + desc string + } + + for _, t := range []T{ + {isTTY: false, expected: cmdsnap.NoEscColorTable, desc: "not a tty"}, + {isTTY: false, noColor: "1", expected: cmdsnap.NoEscColorTable, desc: "no tty *and* NO_COLOR set"}, + {isTTY: false, term: "linux-m", expected: cmdsnap.NoEscColorTable, desc: "no tty *and* mono term set"}, + {isTTY: true, expected: cmdsnap.ColorColorTable, desc: "is a tty"}, + {isTTY: true, noColor: "1", expected: cmdsnap.MonoColorTable, desc: "is a tty, but NO_COLOR set"}, + {isTTY: true, term: "linux-m", expected: cmdsnap.MonoColorTable, desc: "is a tty, but TERM=linux-m"}, + {isTTY: true, term: "xterm-mono", expected: cmdsnap.MonoColorTable, desc: "is a tty, but TERM=xterm-mono"}, + } { + restoreIsTTY := cmdsnap.MockIsStdoutTTY(t.isTTY) + restoreEnv := setEnviron(map[string]string{"NO_COLOR": t.noColor, "TERM": t.term}) + c.Check(cmdsnap.ColorTable("never"), check.DeepEquals, cmdsnap.NoEscColorTable, check.Commentf(t.desc)) + c.Check(cmdsnap.ColorTable("always"), check.DeepEquals, cmdsnap.ColorColorTable, check.Commentf(t.desc)) + c.Check(cmdsnap.ColorTable("auto"), check.DeepEquals, t.expected, check.Commentf(t.desc)) + restoreEnv() + restoreIsTTY() + } +} + +func (s *SnapSuite) TestPublisherEscapes(c *check.C) { + // just check never/always; for auto checks look above + type T struct { + color, unicode bool + username, display string + verified bool + short, long, fill string + } + for _, t := range []T{ + // non-verified equal under fold: + {color: false, unicode: false, username: "potato", display: "Potato", + short: "potato", long: "Potato", fill: ""}, + {color: false, unicode: true, username: "potato", display: "Potato", + short: "potato", long: "Potato", fill: ""}, + {color: true, unicode: false, username: "potato", display: "Potato", + short: "potato\x1b[32m\x1b[0m", long: "Potato\x1b[32m\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + {color: true, unicode: true, username: "potato", display: "Potato", + short: "potato\x1b[32m\x1b[0m", long: "Potato\x1b[32m\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + // verified equal under fold: + {color: false, unicode: false, username: "potato", display: "Potato", verified: true, + short: "potato*", long: "Potato*", fill: ""}, + {color: false, unicode: true, username: "potato", display: "Potato", verified: true, + short: "potato✓", long: "Potato✓", fill: ""}, + {color: true, unicode: false, username: "potato", display: "Potato", verified: true, + short: "potato\x1b[32m*\x1b[0m", long: "Potato\x1b[32m*\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + {color: true, unicode: true, username: "potato", display: "Potato", verified: true, + short: "potato\x1b[32m✓\x1b[0m", long: "Potato\x1b[32m✓\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + // non-verified, different + {color: false, unicode: false, username: "potato", display: "Carrot", + short: "potato", long: "Carrot (potato)", fill: ""}, + {color: false, unicode: true, username: "potato", display: "Carrot", + short: "potato", long: "Carrot (potato)", fill: ""}, + {color: true, unicode: false, username: "potato", display: "Carrot", + short: "potato\x1b[32m\x1b[0m", long: "Carrot (potato\x1b[32m\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + {color: true, unicode: true, username: "potato", display: "Carrot", + short: "potato\x1b[32m\x1b[0m", long: "Carrot (potato\x1b[32m\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + // verified, different + {color: false, unicode: false, username: "potato", display: "Carrot", verified: true, + short: "potato*", long: "Carrot (potato*)", fill: ""}, + {color: false, unicode: true, username: "potato", display: "Carrot", verified: true, + short: "potato✓", long: "Carrot (potato✓)", fill: ""}, + {color: true, unicode: false, username: "potato", display: "Carrot", verified: true, + short: "potato\x1b[32m*\x1b[0m", long: "Carrot (potato\x1b[32m*\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + {color: true, unicode: true, username: "potato", display: "Carrot", verified: true, + short: "potato\x1b[32m✓\x1b[0m", long: "Carrot (potato\x1b[32m✓\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + // some interesting equal-under-folds: + {color: false, unicode: false, username: "potato", display: "PoTaTo", + short: "potato", long: "PoTaTo", fill: ""}, + {color: false, unicode: false, username: "potato-team", display: "Potato Team", + short: "potato-team", long: "Potato Team", fill: ""}, + } { + pub := &snap.StoreAccount{Username: t.username, DisplayName: t.display} + if t.verified { + pub.Validation = "verified" + } + color := "never" + if t.color { + color = "always" + } + unicode := "never" + if t.unicode { + unicode = "always" + } + + mx := cmdsnap.ColorMixin(color, unicode) + esc := cmdsnap.ColorMixinGetEscapes(mx) + + c.Check(cmdsnap.ShortPublisher(esc, pub), check.Equals, t.short) + c.Check(cmdsnap.LongPublisher(esc, pub), check.Equals, t.long) + c.Check(cmdsnap.FillerPublisher(esc), check.Equals, t.fill) + } +} diff --git a/cmd/snap/complete.go b/cmd/snap/complete.go new file mode 100644 index 00000000..d7e77cb1 --- /dev/null +++ b/cmd/snap/complete.go @@ -0,0 +1,494 @@ +// -*- 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 ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" +) + +type installedSnapName string + +func (s installedSnapName) Complete(match string) []flags.Completion { + snaps, err := mkClient().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 +} + +func installedSnapNames(snaps []installedSnapName) []string { + names := make([]string, len(snaps)) + for i, name := range snaps { + names[i] = string(name) + } + + return names +} + +func completeFromSortedFile(filename, match string) ([]flags.Completion, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var ret []flags.Completion + + // TODO: look into implementing binary search + // e.g. https://github.com/pts/pts-line-bisect/ + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if line < match { + continue + } + if !strings.HasPrefix(line, match) { + break + } + ret = append(ret, flags.Completion{Item: line}) + if len(ret) > 10000 { + // too many matches; slow machines could take too long to process this + // e.g. the bbb takes ~1s to process ~2M entries (i.e. to reach the + // point of asking the user if they actually want to see that many + // results). 10k ought to be enough for anybody. + break + } + } + + return ret, nil +} + +type remoteSnapName string + +func (s remoteSnapName) Complete(match string) []flags.Completion { + if ret, err := completeFromSortedFile(dirs.SnapNamesFile, match); err == nil { + return ret + } + + if len(match) < 3 { + return nil + } + snaps, _, err := mkClient().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 +} + +func remoteSnapNames(snaps []remoteSnapName) []string { + names := make([]string, len(snaps)) + for i, name := range snaps { + names[i] = string(name) + } + + return names +} + +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 { + changes, err := mkClient().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 assertTypeName string + +func (n assertTypeName) Complete(match string) []flags.Completion { + cli := mkClient() + names, err := cli.AssertionTypes() + if err != nil { + return nil + } + ret := make([]flags.Completion, 0, len(names)) + for _, name := range names { + if strings.HasPrefix(name, match) { + ret = append(ret, flags.Completion{Item: name}) + } + } + + 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 +} + +type disconnectSlotOrPlugSpec struct { + SnapAndName +} + +func (dps disconnectSlotOrPlugSpec) Complete(match string) []flags.Completion { + spec := &interfaceSpec{ + SnapAndName: dps.SnapAndName, + slots: true, + plugs: true, + connected: true, + disconnected: false, + } + return spec.Complete(match) +} + +type disconnectSlotSpec struct { + SnapAndName +} + +// TODO: look at what the previous arg is, and filter accordingly +func (dss disconnectSlotSpec) Complete(match string) []flags.Completion { + spec := &interfaceSpec{ + SnapAndName: dss.SnapAndName, + slots: true, + plugs: false, + connected: true, + disconnected: false, + } + return spec.Complete(match) +} + +type connectPlugSpec struct { + SnapAndName +} + +func (cps connectPlugSpec) Complete(match string) []flags.Completion { + spec := &interfaceSpec{ + SnapAndName: cps.SnapAndName, + slots: false, + plugs: true, + connected: false, + disconnected: true, + } + return spec.Complete(match) +} + +type connectSlotSpec struct { + SnapAndName +} + +// TODO: look at what the previous arg is, and filter accordingly +func (css connectSlotSpec) Complete(match string) []flags.Completion { + spec := &interfaceSpec{ + SnapAndName: css.SnapAndName, + slots: true, + plugs: false, + connected: false, + disconnected: true, + } + return spec.Complete(match) +} + +type interfacesSlotOrPlugSpec struct { + SnapAndName +} + +func (is interfacesSlotOrPlugSpec) Complete(match string) []flags.Completion { + spec := &interfaceSpec{ + SnapAndName: is.SnapAndName, + slots: true, + plugs: true, + connected: true, + disconnected: true, + } + return spec.Complete(match) +} + +type interfaceSpec struct { + SnapAndName + slots bool + plugs bool + connected bool + disconnected bool +} + +func (spec *interfaceSpec) connFilter(numConns int) bool { + if spec.connected && numConns > 0 { + return true + } + if spec.disconnected && numConns == 0 { + return true + } + + return false +} + +func (spec *interfaceSpec) Complete(match string) []flags.Completion { + // Parse what the user typed so far, it can be either + // nothing (""), a "snap", a "snap:" or a "snap:name". + parts := strings.SplitN(match, ":", 2) + + // Ask snapd about available interfaces. + ifaces, err := mkClient().Connections() + if err != nil { + return nil + } + + snaps := make(map[string]bool) + + var ret []flags.Completion + + var prefix string + if len(parts) == 2 { + // The user typed the colon, means they know the snap they want; + // go with that. + prefix = parts[1] + snaps[parts[0]] = true + } else { + // The user is about to or has started typing a snap name but didn't + // reach the colon yet. Offer plugs for snaps with names that start + // like that. + snapPrefix := parts[0] + if spec.plugs { + for _, plug := range ifaces.Plugs { + if strings.HasPrefix(plug.Snap, snapPrefix) && spec.connFilter(len(plug.Connections)) { + snaps[plug.Snap] = true + } + } + } + if spec.slots { + for _, slot := range ifaces.Slots { + if strings.HasPrefix(slot.Snap, snapPrefix) && spec.connFilter(len(slot.Connections)) { + snaps[slot.Snap] = true + } + } + } + } + + if len(snaps) == 1 { + for snapName := range snaps { + actualName := snapName + if spec.plugs { + if spec.connected && snapName == "" { + actualName = "core" + } + for _, plug := range ifaces.Plugs { + if plug.Snap == actualName && strings.HasPrefix(plug.Name, prefix) && spec.connFilter(len(plug.Connections)) { + // TODO: in the future annotate plugs that can take + // multiple connection sensibly and don't skip those even + // if they have connections already. + ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:%s", snapName, plug.Name), Description: "plug"}) + } + } + } + if spec.slots { + if actualName == "" { + actualName = "core" + } + for _, slot := range ifaces.Slots { + if slot.Snap == actualName && strings.HasPrefix(slot.Name, prefix) && spec.connFilter(len(slot.Connections)) { + ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:%s", snapName, slot.Name), Description: "slot"}) + } + } + } + } + } else { + snaps: + for snapName := range snaps { + if spec.plugs { + for _, plug := range ifaces.Plugs { + if plug.Snap == snapName && spec.connFilter(len(plug.Connections)) { + ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:", snapName)}) + continue snaps + } + } + } + if spec.slots { + for _, slot := range ifaces.Slots { + if slot.Snap == snapName && spec.connFilter(len(slot.Connections)) { + ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:", snapName)}) + continue snaps + } + } + } + } + } + + return ret +} + +type interfaceName string + +func (s interfaceName) Complete(match string) []flags.Completion { + ifaces, err := mkClient().Interfaces(nil) + if err != nil { + return nil + } + + ret := make([]flags.Completion, 0, len(ifaces)) + for _, iface := range ifaces { + if strings.HasPrefix(iface.Name, match) { + ret = append(ret, flags.Completion{Item: iface.Name, Description: iface.Summary}) + } + } + + return ret +} + +type appName string + +func (s appName) Complete(match string) []flags.Completion { + cli := mkClient() + apps, err := cli.Apps(nil, client.AppOptions{}) + if err != nil { + return nil + } + + var ret []flags.Completion + for _, app := range apps { + if app.IsService() { + continue + } + name := snap.JoinSnapApp(app.Snap, app.Name) + if !strings.HasPrefix(name, match) { + continue + } + ret = append(ret, flags.Completion{Item: name}) + } + + return ret +} + +type serviceName string + +func (s serviceName) Complete(match string) []flags.Completion { + cli := mkClient() + apps, err := cli.Apps(nil, client.AppOptions{Service: true}) + if err != nil { + return nil + } + + snaps := map[string]bool{} + var ret []flags.Completion + for _, app := range apps { + if !app.IsService() { + continue + } + if !snaps[app.Snap] { + snaps[app.Snap] = true + ret = append(ret, flags.Completion{Item: app.Snap}) + } + ret = append(ret, flags.Completion{Item: app.Snap + "." + app.Name}) + } + + return ret +} + +type aliasOrSnap string + +func (s aliasOrSnap) Complete(match string) []flags.Completion { + aliases, err := mkClient().Aliases() + if err != nil { + return nil + } + var ret []flags.Completion + for snap, aliases := range aliases { + if strings.HasPrefix(snap, match) { + ret = append(ret, flags.Completion{Item: snap}) + } + for alias, status := range aliases { + if status.Status == "disabled" { + continue + } + if strings.HasPrefix(alias, match) { + ret = append(ret, flags.Completion{Item: alias}) + } + } + } + return ret +} + +type snapshotID uint64 + +func (snapshotID) Complete(match string) []flags.Completion { + shots, err := mkClient().SnapshotSets(0, nil) + if err != nil { + return nil + } + var ret []flags.Completion + for _, sg := range shots { + sid := strconv.FormatUint(sg.ID, 10) + if strings.HasPrefix(sid, match) { + ret = append(ret, flags.Completion{Item: sid}) + } + } + + return ret +} diff --git a/cmd/snap/error.go b/cmd/snap/error.go new file mode 100644 index 00000000..841e331c --- /dev/null +++ b/cmd/snap/error.go @@ -0,0 +1,400 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017-2018 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" + "errors" + "fmt" + "go/doc" + "os" + "os/user" + "strings" + "text/tabwriter" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" +) + +var errorPrefix = i18n.G("error: %v\n") + +func termSize() (width, height int) { + if f, ok := Stdout.(*os.File); ok { + width, height, _ = terminal.GetSize(int(f.Fd())) + } + + if width <= 0 { + width = int(osutil.GetenvInt64("COLUMNS")) + } + + if height <= 0 { + height = int(osutil.GetenvInt64("LINES")) + } + + if width < 40 { + width = 80 + } + + if height < 15 { + height = 25 + } + + return width, height +} + +func fill(para string, indent int) string { + width, _ := termSize() + + if width > 100 { + width = 100 + } + + // some terminals aren't happy about writing in the last + // column (they'll add line for you). We could check terminfo + // for "sam" (semi_auto_right_margin), but that's a lot of + // work just for this. + width-- + + var buf bytes.Buffer + indentStr := strings.Repeat(" ", indent) + doc.ToText(&buf, para, indentStr, indentStr, width-indent) + + return strings.TrimSpace(buf.String()) +} + +func errorToCmdMessage(snapName string, e error, opts *client.SnapOptions) (string, error) { + // do this here instead of in the caller for more DRY + err, ok := e.(*client.Error) + if !ok { + return "", e + } + // retryable errors are just passed through + if client.IsRetryable(err) { + return "", err + } + + // ensure the "real" error is available if we ask for it + logger.Debugf("error: %s", err) + + // FIXME: using err.Message in user-facing messaging is not + // l10n-friendly, and probably means we're missing ad-hoc messaging. + isError := true + usesSnapName := true + var msg string + switch err.Kind { + case client.ErrorKindNotSnap: + msg = i18n.G(`%q does not contain an unpacked snap. + +Try 'snapcraft prime' in your project directory, then 'snap try' again.`) + if snapName == "" || snapName == "./" { + errValStr, ok := err.Value.(string) + if ok && errValStr != "" { + snapName = errValStr + } + } + case client.ErrorKindSnapNotFound: + msg = i18n.G("snap %q not found") + if snapName == "" { + errValStr, ok := err.Value.(string) + if ok && errValStr != "" { + snapName = errValStr + } + } + case client.ErrorKindChannelNotAvailable, + client.ErrorKindArchitectureNotAvailable: + values, ok := err.Value.(map[string]interface{}) + if ok { + candName, _ := values["snap-name"].(string) + if candName != "" { + snapName = candName + } + action, _ := values["action"].(string) + arch, _ := values["architecture"].(string) + channel, _ := values["channel"].(string) + releases, _ := values["releases"].([]interface{}) + if snapName != "" && action != "" && arch != "" && channel != "" && len(releases) != 0 { + usesSnapName = false + msg = snapRevisionNotAvailableMessage(err.Kind, snapName, action, arch, channel, releases) + break + } + } + fallthrough + case client.ErrorKindRevisionNotAvailable: + if snapName == "" { + errValStr, ok := err.Value.(string) + if ok && errValStr != "" { + snapName = errValStr + } + } + + usesSnapName = false + // TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name). + msg = fmt.Sprintf(i18n.G(`snap %[1]q not available as specified (see 'snap info %[1]s')`), snapName) + + if opts != nil { + if opts.Revision != "" { + // TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name); %s is whatever the user used for --revision= + msg = fmt.Sprintf(i18n.G(`snap %[1]q revision %s not available (see 'snap info %[1]s')`), snapName, opts.Revision) + } else if opts.Channel != "" { + // (note --revision overrides --channel) + + // TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name); %q is whatever foo the user used for --channel=foo + msg = fmt.Sprintf(i18n.G(`snap %[1]q not available on channel %q (see 'snap info %[1]s')`), snapName, opts.Channel) + } + } + case client.ErrorKindSnapAlreadyInstalled: + isError = false + msg = i18n.G(`snap %q is already installed, see 'snap help refresh'`) + case client.ErrorKindSnapNeedsDevMode: + if opts != nil && opts.Dangerous { + msg = i18n.G("snap %q requires devmode or confinement override") + break + } + msg = i18n.G(` +The publisher of snap %q has indicated that they do not consider this revision +to be of production quality and that it is only meant for development or testing +at this point. As a consequence this snap will not refresh automatically and may +perform arbitrary system changes outside of the security sandbox snaps are +generally confined to, which may put your system at risk. + +If you understand and want to proceed repeat the command including --devmode; +if instead you want to install the snap forcing it into strict confinement +repeat the command including --jailmode.`) + case client.ErrorKindSnapNeedsClassic: + msg = i18n.G(` +This revision of snap %q was published using classic confinement and thus may +perform arbitrary system changes outside of the security sandbox that snaps are +usually confined to, which may put your system at risk. + +If you understand and want to proceed repeat the command including --classic. +`) + case client.ErrorKindSnapNotClassic: + msg = i18n.G(`snap %q is not compatible with --classic`) + case client.ErrorKindLoginRequired: + usesSnapName = false + u, _ := user.Current() + if u != nil && u.Username == "root" { + // TRANSLATORS: %s is an error message (e.g. “cannot yadda yadda: permission denied”) + msg = fmt.Sprintf(i18n.G(`%s (see 'snap help login')`), err.Message) + } else { + // TRANSLATORS: %s is an error message (e.g. “cannot yadda yadda: permission denied”) + msg = fmt.Sprintf(i18n.G(`%s (try with sudo)`), err.Message) + } + case client.ErrorKindSnapLocal: + msg = i18n.G("local snap %q is unknown to the store, use --amend to proceed anyway") + case client.ErrorKindNoUpdateAvailable: + isError = false + msg = i18n.G("snap %q has no updates available") + case client.ErrorKindSnapNotInstalled: + isError = false + usesSnapName = false + msg = err.Message + case client.ErrorKindNetworkTimeout: + isError = true + usesSnapName = false + msg = i18n.G("unable to contact snap store") + case client.ErrorKindSystemRestart: + isError = false + usesSnapName = false + msg = i18n.G("snapd is about to reboot the system") + default: + usesSnapName = false + msg = err.Message + } + + if usesSnapName { + msg = fmt.Sprintf(msg, snapName) + } + // 3 is the %v\n, which will be present in any locale + msg = fill(msg, len(errorPrefix)-3) + if isError { + return "", errors.New(msg) + } + + return msg, nil +} + +func snapRevisionNotAvailableMessage(kind, snapName, action, arch, channel string, releases []interface{}) string { + // releases contains all available (arch x channel) + // as reported by the store through the daemon + req, err := snap.ParseChannel(channel, arch) + if err != nil { + // TRANSLATORS: %q is the invalid request channel, %s is the snap name + msg := fmt.Sprintf(i18n.G("requested channel %q is not valid (see 'snap info %s' for valid ones)"), channel, snapName) + return msg + } + avail := make([]*snap.Channel, 0, len(releases)) + for _, v := range releases { + rel, _ := v.(map[string]interface{}) + relCh, _ := rel["channel"].(string) + relArch, _ := rel["architecture"].(string) + if relArch == "" { + logger.Debugf("internal error: %q daemon error carries a release with invalid/empty architecture: %v", kind, v) + continue + } + a, err := snap.ParseChannel(relCh, relArch) + if err != nil { + logger.Debugf("internal error: %q daemon error carries a release with invalid/empty channel (%v): %v", kind, err, v) + continue + } + avail = append(avail, &a) + } + + matches := map[string][]*snap.Channel{} + for _, a := range avail { + m := req.Match(a) + matchRepr := m.String() + if matchRepr != "" { + matches[matchRepr] = append(matches[matchRepr], a) + } + } + + // no release is for this architecture + if kind == client.ErrorKindArchitectureNotAvailable { + // TODO: add "Get more information..." hints once snap info + // support showing multiple/all archs + + // there are matching track+risk releases for other archs + if hits := matches["track:risk"]; len(hits) != 0 { + archs := strings.Join(archsForChannels(hits), ", ") + // TRANSLATORS: %q is for the snap name, %v is the requested channel, first %s is the system architecture short name, second %s is a comma separated list of available arch short names + msg := fmt.Sprintf(i18n.G("snap %q is not available on %v for this architecture (%s) but exists on other architectures (%s)."), snapName, req, arch, archs) + return msg + } + + // not even that, generic error + archs := strings.Join(archsForChannels(avail), ", ") + // TRANSLATORS: %q is for the snap name, first %s is the system architecture short name, second %s is a comma separated list of available arch short names + msg := fmt.Sprintf(i18n.G("snap %q is not available on this architecture (%s) but exists on other architectures (%s)."), snapName, arch, archs) + return msg + } + + // a branch was requested + if req.Branch != "" { + // there are matching arch+track+risk, give main track info + if len(matches["architecture:track:risk"]) != 0 { + trackRisk := snap.Channel{Track: req.Track, Risk: req.Risk} + trackRisk = trackRisk.Clean() + + // TRANSLATORS: %q is for the snap name, first %s is the full requested channel + msg := fmt.Sprintf(i18n.G("requested a non-existing branch on %s for snap %q: %s"), trackRisk.Full(), snapName, req.Branch) + return msg + } + + msg := fmt.Sprintf(i18n.G("requested a non-existing branch for snap %q: %s"), snapName, req.Full()) + return msg + } + + // TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint + preRelWarn := i18n.G("Please be mindful pre-release channels may include features not completely tested or implemented.") + // TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint + trackWarn := i18n.G("Please be mindful that different tracks may include different features.") + // TRANSLATORS: %s is for the snap name, will be concatenated after at the end of other error messages, possibly after a blank line + moreInfoHint := fmt.Sprintf(i18n.G("Get more information with 'snap info %s'."), snapName) + + // there are matching arch+track releases => give hint and instructions + // about pre-release channels + if hits := matches["architecture:track"]; len(hits) != 0 { + // TRANSLATORS: %q is for the snap name, %v is the requested channel + msg := fmt.Sprintf(i18n.G("snap %q is not available on %v but is available to install on the following channels:\n"), snapName, req) + msg += installTable(snapName, action, hits, false) + msg += "\n" + if req.Risk == "stable" { + msg += "\n" + preRelWarn + } + msg += "\n" + moreInfoHint + return msg + } + + // there are matching arch+risk releases => give hints and instructions + // about these other tracks + if hits := matches["architecture:risk"]; len(hits) != 0 { + // TRANSLATORS: %q is for the snap name, %s is the full requested channel + msg := fmt.Sprintf(i18n.G("snap %q is not available on %s but is available to install on the following tracks:\n"), snapName, req.Full()) + msg += installTable(snapName, action, hits, true) + msg += "\n\n" + trackWarn + msg += "\n" + moreInfoHint + return msg + } + + // generic error + // TRANSLATORS: %q is for the snap name, %s is the full requested channel + msg := fmt.Sprintf(i18n.G("snap %q is not available on %s but other tracks exist.\n"), snapName, req.Full()) + msg += "\n\n" + trackWarn + msg += "\n" + moreInfoHint + return msg +} + +func installTable(snapName, action string, avail []*snap.Channel, full bool) string { + b := &bytes.Buffer{} + w := tabwriter.NewWriter(b, len("candidate")+2, 1, 2, ' ', 0) + first := true + for _, a := range avail { + if first { + first = false + } else { + fmt.Fprint(w, "\n") + } + var ch string + if full { + ch = a.Full() + } else { + ch = a.String() + } + chOption := channelOption(a) + fmt.Fprintf(w, "%s\tsnap %s %s %s", ch, action, chOption, snapName) + } + w.Flush() + tbl := b.String() + // indent to drive fill/ToText to keep the tabulations intact + lines := strings.SplitAfter(tbl, "\n") + for i := range lines { + lines[i] = " " + lines[i] + } + return strings.Join(lines, "") +} + +func channelOption(c *snap.Channel) string { + if c.Branch == "" { + if c.Track == "" { + return fmt.Sprintf("--%s", c.Risk) + } + if c.Risk == "stable" { + return fmt.Sprintf("--channel=%s", c.Track) + } + } + return fmt.Sprintf("--channel=%s", c) +} + +func archsForChannels(cs []*snap.Channel) []string { + archs := []string{} + for _, c := range cs { + if !strutil.ListContains(archs, c.Architecture) { + archs = append(archs, c.Architecture) + } + } + return archs +} diff --git a/cmd/snap/export_test.go b/cmd/snap/export_test.go new file mode 100644 index 00000000..0dc4b1ce --- /dev/null +++ b/cmd/snap/export_test.go @@ -0,0 +1,243 @@ +// -*- 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/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/selinux" + "github.com/snapcore/snapd/store" +) + +var RunMain = run + +var ( + Client = mkClient + + FirstNonOptionIsRun = firstNonOptionIsRun + + CreateUserDataDirs = createUserDataDirs + ResolveApp = resolveApp + IsReexeced = isReexeced + MaybePrintServices = maybePrintServices + MaybePrintCommands = maybePrintCommands + SortByPath = sortByPath + AdviseCommand = adviseCommand + Antialias = antialias + FormatChannel = fmtChannel + PrintDescr = printDescr + TrueishJSON = trueishJSON + + CanUnicode = canUnicode + ColorTable = colorTable + MonoColorTable = mono + ColorColorTable = color + NoEscColorTable = noesc + ColorMixinGetEscapes = (colorMixin).getEscapes + FillerPublisher = fillerPublisher + LongPublisher = longPublisher + ShortPublisher = shortPublisher + + ReadRpc = readRpc + + WriteWarningTimestamp = writeWarningTimestamp + MaybePresentWarnings = maybePresentWarnings + + LongSnapDescription = longSnapDescription + SnapUsage = snapUsage + SnapHelpCategoriesIntro = snapHelpCategoriesIntro + SnapHelpAllFooter = snapHelpAllFooter + SnapHelpFooter = snapHelpFooter + HelpCategories = helpCategories + + LintArg = lintArg + LintDesc = lintDesc + + FixupArg = fixupArg +) + +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 MockGetEnv(f func(name string) string) (restore func()) { + osGetenvOrig := osGetenv + osGetenv = f + return func() { + osGetenv = osGetenvOrig + } +} + +func MockMountInfoPath(newMountInfoPath string) (restore func()) { + mountInfoPathOrig := mountInfoPath + mountInfoPath = newMountInfoPath + return func() { + mountInfoPath = mountInfoPathOrig + } +} + +func MockOsReadlink(f func(string) (string, error)) (restore func()) { + osReadlinkOrig := osReadlink + osReadlink = f + return func() { + osReadlink = osReadlinkOrig + } +} + +var AutoImportCandidates = autoImportCandidates + +func AliasInfoLess(snapName1, alias1, cmd1, snapName2, alias2, cmd2 string) bool { + x := aliasInfos{ + &aliasInfo{ + Snap: snapName1, + Alias: alias1, + Command: cmd1, + }, + &aliasInfo{ + Snap: snapName2, + Alias: alias2, + Command: cmd2, + }, + } + return x.Less(0, 1) +} + +func AssertTypeNameCompletion(match string) []flags.Completion { + return assertTypeName("").Complete(match) +} + +func MockIsStdoutTTY(t bool) (restore func()) { + oldIsStdoutTTY := isStdoutTTY + isStdoutTTY = t + return func() { + isStdoutTTY = oldIsStdoutTTY + } +} + +func MockIsStdinTTY(t bool) (restore func()) { + oldIsStdinTTY := isStdinTTY + isStdinTTY = t + return func() { + isStdinTTY = oldIsStdinTTY + } +} + +func MockTimeNow(newTimeNow func() time.Time) (restore func()) { + oldTimeNow := timeNow + timeNow = newTimeNow + return func() { + timeNow = oldTimeNow + } +} + +func MockTimeutilHuman(h func(time.Time) string) (restore func()) { + oldH := timeutilHuman + timeutilHuman = h + return func() { + timeutilHuman = oldH + } +} + +func MockWaitConfTimeout(d time.Duration) (restore func()) { + oldWaitConfTimeout := d + waitConfTimeout = d + return func() { + waitConfTimeout = oldWaitConfTimeout + } +} + +func Wait(cli *client.Client, id string) (*client.Change, error) { + wmx := waitMixin{} + wmx.client = cli + return wmx.wait(id) +} + +func ColorMixin(cmode, umode string) colorMixin { + return colorMixin{Color: cmode, Unicode: umode} +} + +func CmdAdviseSnap() *cmdAdviseSnap { + return &cmdAdviseSnap{} +} + +func MockSELinuxIsEnabled(isEnabled func() (bool, error)) (restore func()) { + old := selinuxIsEnabled + selinuxIsEnabled = isEnabled + return func() { + selinuxIsEnabled = old + } +} + +func MockSELinuxVerifyPathContext(verifypathcon func(string) (bool, error)) (restore func()) { + old := selinuxVerifyPathContext + selinuxVerifyPathContext = verifypathcon + return func() { + selinuxVerifyPathContext = old + } +} + +func MockSELinuxRestoreContext(restorecon func(string, selinux.RestoreMode) error) (restore func()) { + old := selinuxRestoreContext + selinuxRestoreContext = restorecon + return func() { + selinuxRestoreContext = old + } +} 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..1e2e513b --- /dev/null +++ b/cmd/snap/interfaces_common.go @@ -0,0 +1,55 @@ +// -*- 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" +) + +// 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..ac56b07c --- /dev/null +++ b/cmd/snap/interfaces_common_test.go @@ -0,0 +1,56 @@ +// -*- 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 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/last.go b/cmd/snap/last.go new file mode 100644 index 00000000..0abb383b --- /dev/null +++ b/cmd/snap/last.go @@ -0,0 +1,106 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package main + +import ( + "errors" + "fmt" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type changeIDMixin struct { + clientMixin + LastChangeType string `long:"last"` + Positional struct { + ID changeID `positional-arg-name:""` + } `positional-args:"yes"` +} + +var changeIDMixinOptDesc = mixinDescs{ + // TRANSLATORS: This should not start with a lowercase letter. + "last": i18n.G("Select last change of given type (install, refresh, remove, try, auto-refresh, etc.). A question mark at the end of the type means to do nothing (instead of returning an error) if no change of the given type is found. Note the question mark could need protecting from the shell."), +} + +var changeIDMixinArgDesc = []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Change ID"), +}} + +// should not be user-visible, but keep it clear and polite because mistakes happen +var noChangeFoundOK = errors.New("no change found but that's ok") + +func (l *changeIDMixin) GetChangeID() (string, error) { + if l.Positional.ID == "" && l.LastChangeType == "" { + return "", fmt.Errorf(i18n.G("please provide change ID or type with --last=")) + } + + if l.Positional.ID != "" { + if l.LastChangeType != "" { + return "", fmt.Errorf(i18n.G("cannot use change ID and type together")) + } + + return string(l.Positional.ID), nil + } + + cli := l.client + // note that at this point we know l.LastChangeType != "" + kind := l.LastChangeType + optional := false + if l := len(kind) - 1; kind[l] == '?' { + optional = true + kind = kind[:l] + } + // our internal change types use "-snap" postfix but let user skip it and use short form. + if kind == "refresh" || kind == "install" || kind == "remove" || kind == "connect" || kind == "disconnect" || kind == "configure" || kind == "try" { + kind += "-snap" + } + changes, err := queryChanges(cli, &client.ChangesOptions{Selector: client.ChangesAll}) + if err != nil { + return "", err + } + if len(changes) == 0 { + if optional { + return "", noChangeFoundOK + } + return "", fmt.Errorf(i18n.G("no changes found")) + } + chg := findLatestChangeByKind(changes, kind) + if chg == nil { + if optional { + return "", noChangeFoundOK + } + return "", fmt.Errorf(i18n.G("no changes of type %q found"), l.LastChangeType) + } + + return chg.ID, nil +} + +func findLatestChangeByKind(changes []*client.Change, kind string) (latest *client.Change) { + for _, chg := range changes { + if chg.Kind == kind && (latest == nil || latest.SpawnTime.Before(chg.SpawnTime)) { + latest = chg + } + } + return latest +} diff --git a/cmd/snap/main.go b/cmd/snap/main.go new file mode 100644 index 00000000..3b1db29e --- /dev/null +++ b/cmd/snap/main.go @@ -0,0 +1,521 @@ +// -*- 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" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "unicode" + "unicode/utf8" + + "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/httputil" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" +) + +func init() { + // set User-Agent for when 'snap' talks to the store directly (snap download etc...) + httputil.SetUserAgentFromVersion(cmd.Version, "snap") + + if osutil.GetenvBool("SNAPD_DEBUG") || osutil.GetenvBool("SNAPPY_TESTING") { + // in tests or when debugging, enforce the "tidy" lint checks + noticef = logger.Panicf + } + + // plug/slot sanitization not used nor possible from snap command, make it no-op + snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {} +} + +var ( + // Standard streams, redirected for testing. + Stdin io.Reader = os.Stdin + Stdout io.Writer = os.Stdout + Stderr io.Writer = os.Stderr + // overridden for testing + ReadPassword = terminal.ReadPassword + // set to logger.Panicf in testing + noticef = logger.Noticef +) + +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 + alias string + extra func(*flags.Command) +} + +// commands holds information about all non-debug commands. +var commands []*cmdInfo + +// debugCommands holds information about all debug commands. +var debugCommands []*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 +} + +// addDebugCommand replaces parser.addCommand() in a way that is +// compatible with re-constructing a pristine parser. It is meant for +// adding debug commands. +func addDebugCommand(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, + } + debugCommands = append(debugCommands, 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 { + // decode the first rune instead of converting all of desc into []rune + r, _ := utf8.DecodeRuneInString(desc) + // note IsLower != !IsUpper for runes with no upper/lower. + // Also note that login.u.c. is the only exception we're allowing for + // now, but the list of exceptions could grow -- if it does, we might + // want to change it to check for urlish things instead of just + // login.u.c. + if unicode.IsLower(r) && !strings.HasPrefix(desc, "login.ubuntu.com") { + noticef("description of %s's %q is lowercase: %q", cmdName, optName, desc) + } + } +} + +func lintArg(cmdName, optName, desc, origDesc string) { + lintDesc(cmdName, optName, desc, origDesc) + if len(optName) > 0 && optName[0] == '<' && optName[len(optName)-1] == '>' { + return + } + if len(optName) > 0 && optName[0] == '<' && strings.HasSuffix(optName, ">s") { + // see comment in fixupArg about the >s case + return + } + noticef("argument %q's %q should begin with < and end with >", cmdName, optName) +} + +func fixupArg(optName string) string { + // Due to misunderstanding some localized versions of option name are + // literally "