Skip to content

Commit

Permalink
interfaces/desktop: allow snaps to provide a desktop slot, granting a…
Browse files Browse the repository at this point in the history
…ccess to system fonts and xdg-desktop-portal (canonical#10753)

* Mount system fonts via app desktop slot

* interfaces: update desktop tests to match changes to interface

* interfaces: note that desktop interface allows app slot implementations

* tests: add desktop slot to test-snapd-policy-app-provider-core

* interfaces: deny connection of desktop interface by default on Ubuntu Core.

* tests: add a spread test for providing a desktop slot on Ubuntu Core

* interfaces: add support for xdg-desktop-portal to app-provided desktop slot

In this setup, we expect xdg-desktop-portal and xdg-document-portal to
be running outside of confinement (as snap userd does), with the user
interface backend services running in the context of the slot snap.

* interfaces: add a comment about the assumption that a snap providing a
desktop slot uses the boot base snap as its base.

* Fix documents portal not being launched

It uses a different bus name from the desktop portal.

* interfaces: fix up permanent slot rules for desktop interface

* interfaces: update desktop slot declaration to only allow app snaps to provide the slot under the minimal install check

* interfaces: add access to files necessary for xdg-user-dirs to the
desktop slot

* interfaces: add file access covering what the ubuntu-desktop-session:shell-config-files system-files plug granted

* interfaces: add a comment noting the unusual base declaration for desktop

* interfaces: make desktopPermanentSlotAppArmor a constant

---------

Co-authored-by: Marcus Tomlinson <[email protected]>
Co-authored-by: Robert Ancell <[email protected]>
Co-authored-by: Ken VanDine <[email protected]>
  • Loading branch information
4 people authored Sep 14, 2023
1 parent 1437ce2 commit c535590
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 88 deletions.
166 changes: 106 additions & 60 deletions interfaces/builtin/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
Expand All @@ -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/<uid>/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,
Expand Down Expand Up @@ -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/<uid>/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,
Expand Down Expand Up @@ -324,7 +342,7 @@ dbus (send, receive)
peer=(label=unconfined),
`

var desktopPermanentSlotAppArmor = `
const desktopPermanentSlotAppArmor = `
# Description: Can provide various desktop services
#include <abstractions/dbus-session-strict>
Expand All @@ -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 {
Expand Down Expand Up @@ -389,19 +431,21 @@ 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
emit(" # Mount the document portal\n")
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
68 changes: 42 additions & 26 deletions interfaces/builtin/desktop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

. "gopkg.in/check.v1"

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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")
}

Expand Down Expand Up @@ -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 <abstractions/fonts>")
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 <abstractions/fonts>")

// 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")
Expand All @@ -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)
}
Expand All @@ -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.
Expand All @@ -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"
Expand All @@ -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})
Expand Down
Loading

0 comments on commit c535590

Please sign in to comment.