Files
snapd/interfaces/builtin/utils_test.go
Sergio Costas c59a5f6e87 i/apparmor: add snippets with priorities (#14061)
* Add snippets with priorities

AppArmor rules that forbid access to a resource have more
priority than rules that allow access to those same resources.
This means that if an interface restricts access to an specific
resource, it won't be possible to enable access to that same
resource from another, more privileged, interface.

An example of this is reading the .desktop files of all the
installed snaps in the system: the superprivileged interface
'desktop-launch' enables access to these files, so any snap
that has a connected plug for this interface should be able
to read them. Unfortunately, the 'desktop-legacy' interface
explicitly denies access to these files, and since it is
connected automatically if a snap uses the 'desktop' or the
'unity7' interfaces, this mean that no graphical application
will be able to read the .desktop files, even if the super-
privileged interface 'desktop-launch' interface is connected.

To allow this specific case, a temporary patch (
https://github.com/snapcore/snapd/pull/13933) was created and
merged, but it is clearly an ugly and not-generic solution.
For this reason, this new patch was created, following the
specification https://docs.google.com/document/d/1K-1MYhp1RKSW_jzuuyX7TSVCg2rYplKZFdJbZAupP4Y/edit

This patch allows to add "prioritized snippets". Each one has
an UID and a priority. If no prioritized snippet with the same
UID has been previously added, the new prioritized snippet will
be added like any other normal snippet. But if there is already
an added snippet with the same UID, then the priority of both
the old and the new snippets are compared. If the new priority
is lower than the old one, the new snippet is ignored; if the
new priority is bigger than the old one, the new snippet fully
replaces the old one. Finally, if both priorities are the same,
the new snippet will be appended to the old snippet.

This generic mechanism allows to give an interface priority
over others if needed, like in the previous case.

* Remove slices.Contains, since seems to be not supported

* Update interfaces/apparmor/spec.go

Co-authored-by: Zygmunt Bazyli Krynicki <me@zygoon.pl>

* Use testutils.Contains

* Replace "uid" with "key" for clarity and sanity

* Add specific type for priority keys and force registering them

* Remove unneeded return

* Use SnippetKey as type

* Don't use "slice" since MacOS seems to not support it

* Update interfaces/apparmor/spec.go

Co-authored-by: Zygmunt Bazyli Krynicki <me@zygoon.pl>

* Update interfaces/apparmor/spec.go

Co-authored-by: Zygmunt Bazyli Krynicki <me@zygoon.pl>

* Use String instead of GetValue

* Use SnippetKey as key instead of the inner string

* Update interfaces/connection.go

Co-authored-by: Zygmunt Bazyli Krynicki <me@zygoon.pl>

* Several changes requested

* Create the SnippetKeys inside Spec

* Move key registration outside Spec

This creates a centralized key registry inside apparmor module,
so keys can be registered using top variables, and any
duplicated key will produce a panic when snapd is launched,
thus just panicking in any test too.

* Added extra ways of working with SnippetKeys

* Add extra check

* Replace GetSnippetKey with GetSnippetKeys

* Update the priority code use case

A previous PR was merged with a Quick&Dirty(tm) solution to the
priority problem between unity7 and desktop-legacy interfaces
against desktop-launch interface.

Now that it has been merged, that code must be updated to the
new mechanism implemented in this PR. This is exactly what this
commit does.

* Add explanation and constants for prioritized snippets

* Fix prioritized snippet key and add test in all_test

* Several changes requested by Zygmunt Vazyli

---------

Co-authored-by: Zygmunt Bazyli Krynicki <me@zygoon.pl>
2024-07-08 22:27:44 +02:00

264 lines
7.1 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 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 <http://www.gnu.org/licenses/>.
*
*/
package builtin_test
import (
"fmt"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/builtin"
"github.com/snapcore/snapd/interfaces/ifacetest"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
)
type utilsSuite struct {
iface interfaces.Interface
slotOS *snap.SlotInfo
slotApp *snap.SlotInfo
slotSnapd *snap.SlotInfo
slotGadget *snap.SlotInfo
conSlotOS *interfaces.ConnectedSlot
conSlotSnapd *interfaces.ConnectedSlot
conSlotApp *interfaces.ConnectedSlot
}
func connectedSlotFromInfo(info *snap.Info) *interfaces.ConnectedSlot {
appSet, err := interfaces.NewSnapAppSet(info, nil)
if err != nil {
panic(fmt.Sprintf("cannot create snap app set: %v", err))
}
return interfaces.NewConnectedSlot(&snap.SlotInfo{Snap: info}, appSet, nil, nil)
}
var _ = Suite(&utilsSuite{
iface: &ifacetest.TestInterface{InterfaceName: "iface"},
slotOS: &snap.SlotInfo{Snap: &snap.Info{SnapType: snap.TypeOS}},
slotApp: &snap.SlotInfo{Snap: &snap.Info{SnapType: snap.TypeApp}},
slotSnapd: &snap.SlotInfo{Snap: &snap.Info{SnapType: snap.TypeSnapd, SuggestedName: "snapd"}},
slotGadget: &snap.SlotInfo{Snap: &snap.Info{SnapType: snap.TypeGadget}},
conSlotOS: connectedSlotFromInfo(&snap.Info{SnapType: snap.TypeOS, SuggestedName: "core"}),
conSlotSnapd: connectedSlotFromInfo(&snap.Info{SnapType: snap.TypeSnapd, SuggestedName: "snapd"}),
conSlotApp: connectedSlotFromInfo(&snap.Info{SnapType: snap.TypeApp, SuggestedName: "app"}),
})
func (s *utilsSuite) TestIsSlotSystemSlot(c *C) {
c.Assert(builtin.ImplicitSystemPermanentSlot(s.slotApp), Equals, false)
c.Assert(builtin.ImplicitSystemPermanentSlot(s.slotOS), Equals, true)
c.Assert(builtin.ImplicitSystemPermanentSlot(s.slotSnapd), Equals, true)
}
func (s *utilsSuite) TestImplicitSystemConnectedSlot(c *C) {
c.Assert(builtin.ImplicitSystemConnectedSlot(s.conSlotApp), Equals, false)
c.Assert(builtin.ImplicitSystemConnectedSlot(s.conSlotOS), Equals, true)
c.Assert(builtin.ImplicitSystemConnectedSlot(s.conSlotSnapd), Equals, true)
}
func (s *utilsSuite) TestAareExclusivePatterns(c *C) {
res := builtin.AareExclusivePatterns("foo-bar")
c.Check(res, DeepEquals, []string{
"[^f]*",
"f[^o]*",
"fo[^o]*",
"foo[^-]*",
"foo-[^b]*",
"foo-b[^a]*",
"foo-ba[^r]*",
})
}
func (s *utilsSuite) TestAareExclusivePatternsInstance(c *C) {
res := builtin.AareExclusivePatterns("foo-bar+baz")
c.Check(res, DeepEquals, []string{
"[^f]*",
"f[^o]*",
"fo[^o]*",
"foo[^-]*",
"foo-[^b]*",
"foo-b[^a]*",
"foo-ba[^r]*",
"foo-bar[^+]*",
"foo-bar+[^b]*",
"foo-bar+b[^a]*",
"foo-bar+ba[^z]*",
})
}
func (s *utilsSuite) TestAareExclusivePatternsInvalid(c *C) {
bad := []string{
// AARE in name (man apparmor.d: AARE = ?*[]{}^)
"bad{",
"ba}d",
"b[ad",
"]bad",
"b^d",
"b*d",
"b?d",
"bad{+good",
"ba}d+good",
"b[ad+good",
"]bad+good",
"b^d+good",
"b*d+good",
"b?d+good",
// AARE in instance (man apparmor.d: AARE = ?*[]{}^)
"good+bad{",
"good+ba}d",
"good+b[ad",
"good+]bad",
"good+b^d",
"good+b*d",
"good+b?d",
// various other unexpected in name
"+good",
"/bad",
"bad,",
".bad.",
"ba'd",
"b\"ad",
"=bad",
"b\\0d",
"b\ad",
"(bad",
"bad)",
"b<ad",
"b>ad",
"bad!",
"b#d",
":bad",
"b@d",
"@{BAD}",
"b**d",
"bad -> evil",
"b a d",
// various other unexpected in instance
"good+",
"good+/bad",
"good+bad,",
"good+.bad.",
"good+ba'd",
"good+b\"ad",
"good+=bad",
"good+b\\0d",
"good+b\ad",
"good+(bad",
"good+bad)",
"good+b<ad",
"good+b>ad",
"good+bad!",
"good+b#d",
"good+:bad",
"good+b@d",
"good+@{BAD}",
"good+b**d",
"good+bad -> evil",
}
for _, s := range bad {
res := builtin.AareExclusivePatterns(s)
c.Check(res, IsNil)
}
}
func MockPlug(c *C, yaml string, si *snap.SideInfo, plugName string) *snap.PlugInfo {
return builtin.MockPlug(c, yaml, si, plugName)
}
func MockSlot(c *C, yaml string, si *snap.SideInfo, slotName string) *snap.SlotInfo {
return builtin.MockSlot(c, yaml, si, slotName)
}
func MockConnectedPlug(c *C, yaml string, si *snap.SideInfo, plugName string) (*interfaces.ConnectedPlug, *snap.PlugInfo) {
info := snaptest.MockInfo(c, yaml, si)
set, err := interfaces.NewSnapAppSet(info, nil)
c.Assert(err, IsNil)
if plugInfo, ok := info.Plugs[plugName]; ok {
return interfaces.NewConnectedPlug(plugInfo, set, nil, nil), plugInfo
}
panic(fmt.Sprintf("cannot find plug %q in snap %q", plugName, info.InstanceName()))
}
func MockConnectedSlot(c *C, yaml string, si *snap.SideInfo, slotName string) (*interfaces.ConnectedSlot, *snap.SlotInfo) {
info := snaptest.MockInfo(c, yaml, si)
set, err := interfaces.NewSnapAppSet(info, nil)
c.Assert(err, IsNil)
if slotInfo, ok := info.Slots[slotName]; ok {
return interfaces.NewConnectedSlot(slotInfo, set, nil, nil), slotInfo
}
panic(fmt.Sprintf("cannot find slot %q in snap %q", slotName, info.InstanceName()))
}
func MockHotplugSlot(c *C, yaml string, si *snap.SideInfo, hotplugKey snap.HotplugKey, ifaceName, slotName string, staticAttrs map[string]interface{}) *snap.SlotInfo {
info := snaptest.MockInfo(c, yaml, si)
if _, ok := info.Slots[slotName]; ok {
panic(fmt.Sprintf("slot %q already present in the snap yaml", slotName))
}
return &snap.SlotInfo{
Snap: info,
Name: slotName,
Attrs: staticAttrs,
HotplugKey: hotplugKey,
}
}
func (s *utilsSuite) TestStringListAttributeHappy(c *C) {
const snapYaml = `name: consumer
version: 0
plugs:
personal-files:
write: ["$HOME/dir1", "/etc/.hidden2"]
slots:
shared-memory:
write: ["foo", "bar"]
`
plug, _ := MockConnectedPlug(c, snapYaml, nil, "personal-files")
slot, _ := MockConnectedSlot(c, snapYaml, nil, "shared-memory")
list, err := builtin.StringListAttribute(plug, "write")
c.Assert(err, IsNil)
c.Check(list, DeepEquals, []string{"$HOME/dir1", "/etc/.hidden2"})
list, err = builtin.StringListAttribute(plug, "read")
c.Assert(err, IsNil)
c.Check(list, IsNil)
list, err = builtin.StringListAttribute(slot, "write")
c.Assert(err, IsNil)
c.Check(list, DeepEquals, []string{"foo", "bar"})
}
func (s *utilsSuite) TestStringListAttributeErrorNotListStrings(c *C) {
const snapYaml = `name: consumer
version: 0
plugs:
personal-files:
write: [1, "two"]
`
plug, _ := MockConnectedPlug(c, snapYaml, nil, "personal-files")
list, err := builtin.StringListAttribute(plug, "write")
c.Assert(list, IsNil)
c.Assert(err, ErrorMatches, `"write" attribute must be a list of strings, not "\[1 two\]"`)
}