diff --git a/interfaces/builtin/systemd_user_control.go b/interfaces/builtin/systemd_user_control.go new file mode 100644 index 000000000000..80f42677ef8a --- /dev/null +++ b/interfaces/builtin/systemd_user_control.go @@ -0,0 +1,107 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public 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 + +const systemdUserControlSummary = `allows to control the user session service manager` + +const systemdUserControlBaseDeclarationPlugs = ` + systemd-user-control: + allow-installation: false + deny-auto-connection: true +` + +const systemdUserControlBaseDeclarationSlots = ` + systemd-user-control: + allow-installation: + slot-snap-type: + - core + deny-auto-connection: true +` + +const systemdUserControlConnectedPlugAppArmor = ` +# Description: Can control the user session service manager + +#include +#include + +# Supporting session boot fully driven by user session systemd +# and D-Bus activation + +# Please note that UpdateActivationEnvironment can alter D-Bus activated services behavior +# (e.g. by setting LD_PRELOAD) +# It is thus intended to be restricted only to snaps acting as a desktop session on Ubuntu Core systems +# +# For such snaps, it allows the session to pass important variables to other processes in the session +# (e.g. DISPLAY, WAYLAND_DISPLAY) +dbus (send) + bus=session + path={/,/org/freedesktop/DBus} + interface=org.freedesktop.DBus + member=UpdateActivationEnvironment + peer=(label=unconfined), + +dbus (send) + bus=session + path=/org/freedesktop/systemd1 + interface=org.freedesktop.DBus.Properties + member={Set,Get,GetAll} + peer=(label=unconfined), + +# Please note that SetEnvironment can alter existing units behavior (e.g. by setting LD_PRELOAD) +# It is thus intended to be restricted only to snaps acting as a desktop session on Ubuntu Core systems +# +# For such snaps, it allows the session to pass important variables to other processes in the session +# (e.g. DISPLAY, WAYLAND_DISPLAY) +dbus (send) + bus=session + path=/org/freedesktop/systemd1 + interface=org.freedesktop.systemd1.Manager + member={SetEnvironment,UnsetEnvironment,UnsetAndSetEnvironment} + peer=(label=unconfined), + +# Allow to introspect the units available in the session +dbus (send) + bus=session + path=/org/freedesktop/systemd1 + interface=org.freedesktop.systemd1.Manager + member={Reload,ListUnitFiles,ListUnitFilesByPatterns} + peer=(label=unconfined), + +# Allow to manage the units available in the session +# (e.g. to start the target describing the full session, phase parts of the startup) +dbus (send) + bus=session + path=/org/freedesktop/systemd1 + interface=org.freedesktop.systemd1.Manager + member={ResetFailed,Reload,StartUnit,StopUnit,RestartUnit} + peer=(label=unconfined), +` + +func init() { + registerIface(&commonInterface{ + name: "systemd-user-control", + summary: systemdUserControlSummary, + implicitOnCore: true, + implicitOnClassic: false, // This is meant for use by session snaps on core, no use for apps in classic mode + baseDeclarationPlugs: systemdUserControlBaseDeclarationPlugs, + baseDeclarationSlots: systemdUserControlBaseDeclarationSlots, + connectedPlugAppArmor: systemdUserControlConnectedPlugAppArmor, + }) +} diff --git a/interfaces/builtin/systemd_user_control_test.go b/interfaces/builtin/systemd_user_control_test.go new file mode 100644 index 000000000000..ec1cdf815d52 --- /dev/null +++ b/interfaces/builtin/systemd_user_control_test.go @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public 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 ( + . "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 SystemdUserControlInterfaceSuite struct { + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&SystemdUserControlInterfaceSuite{ + iface: builtin.MustInterface("systemd-user-control"), +}) + +func (s *SystemdUserControlInterfaceSuite) SetUpTest(c *C) { + var mockPlugSnapInfoYaml = `name: other +version: 1.0 +apps: + app: + command: foo + plugs: [systemd-user-control] +` + var mockSlotSnapInfoYaml = `name: core +version: 0 +type: os +slots: + systemd-user-control: +` + s.plug, s.plugInfo = MockConnectedPlug(c, mockPlugSnapInfoYaml, nil, "systemd-user-control") + s.slot, s.slotInfo = MockConnectedSlot(c, mockSlotSnapInfoYaml, nil, "systemd-user-control") +} + +func (s *SystemdUserControlInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "systemd-user-control") +} + +func (s *SystemdUserControlInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) +} + +func (s *SystemdUserControlInterfaceSuite) TestSanitizePlug(c *C) { + c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) +} + +func (s *SystemdUserControlInterfaceSuite) TestUsedSecuritySystems(c *C) { + // connected plugs have a non-nil security snippet for apparmor + appSet, err := interfaces.NewSnapAppSet(s.plug.Snap(), nil) + c.Assert(err, IsNil) + apparmorSpec := apparmor.NewSpecification(appSet) + err = apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) + c.Assert(err, IsNil) + c.Assert(apparmorSpec.Snippets(), HasLen, 1) + c.Assert(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, "bus=session") + c.Assert(apparmorSpec.SnippetForTag("snap.other.app"), Not(testutil.Contains), "bus=system") + c.Assert(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, "UpdateActivationEnvironment") +} + +func (s *SystemdUserControlInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go index efa0862993a5..2ad3b4829485 100644 --- a/interfaces/policy/basedeclaration_test.go +++ b/interfaces/policy/basedeclaration_test.go @@ -143,19 +143,20 @@ func (s *baseDeclSuite) TestAutoConnection(c *C) { // these have more complex or in flux policies and have their // own separate tests snowflakes := map[string]bool{ - "content": true, - "core-support": true, - "desktop": true, - "home": true, - "lxd-support": true, - "microstack-support": true, - "multipass-support": true, - "packagekit-control": true, - "pkcs11": true, - "remoteproc": true, - "snapd-control": true, - "upower-observe": true, - "empty": true, + "content": true, + "core-support": true, + "desktop": true, + "home": true, + "lxd-support": true, + "microstack-support": true, + "multipass-support": true, + "packagekit-control": true, + "pkcs11": true, + "remoteproc": true, + "snapd-control": true, + "systemd-user-control": true, + "upower-observe": true, + "empty": true, } // these simply auto-connect, anything else doesn't @@ -333,6 +334,25 @@ plugs: c.Check(arity.SlotsPerPlugAny(), Equals, false) } +func (s *baseDeclSuite) TestAutoConnectionSystemdUserControl(c *C) { + cand := s.connectCand(c, "systemd-user-control", "", "") + _, err := cand.CheckAutoConnect() + c.Check(err, NotNil) + c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"systemd-user-control\"") + + plugsSlots := ` +plugs: + systemd-user-control: + allow-auto-connection: true +` + + lxdDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots) + cand.PlugSnapDeclaration = lxdDecl + arity, err := cand.CheckAutoConnect() + c.Check(err, IsNil) + c.Check(arity.SlotsPerPlugAny(), Equals, false) +} + func (s *baseDeclSuite) TestAutoConnectionContent(c *C) { // random snaps cannot connect with content // (Sanitize* will now also block this) @@ -1040,6 +1060,7 @@ func (s *baseDeclSuite) TestPlugInstallation(c *C) { "snapd-control": true, "steam-support": true, "system-files": true, + "systemd-user-control": true, "tee": true, "uinput": true, "unity8": true, @@ -1341,6 +1362,7 @@ func (s *baseDeclSuite) TestValidity(c *C) { "snapd-control": true, "steam-support": true, "system-files": true, + "systemd-user-control": true, "tee": true, "udisks2": true, "uinput": true, 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 a808e59b43df..ef53cb65c59a 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 @@ -290,6 +290,9 @@ apps: xilinx-dma: command: bin/run plugs: [ xilinx-dma ] + systemd-user-control: + command: bin/run + plugs: [ systemd-user-control ] plugs: custom-device: