Skip to content

Commit

Permalink
Add snippets with priorities
Browse files Browse the repository at this point in the history
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 (
canonical#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.
  • Loading branch information
sergio-costas committed Jun 11, 2024
1 parent 23f572b commit 912eac8
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 2 deletions.
73 changes: 71 additions & 2 deletions interfaces/apparmor/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ const (
UnconfinedEnabled
)

type prioritizedSnippetsType struct {
priority uint
snippets []string
}

// Specification assists in collecting apparmor entries associated with an interface.
type Specification struct {
// appSet is the set of snap applications and hooks that the specification
Expand All @@ -56,6 +61,15 @@ type Specification struct {
// of the application or hook.
snippets map[string][]string

// prioritizedSnippets are just like snippets, but they have a priority value
// and an UID. An interface can add a snippet with a specific UID and a priority.
// If there doesn't exist any snippet with that UID, the passed snippet will be
// added as-is. But if it does exist, the new snippet will replace the old one if
// the new priority is bigger than the old one; will be appended if the new
// priority is the same than the old one, and will be discarded if the new priority
// is smaller than the old one.
prioritizedSnippets map[string]map[string]prioritizedSnippetsType

// dedupSnippets are just like snippets but are added only once to the
// resulting policy in an effort to avoid certain expensive to de-duplicate
// rules by apparmor_parser.
Expand Down Expand Up @@ -146,6 +160,55 @@ func (spec *Specification) AddSnippet(snippet string) {
}
}

// AddPrioritizedSnippet adds a new apparmor snippet to all applications and hooks using the interface,
// but identified with an UID and a priority. If no other snippet exists with that UID, the snippet is
// added like with AddSnippet, but if there is already another snippet with that UID, the priority of
// both will be taken into account to decide whether the new snippet replaces the old one, is appended
// to it, or is just ignored.
func (spec *Specification) AddPrioritizedSnippet(snippet string, uid string, priority uint) {
if len(spec.securityTags) == 0 {
return
}
if spec.prioritizedSnippets == nil {
spec.prioritizedSnippets = make(map[string]map[string]prioritizedSnippetsType)
}
for _, tag := range spec.securityTags {
if _, exists := spec.prioritizedSnippets[tag]; !exists {
spec.prioritizedSnippets[tag] = make(map[string]prioritizedSnippetsType)
}
if snippets, exists := spec.prioritizedSnippets[tag][uid]; exists {
if snippets.priority == priority {
// if priority is the same, append the snippet to the already existing snippets
spec.prioritizedSnippets[tag][uid] = prioritizedSnippetsType{
priority: priority,
snippets: append(snippets.snippets, snippet),
}
} else if snippets.priority < priority {
// if priority is greater, replace the snippets with the new one
spec.prioritizedSnippets[tag][uid] = prioritizedSnippetsType{
priority: priority,
snippets: append([]string(nil), snippet),
}
} // if priority is smaller, do nothing
} else {
spec.prioritizedSnippets[tag][uid] = prioritizedSnippetsType{
priority: priority,
snippets: append([]string(nil), snippet),
}
}
}
}

func (spec *Specification) composeSnippetsForTag(tag string) []string {
// Compose the normal and the prioritized snippets in a single string array
composedSnippets := append([]string(nil), spec.snippets[tag]...)
for uid := range spec.prioritizedSnippets[tag] {
composedSnippets = append(composedSnippets, spec.prioritizedSnippets[tag][uid].snippets...)
}
sort.Strings(composedSnippets)
return composedSnippets
}

// AddDeduplicatedSnippet adds a new apparmor snippet to all applications and hooks using the interface.
//
// Certain combinations of snippets may be computationally expensive for
Expand Down Expand Up @@ -529,11 +592,17 @@ func (spec *Specification) SnippetForTag(tag string) string {
// SecurityTags returns a list of security tags which have a snippet.
func (spec *Specification) SecurityTags() []string {
var tags []string
seen := make(map[string]bool, len(spec.snippets))
seen := make(map[string]bool)
for t := range spec.snippets {
tags = append(tags, t)
seen[t] = true
}
for t := range spec.prioritizedSnippets {
if !seen[t] {
tags = append(tags, t)
seen[t] = true
}
}
for t := range spec.dedupSnippets {
if !seen[t] {
tags = append(tags, t)
Expand All @@ -549,7 +618,7 @@ func (spec *Specification) SecurityTags() []string {
}

func (spec *Specification) snippetsForTag(tag string) []string {
snippets := append([]string(nil), spec.snippets[tag]...)
snippets := append([]string(nil), spec.composeSnippetsForTag(tag)...)
// First add any deduplicated snippets
if bag := spec.dedupSnippets[tag]; bag != nil {
snippets = append(snippets, bag.Items()...)
Expand Down
44 changes: 44 additions & 0 deletions interfaces/apparmor/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package apparmor_test

import (
"slices"
"strings"

. "gopkg.in/check.v1"
Expand Down Expand Up @@ -615,3 +616,46 @@ func (s *specSuite) TestSetSuppressPycacheDeny(c *C) {
s.spec.SetSuppressPycacheDeny()
c.Assert(s.spec.SuppressPycacheDeny(), Equals, true)
}

func (s *specSuite) TestPrioritySnippets(c *C) {
restore := apparmor.SetSpecScope(s.spec, []string{"snap.demo.scope1"})

// Test a scope with a normal snippet and prioritized ones
s.spec.AddSnippet("Test snippet 1")
s.spec.AddPrioritizedSnippet("Prioritized snippet 1", "uid1", 0)
s.spec.AddPrioritizedSnippet("Prioritized snippet 2", "uid1", 0)
s.spec.AddPrioritizedSnippet("Prioritized snippet 3", "uid2", 1)
s.spec.AddPrioritizedSnippet("Prioritized snippet 4", "uid2", 2)
s.spec.AddPrioritizedSnippet("Prioritized snippet 5", "uid2", 0)

restore()

// Test a scope with only prioritized snippets
restore = apparmor.SetSpecScope(s.spec, []string{"snap.demo.scope2"})
defer restore()

s.spec.AddPrioritizedSnippet("Prioritized snippet 6", "uid1", 0)
s.spec.AddPrioritizedSnippet("Prioritized snippet 7", "uid1", 0)
s.spec.AddPrioritizedSnippet("Prioritized snippet 8", "uid2", 1)
s.spec.AddPrioritizedSnippet("Prioritized snippet 9", "uid2", 2)
s.spec.AddPrioritizedSnippet("Prioritized snippet 10", "uid2", 0)

snippets := s.spec.SnippetForTag("snap.demo.scope1")
c.Assert(snippets, testutil.Contains, "Test snippet 1")
c.Assert(snippets, testutil.Contains, "Prioritized snippet 1")
c.Assert(snippets, testutil.Contains, "Prioritized snippet 2")
c.Assert(snippets, Not(testutil.Contains), "Prioritized snippet 3")
c.Assert(snippets, testutil.Contains, "Prioritized snippet 4")
c.Assert(snippets, Not(testutil.Contains), "Prioritized snippet 5")

snippets = s.spec.SnippetForTag("snap.demo.scope2")
c.Assert(snippets, testutil.Contains, "Prioritized snippet 6")
c.Assert(snippets, testutil.Contains, "Prioritized snippet 7")
c.Assert(snippets, Not(testutil.Contains), "Prioritized snippet 8")
c.Assert(snippets, testutil.Contains, "Prioritized snippet 9")
c.Assert(snippets, Not(testutil.Contains), "Prioritized snippet 10")

tags := s.spec.SecurityTags()
c.Assert(slices.Contains(tags, "snap.demo.scope1"), Equals, true)
c.Assert(slices.Contains(tags, "snap.demo.scope2"), Equals, true)
}

0 comments on commit 912eac8

Please sign in to comment.