diff --git a/interfaces/builtin/desktop_legacy.go b/interfaces/builtin/desktop_legacy.go index 724c91c365..6803ad611d 100644 --- a/interfaces/builtin/desktop_legacy.go +++ b/interfaces/builtin/desktop_legacy.go @@ -20,6 +20,8 @@ package builtin import ( + "strings" + "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/apparmor" ) @@ -43,13 +45,6 @@ const desktopLegacyConnectedPlugAppArmor = ` # accessibility (a11y) #include -dbus (send) - bus=session - path=/org/a11y/bus - interface=org.a11y.Bus - member=GetAddress - peer=(label=unconfined), - #include # Allow access to the non-abstract D-Bus socket used by at-spi > 2.42.0 @@ -59,72 +54,6 @@ owner /{,var/}run/user/[0-9]*/at-spi/bus* rw, # Allow access to the socket used by speech-dispatcher owner /{,var/}run/user/[0-9]*/speech-dispatcher/speechd.sock rw, -# Allow the accessibility services in the user session to send us any events -dbus (receive) - bus=accessibility - peer=(label=unconfined), - -# Allow querying for capabilities and registering -dbus (send) - bus=accessibility - path="/org/a11y/atspi/accessible/root" - interface="org.a11y.atspi.Socket" - member="Embed" - peer=(name=org.a11y.atspi.Registry, label=unconfined), -dbus (send) - bus=accessibility - path="/org/a11y/atspi/registry" - interface="org.a11y.atspi.Registry" - member="GetRegisteredEvents" - peer=(name=org.a11y.atspi.Registry, label=unconfined), -dbus (send) - bus=accessibility - path="/org/a11y/atspi/registry/deviceeventcontroller" - interface="org.a11y.atspi.DeviceEventController" - member="Get{DeviceEvent,Keystroke}Listeners" - peer=(name=org.a11y.atspi.Registry, label=unconfined), -dbus (send) - bus=accessibility - path="/org/a11y/atspi/registry/deviceeventcontroller" - interface="org.a11y.atspi.DeviceEventController" - member="NotifyListenersSync" - peer=(name=org.a11y.atspi.Registry, label=unconfined), - -# org.a11y.atspi is not designed for application isolation and these rules -# can be used to send change events for other processes. -dbus (send) - bus=accessibility - path="/org/a11y/atspi/accessible/root" - interface="org.a11y.atspi.Event.Object" - member="ChildrenChanged" - peer=(name=org.freedesktop.DBus, label=unconfined), -dbus (send) - bus=accessibility - path="/org/a11y/atspi/accessible/root" - interface="org.a11y.atspi.Accessible" - member="Get*" - peer=(label=unconfined), -dbus (send) - bus=accessibility - path="/org/a11y/atspi/accessible/[0-9]*" - interface="org.a11y.atspi.Event.Object" - member="{ChildrenChanged,PropertyChange,StateChanged,TextCaretMoved}" - peer=(name=org.freedesktop.DBus, label=unconfined), -dbus (send) - bus=accessibility - path="/org/a11y/atspi/accessible/[0-9]*" - interface="org.freedesktop.DBus.Properties" - member="Get{,All}" - peer=(label=unconfined), - -dbus (send) - bus=accessibility - path="/org/a11y/atspi/cache" - interface="org.a11y.atspi.Cache" - member="{Add,Remove}Accessible" - peer=(name=org.freedesktop.DBus, label=unconfined), - - # ibus # subset of ibus abstraction /usr/lib/@{multiarch}/gtk-2.0/[0-9]*/immodules/im-ibus.so mr, @@ -404,6 +333,84 @@ dbus (send) peer=(name=org.freedesktop.DBus, label="{plasmashell,unconfined}"), ` +const accessibilityRules = ` +dbus (send) + bus=session + path=/org/a11y/bus + interface=org.a11y.Bus + member=GetAddress + peer=(label=###ACCESSIBILITY-LABEL###), + +# Allow the accessibility services in the user session to send us any events +dbus (receive) + bus=accessibility + peer=(label=###ACCESSIBILITY-LABEL###), + +# Allow querying for capabilities and registering +dbus (send) + bus=accessibility + path="/org/a11y/atspi/accessible/root" + interface="org.a11y.atspi.Socket" + member="Embed" + peer=(name=org.a11y.atspi.Registry, label=###ACCESSIBILITY-LABEL###), +dbus (send) + bus=accessibility + path="/org/a11y/atspi/registry" + interface="org.a11y.atspi.Registry" + member="GetRegisteredEvents" + peer=(name=org.a11y.atspi.Registry, label=###ACCESSIBILITY-LABEL###), +dbus (send) + bus=accessibility + path="/org/a11y/atspi/registry/deviceeventcontroller" + interface="org.a11y.atspi.DeviceEventController" + member="Get{DeviceEvent,Keystroke}Listeners" + peer=(name=org.a11y.atspi.Registry, label=###ACCESSIBILITY-LABEL###), +dbus (send) + bus=accessibility + path="/org/a11y/atspi/registry/deviceeventcontroller" + interface="org.a11y.atspi.DeviceEventController" + member="NotifyListenersSync" + peer=(name=org.a11y.atspi.Registry, label=###ACCESSIBILITY-LABEL###), + +# org.a11y.atspi is not designed for application isolation and these rules +# can be used to send change events for other processes. +dbus (send) + bus=accessibility + path="/org/a11y/atspi/accessible/root" + interface="org.a11y.atspi.Event.Object" + member="ChildrenChanged" + peer=(name=org.freedesktop.DBus, label=###ACCESSIBILITY-LABEL###), +dbus (send) + bus=accessibility + path="/org/a11y/atspi/accessible/root" + interface="org.a11y.atspi.Accessible" + member="Get*" + peer=(label=###ACCESSIBILITY-LABEL###), + +# For some reason, this path must be removed to allow snapped applications to work +# with snapped Orca, but not for unconfined Orca. +# path="/org/a11y/atspi/accessible/root" +dbus (send) + bus=accessibility + interface="org.a11y.atspi.Event.Object" + member="{ChildrenChanged,PropertyChange,StateChanged,TextCaretMoved}" + peer=(name=org.freedesktop.DBus, label=###ACCESSIBILITY-LABEL###), + +dbus (send) + bus=accessibility + path="/org/a11y/atspi/accessible/[0-9]*" + interface="org.freedesktop.DBus.Properties" + member="Get{,All}" + peer=(label=###ACCESSIBILITY-LABEL###), + +dbus (send) + bus=accessibility + path="/org/a11y/atspi/cache" + interface="org.a11y.atspi.Cache" + member="{Add,Remove}Accessible" + peer=(name=org.freedesktop.DBus, label=###ACCESSIBILITY-LABEL###), +` + const desktopLegacyConnectedPlugSecComp = ` # Description: Can access common desktop legacy methods. This gives privileged # access to the user's input. @@ -419,6 +426,9 @@ type desktopLegacyInterface struct { func (iface *desktopLegacyInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { spec.AddSnippet(desktopLegacyConnectedPlugAppArmor) + // Add accessibility rules both for unconfined and snap.orca.* labels, since the accessibility service may run under either label. + spec.AddSnippet(strings.Replace(accessibilityRules, "###ACCESSIBILITY-LABEL###", "unconfined", -1)) + spec.AddSnippet(strings.Replace(accessibilityRules, "###ACCESSIBILITY-LABEL###", "snap.orca.*", -1)) // the DesktopFileRules can conflict with the rules in other, more privileged, // interfaces (like desktop-launch), so they are added here with the minimum diff --git a/interfaces/builtin/orca_screen_reader.go b/interfaces/builtin/orca_screen_reader.go new file mode 100644 index 0000000000..60eb061d8f --- /dev/null +++ b/interfaces/builtin/orca_screen_reader.go @@ -0,0 +1,91 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2026 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +import ( + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/snap" +) + +const orcaScreenReaderSummary = `special permissions for Orca screen reader` + +const orcaScreenReaderBaseDeclarationSlots = ` + orca-screen-reader: + allow-installation: + slot-snap-type: + - core + deny-connection: + on-classic: false + deny-auto-connection: true +` + +const orcaScreenReaderPlugAppArmor = ` +#include +#include + +network netlink, + +# full access to the accessibility bus +dbus (send, receive) + bus=accessibility, + +# full access to a11y elements in session bus +dbus (send, receive) + bus=session + path=/org/a11y/bus{,/**}, + +dbus (bind) + bus=session + name="org.gnome.Orca.Service", + +# allow access to the at-spi folder and +# the at-spi1-XXXXX folders +/run/user/[0-9]*/at-spi{,2-[0-9A-Z]*}/ rw, +/run/user/[0-9]*/at-spi{,2-[0-9A-Z]*}/** rwk, +` + +type orcaScreenReaderInterface struct{} + +func (iface *orcaScreenReaderInterface) Name() string { + return "orca-screen-reader" +} + +func (iface *orcaScreenReaderInterface) StaticInfo() interfaces.StaticInfo { + return interfaces.StaticInfo{ + Summary: orcaScreenReaderSummary, + ImplicitOnClassic: true, + BaseDeclarationSlots: orcaScreenReaderBaseDeclarationSlots, + } +} + +func (iface *orcaScreenReaderInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + spec.AddSnippet(orcaScreenReaderPlugAppArmor) + return nil +} + +func (iface *orcaScreenReaderInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool { + // allow what declarations allowed + return true +} + +func init() { + registerIface(&orcaScreenReaderInterface{}) +} diff --git a/interfaces/builtin/orca_screen_reader_test.go b/interfaces/builtin/orca_screen_reader_test.go new file mode 100644 index 0000000000..5515a99fcb --- /dev/null +++ b/interfaces/builtin/orca_screen_reader_test.go @@ -0,0 +1,117 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2026 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type AccessibilityLegacyInterfaceSuite struct { + iface interfaces.Interface + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +const accessibilityLegacyConsumerYaml = `name: consumer +version: 0 +apps: + app: + plugs: [orca-screen-reader] +` + +var _ = Suite(&AccessibilityLegacyInterfaceSuite{ + iface: builtin.MustInterface("orca-screen-reader"), +}) + +func (s *AccessibilityLegacyInterfaceSuite) SetUpTest(c *C) { + s.plug, s.plugInfo = MockConnectedPlug(c, accessibilityLegacyConsumerYaml, nil, "orca-screen-reader") +} + +func (s *AccessibilityLegacyInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "orca-screen-reader") +} + +// check if a string is part of any of the strings in a slice +func checkDBusRules(dbus_rules []string, matches []string) bool { + for _, rule := range dbus_rules { + found := 0 + for _, match := range matches { + if strings.HasSuffix(match, ",") { + match = strings.TrimSuffix(match, ",") + } + if strings.Contains(rule, match+"\n") { + found++ + } else { + found = -1 + break + } + } + if found == len(matches) { + return true + } + } + return false +} + +func getDBusRules(spec *apparmor.Specification) []string { + dbus_rules := strings.Split(spec.SnippetForTag("snap.consumer.app"), "dbus ") + out := []string{} + for _, rule := range dbus_rules { + out = append(out, strings.Split(rule, ",\n")[0]+"\n") + } + return out +} + +func (s *AccessibilityLegacyInterfaceSuite) TestAppArmorSpecForConnectedPlug(c *C) { + // connected plug to core slot + appSet, err := interfaces.NewSnapAppSet(s.plug.Snap(), nil) + c.Assert(err, IsNil) + spec := apparmor.NewSpecification(appSet) + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, nil), IsNil) + + dbus_rules := getDBusRules(spec) + c.Assert(checkDBusRules(dbus_rules, []string{"(send, receive)", "bus=accessibility"}), Equals, true) + c.Assert(checkDBusRules(dbus_rules, []string{"(send, receive)", "bus=session", "path=/org/a11y/bus{,/**}"}), Equals, true) + c.Assert(checkDBusRules(dbus_rules, []string{"(bind)", "bus=session", "name=\"org.gnome.Orca.Service\""}), Equals, true) + + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "#include ") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/run/user/[0-9]*/at-spi{,2-[0-9A-Z]*}/ rw,") +} + +func (s *AccessibilityLegacyInterfaceSuite) TestAppArmorSpecForNotConnectedPlug(c *C) { + appSet, err := interfaces.NewSnapAppSet(s.plug.Snap(), nil) + c.Assert(err, IsNil) + spec := apparmor.NewSpecification(appSet) + dbus_rules := getDBusRules(spec) + c.Assert(checkDBusRules(dbus_rules, []string{"(send, receive)", "bus=accessibility"}), Equals, false) + c.Assert(checkDBusRules(dbus_rules, []string{"(send, receive)", "bus=session", "path=/org/a11y/bus{,/**}"}), Equals, false) + c.Assert(checkDBusRules(dbus_rules, []string{"(bind)", "bus=session", "name=\"org.gnome.Orca.Service\""}), Equals, false) + + c.Assert(spec.SnippetForTag("snap.consumer.app"), Not(testutil.Contains), "#include ") + c.Assert(spec.SnippetForTag("snap.consumer.app"), Not(testutil.Contains), "/run/user/[0-9]*/at-spi{,2-[0-9A-Z]*}/ rw,") +} diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go index 46d853cd19..0f423d9d4d 100644 --- a/interfaces/policy/basedeclaration_test.go +++ b/interfaces/policy/basedeclaration_test.go @@ -854,6 +854,7 @@ var ( "nvme-control": {"core"}, "ofono": {"app", "core"}, "online-accounts-service": {"app"}, + "orca-screen-reader": {"core"}, "power-control": {"core"}, "ppp": {"core"}, "polkit-agent": {"core"}, diff --git a/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml b/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml index a51baf39f6..b9891cb472 100644 --- a/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml +++ b/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml @@ -191,6 +191,9 @@ apps: optical-drive: command: bin/run plugs: [ optical-drive ] + orca-screen-reader: + command: bin/run + plugs: [ orca-screen-reader ] pcscd: command: bin/run plugs: [ pcscd ]