Skip to content

Commit f1cc07c

Browse files
author
Charles
committed
interfaces/builtin,tests: add xdg-portal-permission-store interface
Provide a dedicated super-privileged interface for xdg-desktop-portal PermissionStore access. Also add a spread test that starts a fake PermissionStore service on the session bus and verifies that a confined snap can only talk to it when xdg-portal-permission-store is connected.
1 parent 409deb4 commit f1cc07c

7 files changed

Lines changed: 483 additions & 0 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// -*- Mode: Go; indent-tabs-mode: t -*-
2+
3+
/*
4+
* Copyright (C) 2026 Canonical Ltd
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 3 as
8+
* published by the Free Software Foundation.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*
18+
*/
19+
20+
package builtin
21+
22+
const xdgPortalPermissionStoreSummary = `allows access to the XDG Desktop Portal PermissionStore service`
23+
24+
const xdgPortalPermissionStoreBaseDeclarationPlugs = `
25+
xdg-portal-permission-store:
26+
allow-installation: false
27+
deny-auto-connection: true
28+
`
29+
30+
const xdgPortalPermissionStoreBaseDeclarationSlots = `
31+
xdg-portal-permission-store:
32+
allow-installation:
33+
slot-snap-type:
34+
- core
35+
deny-auto-connection: true
36+
`
37+
38+
const xdgPortalPermissionStoreConnectedPlugAppArmor = `
39+
# Description: Allow access to xdg-desktop-portal's PermissionStore service.
40+
41+
#include <abstractions/dbus-session-strict>
42+
43+
dbus (receive, send)
44+
bus=session
45+
interface=org.freedesktop.impl.portal.PermissionStore
46+
path=/org/freedesktop/impl/portal/PermissionStore
47+
peer=(label=unconfined),
48+
dbus (receive, send)
49+
bus=session
50+
interface=org.freedesktop.DBus.Properties
51+
path=/org/freedesktop/impl/portal/PermissionStore
52+
peer=(label=unconfined),
53+
dbus (receive, send)
54+
bus=session
55+
interface=org.freedesktop.DBus.Peer
56+
path=/org/freedesktop/impl/portal/PermissionStore
57+
peer=(label=unconfined),
58+
dbus (receive, send)
59+
bus=session
60+
interface=org.freedesktop.DBus.Introspectable
61+
path=/org/freedesktop/impl/portal/PermissionStore
62+
peer=(label=unconfined),
63+
`
64+
65+
func init() {
66+
registerIface(&commonInterface{
67+
name: "xdg-portal-permission-store",
68+
summary: xdgPortalPermissionStoreSummary,
69+
implicitOnCore: true,
70+
implicitOnClassic: true,
71+
baseDeclarationPlugs: xdgPortalPermissionStoreBaseDeclarationPlugs,
72+
baseDeclarationSlots: xdgPortalPermissionStoreBaseDeclarationSlots,
73+
connectedPlugAppArmor: xdgPortalPermissionStoreConnectedPlugAppArmor,
74+
})
75+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// -*- Mode: Go; indent-tabs-mode: t -*-
2+
3+
/*
4+
* Copyright (C) 2026 Canonical Ltd
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 3 as
8+
* published by the Free Software Foundation.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*
18+
*/
19+
20+
package builtin_test
21+
22+
import (
23+
. "gopkg.in/check.v1"
24+
25+
"github.com/snapcore/snapd/interfaces"
26+
"github.com/snapcore/snapd/interfaces/apparmor"
27+
"github.com/snapcore/snapd/interfaces/builtin"
28+
"github.com/snapcore/snapd/snap"
29+
"github.com/snapcore/snapd/testutil"
30+
)
31+
32+
type XdgPortalPermissionStoreInterfaceSuite struct {
33+
iface interfaces.Interface
34+
slot *interfaces.ConnectedSlot
35+
slotInfo *snap.SlotInfo
36+
plug *interfaces.ConnectedPlug
37+
plugInfo *snap.PlugInfo
38+
}
39+
40+
var _ = Suite(&XdgPortalPermissionStoreInterfaceSuite{
41+
iface: builtin.MustInterface("xdg-portal-permission-store"),
42+
})
43+
44+
func (s *XdgPortalPermissionStoreInterfaceSuite) SetUpTest(c *C) {
45+
const coreYaml = `name: core
46+
version: 0
47+
type: os
48+
slots:
49+
xdg-portal-permission-store:
50+
interface: xdg-portal-permission-store
51+
`
52+
s.slot, s.slotInfo = MockConnectedSlot(c, coreYaml, nil, "xdg-portal-permission-store")
53+
54+
const consumerYaml = `name: consumer
55+
version: 0
56+
apps:
57+
app:
58+
plugs: [xdg-portal-permission-store]
59+
`
60+
s.plug, s.plugInfo = MockConnectedPlug(c, consumerYaml, nil, "xdg-portal-permission-store")
61+
}
62+
63+
func (s *XdgPortalPermissionStoreInterfaceSuite) TestName(c *C) {
64+
c.Assert(s.iface.Name(), Equals, "xdg-portal-permission-store")
65+
}
66+
67+
func (s *XdgPortalPermissionStoreInterfaceSuite) TestSanitize(c *C) {
68+
c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
69+
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
70+
}
71+
72+
func (s *XdgPortalPermissionStoreInterfaceSuite) TestAppArmorConnectedPlug(c *C) {
73+
appSet, err := interfaces.NewSnapAppSet(s.plug.Snap(), nil)
74+
c.Assert(err, IsNil)
75+
spec := apparmor.NewSpecification(appSet)
76+
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
77+
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
78+
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "#include <abstractions/dbus-session-strict>")
79+
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "path=/org/freedesktop/impl/portal/PermissionStore")
80+
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "interface=org.freedesktop.impl.portal.PermissionStore")
81+
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "interface=org.freedesktop.DBus.Properties")
82+
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "interface=org.freedesktop.DBus.Peer")
83+
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "interface=org.freedesktop.DBus.Introspectable")
84+
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(label=unconfined)")
85+
}
86+
87+
func (s *XdgPortalPermissionStoreInterfaceSuite) TestAppArmorConnectedSlot(c *C) {
88+
appSet, err := interfaces.NewSnapAppSet(s.slot.Snap(), nil)
89+
c.Assert(err, IsNil)
90+
spec := apparmor.NewSpecification(appSet)
91+
c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.slot), IsNil)
92+
c.Assert(spec.SecurityTags(), HasLen, 0)
93+
}
94+
95+
func (s *XdgPortalPermissionStoreInterfaceSuite) TestAppArmorPermanentSlot(c *C) {
96+
spec := &apparmor.Specification{}
97+
c.Assert(spec.AddPermanentSlot(s.iface, s.slotInfo), IsNil)
98+
c.Assert(spec.SecurityTags(), HasLen, 0)
99+
}
100+
101+
func (s *XdgPortalPermissionStoreInterfaceSuite) TestStaticInfo(c *C) {
102+
si := interfaces.StaticInfoOf(s.iface)
103+
c.Check(si.ImplicitOnCore, Equals, true)
104+
c.Check(si.ImplicitOnClassic, Equals, true)
105+
c.Check(si.Summary, Equals, "allows access to the XDG Desktop Portal PermissionStore service")
106+
c.Check(si.BaseDeclarationPlugs, testutil.Contains, "xdg-portal-permission-store")
107+
c.Check(si.BaseDeclarationPlugs, testutil.Contains, "allow-installation: false")
108+
c.Check(si.BaseDeclarationPlugs, testutil.Contains, "deny-auto-connection: true")
109+
c.Check(si.BaseDeclarationSlots, testutil.Contains, "xdg-portal-permission-store")
110+
c.Check(si.BaseDeclarationSlots, testutil.Contains, "deny-auto-connection: true")
111+
}
112+
113+
func (s *XdgPortalPermissionStoreInterfaceSuite) TestInterfaces(c *C) {
114+
c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
115+
}

interfaces/policy/basedeclaration_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,7 @@ func (s *baseDeclSuite) TestPlugInstallation(c *C) {
11281128
"uinput": true,
11291129
"unity8": true,
11301130
"ubuntu-pro-control": true,
1131+
"xdg-portal-permission-store": true,
11311132
"userns": true,
11321133
"xilinx-dma": true,
11331134
"snap-fde-control": true,
@@ -1452,6 +1453,7 @@ func (s *baseDeclSuite) TestValidity(c *C) {
14521453
"system-files": true,
14531454
"tee": true,
14541455
"ubuntu-pro-control": true,
1456+
"xdg-portal-permission-store": true,
14551457
"udisks2": true,
14561458
"uinput": true,
14571459
"unity8": true,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
command="${1:-all}"
5+
bus_name="${DBUS_BUS_NAME}"
6+
object_path="${DBUS_OBJECT_PATH}"
7+
iface_name="${DBUS_IFACE_NAME}"
8+
9+
cmd_ping() {
10+
# Use --print-reply so dbus-send performs a method call and waits for the
11+
# reply. We discard the reply body because for Ping we only care about the
12+
# success or failure of the round trip.
13+
dbus-send --session --print-reply \
14+
--dest="$bus_name" \
15+
"$object_path" \
16+
org.freedesktop.DBus.Peer.Ping >/dev/null
17+
echo ok
18+
}
19+
20+
cmd_introspect() {
21+
dbus-send --session --print-reply \
22+
--dest="$bus_name" \
23+
"$object_path" \
24+
org.freedesktop.DBus.Introspectable.Introspect | grep -q "$iface_name"
25+
echo ok
26+
}
27+
28+
cmd_get_all() {
29+
dbus-send --session --print-reply \
30+
--dest="$bus_name" \
31+
"$object_path" \
32+
org.freedesktop.DBus.Properties.GetAll \
33+
string:"$iface_name" | grep -q 'uint32 1'
34+
echo ok
35+
}
36+
37+
cmd_test() {
38+
dbus-send --session --print-reply \
39+
--dest="$bus_name" \
40+
"$object_path" \
41+
"$iface_name".Test | grep -q 'string "ok"'
42+
echo ok
43+
}
44+
45+
case "$command" in
46+
ping)
47+
cmd_ping
48+
;;
49+
introspect)
50+
cmd_introspect
51+
;;
52+
get-all)
53+
cmd_get_all
54+
;;
55+
test)
56+
cmd_test
57+
;;
58+
all)
59+
cmd_ping
60+
cmd_introspect
61+
cmd_get_all
62+
cmd_test
63+
;;
64+
*)
65+
echo "unknown command: $command" >&2
66+
exit 1
67+
;;
68+
esac
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: test-snapd-xdg-portal-permission-store
2+
version: 1.0
3+
summary: test client for xdg-portal-permission-store
4+
description: ...
5+
apps:
6+
client:
7+
command: bin/client.sh all
8+
plugs: &permission-store-plugs
9+
- xdg-portal-permission-store
10+
environment: &permission-store-env
11+
DBUS_BUS_NAME: org.freedesktop.impl.portal.PermissionStore
12+
DBUS_OBJECT_PATH: /org/freedesktop/impl/portal/PermissionStore
13+
DBUS_IFACE_NAME: org.freedesktop.impl.portal.PermissionStore
14+
ping:
15+
command: bin/client.sh ping
16+
plugs: *permission-store-plugs
17+
environment: *permission-store-env
18+
introspect:
19+
command: bin/client.sh introspect
20+
plugs: *permission-store-plugs
21+
environment: *permission-store-env
22+
get-all:
23+
command: bin/client.sh get-all
24+
plugs: *permission-store-plugs
25+
environment: *permission-store-env
26+
test:
27+
command: bin/client.sh test
28+
plugs: *permission-store-plugs
29+
environment: *permission-store-env

0 commit comments

Comments
 (0)