diff --git a/interfaces/builtin/desktop.go b/interfaces/builtin/desktop.go index 24ae713b2de..711d12198f2 100644 --- a/interfaces/builtin/desktop.go +++ b/interfaces/builtin/desktop.go @@ -34,11 +34,26 @@ import ( const desktopSummary = `allows access to basic graphical desktop resources` +// The weird allow-installation/deny-installation construct is +// intended to prevent app snaps from the store that provide this slot +// from installing without an override, while allowing an unpublished +// snap to still be installed. +// +// The deny-connection and deny-auto-connection rules should ideally +// use a slot-snap-type constraint when that is supported. const desktopBaseDeclarationSlots = ` desktop: allow-installation: slot-snap-type: + - app - core + deny-installation: + slot-snap-type: + - app + deny-connection: + on-classic: false + deny-auto-connection: + on-classic: false ` const desktopConnectedPlugAppArmor = ` @@ -63,6 +78,57 @@ owner @{HOME}/.local/share/fonts/{,**} r, # some applications are known to mmap fonts /usr/{,local/}share/fonts/** m, +# Allow access to xdg-document-portal file system. Access control is +# handled by bind mounting a snap-specific sub-tree to this location +# (ie, this is /run/user//doc/by-app/snap.@{SNAP_INSTANCE_NAME} +# on the host). +owner /run/user/[0-9]*/doc/{,*/} r, +# Allow rw access without owner match to the documents themselves since +# the user guided the access and can specify anything DAC allows. +/run/user/[0-9]*/doc/*/** rw, + +# Allow access to xdg-desktop-portal and xdg-document-portal +dbus (receive, send) + bus=session + interface=org.freedesktop.portal.* + path=/org/freedesktop/portal/{desktop,documents}{,/**} + peer=(label=unconfined), + +dbus (receive, send) + bus=session + interface=org.freedesktop.DBus.Properties + path=/org/freedesktop/portal/{desktop,documents}{,/**} + peer=(label=unconfined), + +# The portals service is normally running and newer versions of +# xdg-desktop-portal include AssumedAppArmor=unconfined. Since older +# systems don't have this and because gtkfilechoosernativeportal.c relies on +# service activation, allow sends to peer=(name=org.freedesktop.portal.{Desktop,Documents}) +# for service activation. +dbus (send) + bus=session + interface=org.freedesktop.portal.* + path=/org/freedesktop/portal/desktop{,/**} + peer=(name=org.freedesktop.portal.Desktop), +dbus (send) + bus=session + interface=org.freedesktop.DBus.Properties + path=/org/freedesktop/portal/desktop{,/**} + peer=(name=org.freedesktop.portal.Desktop), +dbus (send) + bus=session + interface=org.freedesktop.portal.* + path=/org/freedesktop/portal/documents{,/**} + peer=(name=org.freedesktop.portal.Documents), +dbus (send) + bus=session + interface=org.freedesktop.DBus.Properties + path=/org/freedesktop/portal/documents{,/**} + peer=(name=org.freedesktop.portal.Documents), + +` + +const desktopConnectedPlugAppArmorClassic = ` # subset of gnome abstraction /etc/gtk-3.0/settings.ini r, owner @{HOME}/.config/gtk-3.0/settings.ini r, @@ -237,54 +303,6 @@ dbus (send) member={Check,CheckSub,Get,GetSub,Set,SetSub} peer=(label=unconfined), -# Allow access to xdg-document-portal file system. Access control is -# handled by bind mounting a snap-specific sub-tree to this location -# (ie, this is /run/user//doc/by-app/snap.@{SNAP_INSTANCE_NAME} -# on the host). -owner /run/user/[0-9]*/doc/{,*/} r, -# Allow rw access without owner match to the documents themselves since -# the user guided the access and can specify anything DAC allows. -/run/user/[0-9]*/doc/*/** rw, - -# Allow access to xdg-desktop-portal and xdg-document-portal -dbus (receive, send) - bus=session - interface=org.freedesktop.portal.* - path=/org/freedesktop/portal/{desktop,documents}{,/**} - peer=(label=unconfined), - -dbus (receive, send) - bus=session - interface=org.freedesktop.DBus.Properties - path=/org/freedesktop/portal/{desktop,documents}{,/**} - peer=(label=unconfined), - -# The portals service is normally running and newer versions of -# xdg-desktop-portal include AssumedAppArmor=unconfined. Since older -# systems don't have this and because gtkfilechoosernativeportal.c relies on -# service activation, allow sends to peer=(name=org.freedesktop.portal.{Desktop,Documents}) -# for service activation. -dbus (send) - bus=session - interface=org.freedesktop.portal.* - path=/org/freedesktop/portal/desktop{,/**} - peer=(name=org.freedesktop.portal.Desktop), -dbus (send) - bus=session - interface=org.freedesktop.DBus.Properties - path=/org/freedesktop/portal/desktop{,/**} - peer=(name=org.freedesktop.portal.Desktop), -dbus (send) - bus=session - interface=org.freedesktop.portal.* - path=/org/freedesktop/portal/documents{,/**} - peer=(name=org.freedesktop.portal.Documents), -dbus (send) - bus=session - interface=org.freedesktop.DBus.Properties - path=/org/freedesktop/portal/documents{,/**} - peer=(name=org.freedesktop.portal.Documents), - # These accesses are noisy and applications can't do anything with the found # icon files, so explicitly deny to silence the denials deny /var/lib/snapd/desktop/icons/{,**/} r, @@ -324,7 +342,7 @@ dbus (send, receive) peer=(label=unconfined), ` -var desktopPermanentSlotAppArmor = ` +const desktopPermanentSlotAppArmor = ` # Description: Can provide various desktop services #include @@ -351,6 +369,30 @@ dbus (receive) interface=org.gtk.Notifications member="{AddNotification,RemoveNotification}" peer=(label=unconfined), + +# Allow unconfined xdg-desktop-portal to communicate with impl +# services provided by the snap. +dbus (receive, send) + bus=session + path=/org/freedesktop/portal/desktop{,/**} + interface=org.freedesktop.impl.portal.* + peer=(label=unconfined), +dbus (receive, send) + bus=session + path=/org/freedesktop/portal/desktop{,/**} + interface=org.freedesktop.DBus.Properties + peer=(label=unconfined), + +# Allow access to various paths gnome-session and gnome-shell need. +/etc/fonts{,/**} r, +/etc/glvnd{,/**} r, +/etc/gnome/defaults.list r, +/etc/gtk-3.0{,/**} r, +/etc/shells r, +/etc/xdg/autostart{,/**} r, +/etc/xdg/user-dirs.conf r, +/etc/xdg/user-dirs.defaults r, +/run/udev/tags/seat{,/**} r, ` type desktopInterface struct { @@ -389,6 +431,11 @@ func (iface *desktopInterface) fontconfigDirs(plug *interfaces.ConnectedPlug) ([ func (iface *desktopInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { spec.AddSnippet(desktopConnectedPlugAppArmor) + if implicitSystemConnectedSlot(slot) { + // Extra rules that have not been ported to work with + // a desktop slot provided by a snap. + spec.AddSnippet(desktopConnectedPlugAppArmorClassic) + } // Allow mounting document portal emit := spec.AddUpdateNSf @@ -396,12 +443,9 @@ func (iface *desktopInterface) AppArmorConnectedPlug(spec *apparmor.Specificatio emit(" mount options=(bind) /run/user/[0-9]*/doc/by-app/snap.%s/ -> /run/user/[0-9]*/doc/,\n", plug.Snap().InstanceName()) emit(" umount /run/user/[0-9]*/doc/,\n\n") - if !release.OnClassic { - // We only need the font mount rules on classic systems - return nil - } - - // Allow mounting fonts + // Allow mounting fonts. For the app-provided slot case, we + // assume that the slot snap is using the boot base snap as + // its base, and that base contains fonts. fontDirs, err := iface.fontconfigDirs(plug) if err != nil { return err @@ -426,11 +470,6 @@ func (iface *desktopInterface) MountConnectedPlug(spec *mount.Specification, plu Options: []string{"bind", "rw", osutil.XSnapdIgnoreMissing()}, }) - if !release.OnClassic { - // We only need the font mount rules on classic systems - return nil - } - fontDirs, err := iface.fontconfigDirs(plug) if err != nil { return err @@ -466,6 +505,13 @@ func (iface *desktopInterface) MountConnectedPlug(spec *mount.Specification, plu return nil } +func (iface *desktopInterface) AppArmorPermanentSlot(spec *apparmor.Specification, slot *snap.SlotInfo) error { + if !implicitSystemPermanentSlot(slot) { + spec.AddSnippet(desktopPermanentSlotAppArmor) + } + return nil +} + func (iface *desktopInterface) BeforePreparePlug(plug *snap.PlugInfo) error { _, err := iface.shouldMountHostFontCache(plug) return err diff --git a/interfaces/builtin/desktop_test.go b/interfaces/builtin/desktop_test.go index 6267c47ad4d..5abce6852b7 100644 --- a/interfaces/builtin/desktop_test.go +++ b/interfaces/builtin/desktop_test.go @@ -23,7 +23,6 @@ import ( "fmt" "os" "path/filepath" - "strings" . "gopkg.in/check.v1" @@ -39,6 +38,8 @@ import ( type DesktopInterfaceSuite struct { iface interfaces.Interface + appSlotInfo *snap.SlotInfo + appSlot *interfaces.ConnectedSlot coreSlotInfo *snap.SlotInfo coreSlot *interfaces.ConnectedSlot plugInfo *snap.PlugInfo @@ -56,6 +57,14 @@ apps: plugs: [desktop] ` +const desktopAppSlotYaml = `name: provider +version: 0 +apps: + app: +slots: + desktop: +` + const desktopCoreYaml = `name: core version: 0 type: os @@ -65,6 +74,7 @@ slots: func (s *DesktopInterfaceSuite) SetUpTest(c *C) { s.plug, s.plugInfo = MockConnectedPlug(c, desktopConsumerYaml, nil, "desktop") + s.appSlot, s.appSlotInfo = MockConnectedSlot(c, desktopAppSlotYaml, nil, "desktop") s.coreSlot, s.coreSlotInfo = MockConnectedSlot(c, desktopCoreYaml, nil, "desktop") } @@ -93,31 +103,39 @@ func (s *DesktopInterfaceSuite) TestAppArmorSpec(c *C) { restore := release.MockOnClassic(false) defer restore() - // connected plug to core slot + // On an all-snaps system, the desktop interface grants access + // to system fonts. spec := &apparmor.Specification{} - c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.appSlot), IsNil) c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) - c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Description: Can access basic graphical desktop resources") - c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "#include ") - c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/etc/gtk-3.0/settings.ini r,") - c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Allow access to xdg-desktop-portal and xdg-document-portal") + c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Description: Can access basic graphical desktop resources") + c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "#include ") - // On an all-snaps system, the only UpdateNS rule is for the - // document portal. + // There are UpdateNS rules to allow mounting the font directories too updateNS := spec.UpdateNS() - profile0 := ` # Mount the document portal - mount options=(bind) /run/user/[0-9]*/doc/by-app/snap.consumer/ -> /run/user/[0-9]*/doc/, - umount /run/user/[0-9]*/doc/, + c.Check(updateNS, testutil.Contains, " # Read-only access to /usr/share/fonts\n") + c.Check(updateNS, testutil.Contains, " # Read-only access to /usr/local/share/fonts\n") + c.Check(updateNS, testutil.Contains, " # Read-only access to /var/cache/fontconfig\n") -` - c.Assert(strings.Join(updateNS, ""), Equals, profile0) + // There are permanent rules on the slot side + spec = &apparmor.Specification{} + c.Assert(spec.AddPermanentSlot(s.iface, s.appSlotInfo), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.provider.app"}) + c.Check(spec.SnippetForTag("snap.provider.app"), testutil.Contains, "# Description: Can provide various desktop services") + c.Check(spec.SnippetForTag("snap.provider.app"), testutil.Contains, "interface=org.freedesktop.impl.portal.*") - // On a classic system, there are UpdateNS rules for the host - // system font mounts + // On a classic system, additional permissions are granted restore = release.MockOnClassic(true) defer restore() spec = &apparmor.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) + + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Description: Can access basic graphical desktop resources") + c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/etc/gtk-3.0/settings.ini r,") + c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Allow access to xdg-desktop-portal and xdg-document-portal") + + // As well as the font directories, the document portal can be mounted updateNS = spec.UpdateNS() c.Check(updateNS, testutil.Contains, " # Mount the document portal\n") c.Check(updateNS, testutil.Contains, " # Read-only access to /usr/share/fonts\n") @@ -126,6 +144,7 @@ func (s *DesktopInterfaceSuite) TestAppArmorSpec(c *C) { // connected plug to core slot spec = &apparmor.Specification{} + c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil) c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil) c.Assert(spec.SecurityTags(), HasLen, 0) } @@ -140,16 +159,11 @@ func (s *DesktopInterfaceSuite) TestMountSpec(c *C) { restore := release.MockOnClassic(false) defer restore() - // On all-snaps systems, the font related mount entries are missing + // On all-snaps systems, the mounts are present spec := &mount.Specification{} - c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) - c.Check(spec.MountEntries(), HasLen, 0) - - entries := spec.UserMountEntries() - c.Check(entries, HasLen, 1) - c.Check(entries[0].Name, Equals, "$XDG_RUNTIME_DIR/doc/by-app/snap.consumer") - c.Check(entries[0].Dir, Equals, "$XDG_RUNTIME_DIR/doc") - c.Check(entries[0].Options, DeepEquals, []string{"bind", "rw", "x-snapd.ignore-missing"}) + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.appSlot), IsNil) + c.Check(spec.MountEntries(), HasLen, 3) + c.Check(spec.UserMountEntries(), HasLen, 1) // On classic systems, a number of font related directories // are bind mounted from the host system if they exist. @@ -160,7 +174,7 @@ func (s *DesktopInterfaceSuite) TestMountSpec(c *C) { spec = &mount.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) - entries = spec.MountEntries() + entries := spec.MountEntries() c.Assert(entries, HasLen, 3) const hostfs = "/var/lib/snapd/hostfs" @@ -178,7 +192,9 @@ func (s *DesktopInterfaceSuite) TestMountSpec(c *C) { entries = spec.UserMountEntries() c.Assert(entries, HasLen, 1) + c.Check(entries[0].Name, Equals, "$XDG_RUNTIME_DIR/doc/by-app/snap.consumer") c.Check(entries[0].Dir, Equals, "$XDG_RUNTIME_DIR/doc") + c.Check(entries[0].Options, DeepEquals, []string{"bind", "rw", "x-snapd.ignore-missing"}) for _, distroWithQuirks := range []string{"fedora", "arch"} { restore = release.MockReleaseInfo(&release.OS{ID: distroWithQuirks}) diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go index c04669517b3..2729b87e6f6 100644 --- a/interfaces/policy/basedeclaration_test.go +++ b/interfaces/policy/basedeclaration_test.go @@ -881,9 +881,22 @@ func (s *baseDeclSuite) TestSlotInstallation(c *C) { } } - // test docker specially - ic := s.installSlotCand(c, "docker", snap.TypeApp, ``) + // test desktop specifically + ic := s.installSlotCand(c, "desktop", snap.TypeApp, ``) err := ic.Check() + c.Check(err, Not(IsNil)) + c.Check(err, ErrorMatches, "installation denied by \"desktop\" slot rule of interface \"desktop\"") + // ... but the minimal check (used by --dangerous) allows installation + icMin := &policy.InstallCandidateMinimalCheck{ + Snap: ic.Snap, + BaseDeclaration: s.baseDecl, + } + err = icMin.Check() + c.Check(err, IsNil) + + // test docker specially + ic = s.installSlotCand(c, "docker", snap.TypeApp, ``) + err = ic.Check() c.Assert(err, Not(IsNil)) c.Assert(err, ErrorMatches, "installation not allowed by \"docker\" slot rule of interface \"docker\"") diff --git a/tests/main/interfaces-desktop-host-fonts-core/desktop-provider/meta/snap.yaml b/tests/main/interfaces-desktop-host-fonts-core/desktop-provider/meta/snap.yaml new file mode 100644 index 00000000000..940edce8c42 --- /dev/null +++ b/tests/main/interfaces-desktop-host-fonts-core/desktop-provider/meta/snap.yaml @@ -0,0 +1,4 @@ +name: desktop-provider +version: 1.0 +slots: + desktop: null diff --git a/tests/main/interfaces-desktop-host-fonts-core/task.yaml b/tests/main/interfaces-desktop-host-fonts-core/task.yaml new file mode 100644 index 00000000000..c77a6824869 --- /dev/null +++ b/tests/main/interfaces-desktop-host-fonts-core/task.yaml @@ -0,0 +1,59 @@ +summary: Ensure that the desktop interface gives access to host fonts + +details: | + In order to ensure that confined applications have access to fonts + covering the user's spoken language, the host system's fonts are + bind mounted into the sandbox. + +systems: + - ubuntu-core-* + +prepare: | + tests.session -u test prepare + + # There are no fonts available on Ubuntu Core, so bind mount some + # files to the corresponding directories. + mkdir -p /tmp/distro-fonts /tmp/local-fonts + echo "Distribution font" > /tmp/distro-fonts/dist-font.txt + echo "Local font" > /tmp/local-fonts/local-font.txt + mount --bind /tmp/distro-fonts /usr/share/fonts + mount --bind /tmp/local-fonts /usr/local/share/fonts + + # User directories created via tests.session for correct ownership and SELinux context. + tests.session -u test exec mkdir -p /home/test/.fonts + echo "User font 1" | tests.session -u test exec tee /home/test/.fonts/user-font1.txt + + tests.session -u test exec mkdir -p /home/test/.local/share/fonts + echo "User font 2" | tests.session -u test exec tee /home/test/.local/share/fonts/user-font2.txt + + echo "Install the test-snapd-desktop snap" + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local desktop-provider + +restore: | + tests.session -u test restore + umount /usr/share/fonts + umount /usr/local/share/fonts + rm -rf /tmp/distro-fonts /tmp/local-fonts + rm -rf /home/test/.fonts + rm -f /home/test/.local/share/fonts/user-font2.txt + +execute: | + echo "The plug is disconnected by default" + snap connections test-snapd-desktop | MATCH "desktop +test-snapd-desktop:desktop +- +-" + + echo "The plug can be connected" + snap connect test-snapd-desktop:desktop desktop-provider:desktop + snap connections test-snapd-desktop | MATCH "desktop +test-snapd-desktop:desktop +desktop-provider:desktop +manual" + + echo "Checking access to host /usr/share/fonts" + tests.session -u test exec test-snapd-desktop.check-files /usr/share/fonts/dist-font.txt | MATCH "Distribution font" + + echo "Checking access to host /usr/local/share/fonts" + tests.session -u test exec test-snapd-desktop.check-files /usr/local/share/fonts/local-font.txt | MATCH "Local font" + + echo "Checking access to host ~/.fonts" + tests.session -u test exec test-snapd-desktop.check-files /home/test/.fonts/user-font1.txt | MATCH "User font 1" + + echo "Checking access to host ~/.local/share/fonts" + tests.session -u test exec test-snapd-desktop.check-files /home/test/.local/share/fonts/user-font2.txt | MATCH "User font 2" diff --git a/tests/main/interfaces-many-snap-provided/test-snapd-policy-app-provider-core/meta/snap.yaml b/tests/main/interfaces-many-snap-provided/test-snapd-policy-app-provider-core/meta/snap.yaml index eb1572ef855..9ef14e53016 100644 --- a/tests/main/interfaces-many-snap-provided/test-snapd-policy-app-provider-core/meta/snap.yaml +++ b/tests/main/interfaces-many-snap-provided/test-snapd-policy-app-provider-core/meta/snap.yaml @@ -25,6 +25,7 @@ slots: interface: dbus bus: system name: test.system + desktop: null docker: null fwupd: null location-control: null