Skip to content

Commit

Permalink
many: generate security profiles from component hooks (#13686)
Browse files Browse the repository at this point in the history
* interfaces: add method to SnapAppSet for getting list of runnable entities

* interfaces: replace references to hooks/apps in backends with usage of SnapAppSet.Runnables

* interfaces: rename SecurityTagGlob to SecurityTagGlobs and make it handle component hooks

* snap: add method for getting component hooks for a specific plug

* interfaces: add component hooks to output of SecurityTagsForConnectedPlug

* snapstate: add methods for getting components installed for the current and arbitrary revisions of a snap

* o/ifacestate: properly set up SnapAppSets with components prior to passing them off to security backends

* o/snapstate: create setup-profiles task when installing a component

* many: add side info param to snaptest.MockComponent

* many: fix failing tests caused by changes in rebase

* snap: add ComponentHookSecurityTag for getting a component hook's security tag

* interfaces: implement SecurityTagGlobs with snap.ComponentHookSecurityTag

* interfaces: move some helper functions to helpers.go

* o/snapstate: add functions that are useful when operating on component-related tasks

* o/ifacestate: use functions from snapstate rather than local functions

* i/apparmor: cleanup comment and whitespace

* o/snapstate: replace some speculative code with TODOs for now

* interfaces, o/ifacestate: remove Type from interfaces.Runnable and do not sort the result of SnapAppSet.Runnables()

* o/snapstate: remove unused variable
  • Loading branch information
andrewphelpsj authored May 8, 2024
1 parent 277fbc2 commit c1cf798
Show file tree
Hide file tree
Showing 27 changed files with 1,798 additions and 142 deletions.
30 changes: 13 additions & 17 deletions interfaces/apparmor/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ func nsProfile(snapName string) string {
// apps and hooks while the second profile describes the snap-update-ns profile
// for the whole snap.
func profileGlobs(snapName string) []string {
return []string{interfaces.SecurityTagGlob(snapName), nsProfile(snapName)}
return append(interfaces.SecurityTagGlobs(snapName), nsProfile(snapName))
}

// Determine if a profile filename is removable during core refresh/rollback.
Expand Down Expand Up @@ -357,7 +357,7 @@ func (b *Backend) prepareProfiles(appSet *interfaces.SnapAppSet, opts interfaces
spec.(*Specification).AddOvername(snapInfo)

// Add snippets derived from the layout definition.
spec.(*Specification).AddLayout(snapInfo)
spec.(*Specification).AddLayout(appSet)

// Add additional mount layouts rules for the snap.
spec.(*Specification).AddExtraLayouts(snapInfo, opts.ExtraLayouts)
Expand Down Expand Up @@ -396,13 +396,15 @@ func (b *Backend) prepareProfiles(appSet *interfaces.SnapAppSet, opts interfaces
}

// Get the files that this snap should have
content := b.deriveContent(spec.(*Specification), snapInfo, opts)
content := b.deriveContent(spec.(*Specification), appSet, opts)

dir := dirs.SnapAppArmorDir
globs := profileGlobs(snapInfo.InstanceName())
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("cannot create directory for apparmor profiles %q: %s", dir, err)
}

globs := profileGlobs(snapInfo.InstanceName())

changed, removedPaths, errEnsure := osutil.EnsureDirStateGlobs(dir, globs, content)
// XXX: in the old code this error was reported late, after doing load/removeCached.
if errEnsure != nil {
Expand Down Expand Up @@ -613,22 +615,16 @@ var (
coreRuntimePattern = regexp.MustCompile("^core([0-9][0-9])?$")
)

func (b *Backend) deriveContent(spec *Specification, snapInfo *snap.Info, opts interfaces.ConfinementOptions) (content map[string]osutil.FileState) {
content = make(map[string]osutil.FileState, len(snapInfo.Apps)+len(snapInfo.Hooks)+1)
func (b *Backend) deriveContent(spec *Specification, appSet *interfaces.SnapAppSet, opts interfaces.ConfinementOptions) (content map[string]osutil.FileState) {
runnables := appSet.Runnables()
content = make(map[string]osutil.FileState, len(runnables))
snapInfo := appSet.Info()

// Add profile for each app.
for _, appInfo := range snapInfo.Apps {
securityTag := appInfo.SecurityTag()
b.addContent(securityTag, snapInfo, appInfo.Name, opts, spec.SnippetForTag(securityTag), content, spec)
}
// Add profile for each hook.
for _, hookInfo := range snapInfo.Hooks {
securityTag := hookInfo.SecurityTag()
b.addContent(securityTag, snapInfo, "hook."+hookInfo.Name, opts, spec.SnippetForTag(securityTag), content, spec)
// Add profile for apps and hooks.
for _, r := range runnables {
b.addContent(r.SecurityTag, snapInfo, r.CommandName, opts, spec.SnippetForTag(r.SecurityTag), content, spec)
}

// TODO: something with component hooks will need to happen here

// Add profile for snap-update-ns if we have any apps or hooks.
// If we have neither then we don't have any need to create an executing environment.
// This applies to, for example, kernel snaps or gadget snaps (unless they have hooks).
Expand Down
214 changes: 213 additions & 1 deletion interfaces/apparmor/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,33 @@ func (s *backendSuite) TestInstallingSnapWithHookWritesAndLoadsProfiles(c *C) {
})
}

func (s *backendSuite) TestInstallingComponentWritesAndLoadsProfiles(c *C) {
const instanceName = ""
s.testInstallingComponentWritesAndLoadsProfiles(c, instanceName)
}

func (s *backendSuite) TestInstallingComponentWritesAndLoadsProfilesInstance(c *C) {
const instanceName = "snap_instance"
s.testInstallingComponentWritesAndLoadsProfiles(c, instanceName)
}

func (s *backendSuite) testInstallingComponentWritesAndLoadsProfiles(c *C, instanceName string) {
info := s.InstallSnapWithComponents(c, interfaces.ConfinementOptions{}, instanceName, ifacetest.SnapWithComponentsYaml, 1, []string{ifacetest.ComponentYaml})

expectedName := info.InstanceName()

componentHookProfile := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("snap.%s+comp.hook.install", expectedName))
// verify that profile for component hook was created
c.Check(componentHookProfile, testutil.FilePresent)

appProfile := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("snap.%s.app", expectedName))
updateNSProfile := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("snap-update-ns.%s", expectedName))
// apparmor_parser was used to load that file
c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{
{[]string{updateNSProfile, componentHookProfile, appProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.SkipReadCache},
})
}

const layoutYaml = `name: myapp
version: 1
apps:
Expand Down Expand Up @@ -360,6 +387,43 @@ func (s *backendSuite) TestInstallingSnapWithoutAppsOrHooksDoesntAddProfiles(c *
c.Check(s.loadProfilesCalls, HasLen, 0)
}

func (s *backendSuite) TestInstallingSnapWithComponentButNotInstalledDoesntAddProfiles(c *C) {
// installing a snap that has no app or hooks, but does have components
// defined, but no components are installed, should't generate any profiles
var snapWithComponents = `
name: snap
version: 1
components:
comp:
type: test
hooks:
install:
plugs:
iface:
`

s.InstallSnapWithComponents(c, interfaces.ConfinementOptions{}, "", snapWithComponents, 1, nil)
c.Check(s.loadProfilesCalls, HasLen, 0)
}

func (s *backendSuite) TestInstallingSnapWithComponentWithNoHooks(c *C) {
// installing a snap that has no app or hooks, but does have components
// defined, but the component doesn't have hooks, should't generate any
// profiles
var snapWithComponents = `
name: snap
version: 1
components:
comp:
type: test
plugs:
iface:
`

s.InstallSnapWithComponents(c, interfaces.ConfinementOptions{}, "", snapWithComponents, 1, []string{ifacetest.ComponentYaml})
c.Check(s.loadProfilesCalls, HasLen, 0)
}

func (s *backendSuite) TestTimings(c *C) {
oldDurationThreshold := timings.DurationThreshold
defer func() {
Expand Down Expand Up @@ -438,6 +502,42 @@ func (s *backendSuite) TestRemovingSnapWithHookRemovesAndUnloadsProfiles(c *C) {
}
}

func (s *backendSuite) TestRemovingComponentRemovesAndUnloadsProfiles(c *C) {
const instanceName = ""
s.testRemovingComponentRemovesAndUnloadsProfiles(c, instanceName)
}

func (s *backendSuite) TestRemovingComponentRemovesAndUnloadsProfilesInstance(c *C) {
const instanceName = "snap_instance"
s.testRemovingComponentRemovesAndUnloadsProfiles(c, instanceName)
}

func (s *backendSuite) testRemovingComponentRemovesAndUnloadsProfiles(c *C, instanceName string) {
for _, opts := range testedConfinementOpts {
snapInfo := s.InstallSnapWithComponents(c, opts, instanceName, ifacetest.SnapWithComponentsYaml, 1, []string{ifacetest.ComponentYaml})
s.removeCachedProfilesCalls = nil
s.RemoveSnap(c, snapInfo)

expectedName := snapInfo.InstanceName()

componentHookProfileName := fmt.Sprintf("snap.%s+comp.hook.install", expectedName)
updateNSProfileName := fmt.Sprintf("snap-update-ns.%s", expectedName)
appProfileName := fmt.Sprintf("snap.%s.app", expectedName)

componentHookProfile := filepath.Join(dirs.SnapAppArmorDir, componentHookProfileName)
updateNSProfile := filepath.Join(dirs.SnapAppArmorDir, updateNSProfileName)
appProfile := filepath.Join(dirs.SnapAppArmorDir, appProfileName)

c.Check(componentHookProfile, testutil.FileAbsent)
c.Check(updateNSProfile, testutil.FileAbsent)
c.Check(appProfile, testutil.FileAbsent)

c.Check(s.removeCachedProfilesCalls, DeepEquals, []removeCachedProfilesParams{
{[]string{updateNSProfileName, componentHookProfileName, appProfileName}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir)},
})
}
}

func (s *backendSuite) TestUpdatingSnapMakesNeccesaryChanges(c *C) {
for _, opts := range testedConfinementOpts {
snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1)
Expand Down Expand Up @@ -499,6 +599,81 @@ func (s *backendSuite) TestUpdatingSnapToOneWithMoreHooks(c *C) {
}
}

func (s *backendSuite) TestUpdatingSnapToOneWithMoreComponents(c *C) {
const instanceName = ""
s.testUpdatingSnapToOneWithMoreComponents(c, instanceName)
}

func (s *backendSuite) TestUpdatingSnapToOneWithMoreComponentsInstance(c *C) {
const instanceName = "snap_instance"
s.testUpdatingSnapToOneWithMoreComponents(c, instanceName)
}

func (s *backendSuite) testUpdatingSnapToOneWithMoreComponents(c *C, instanceName string) {
for _, opts := range testedConfinementOpts {
info := s.InstallSnap(c, opts, instanceName, ifacetest.SnapWithComponentsYaml, 1)
s.loadProfilesCalls = nil
info = s.UpdateSnapWithComponents(c, info, opts, ifacetest.SnapWithComponentsYaml, 1, []string{ifacetest.ComponentYaml})

expectedName := info.InstanceName()

updateNSProfile := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("snap-update-ns.%s", expectedName))
componentHookProfile := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("snap.%s+comp.hook.install", expectedName))
appProfile := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("snap.%s.app", expectedName))

// verify that profile "snap.snap+comp.hook.install" was created
c.Check(componentHookProfile, testutil.FilePresent)

// apparmor_parser was used to load all the profiles, the hook profile
// has changed so we force invalidate its cache.
c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{
{[]string{componentHookProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.SkipReadCache},
{[]string{updateNSProfile, appProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0},
})
s.RemoveSnap(c, info)
}
}

func (s *backendSuite) TestUpdatingSnapToOneWithFewerComponents(c *C) {
const instanceName = ""
s.testUpdatingSnapToOneWithFewerComponents(c, instanceName)
}

func (s *backendSuite) TestUpdatingSnapToOneWithFewerComponentsInstance(c *C) {
const instanceName = "snap_instance"
s.testUpdatingSnapToOneWithFewerComponents(c, instanceName)
}

func (s *backendSuite) testUpdatingSnapToOneWithFewerComponents(c *C, instanceName string) {
for _, opts := range testedConfinementOpts {
info := s.InstallSnapWithComponents(c, opts, instanceName, ifacetest.SnapWithComponentsYaml, 1, []string{ifacetest.ComponentYaml})

fmt.Println(info.InstanceName())

s.loadProfilesCalls = nil
// NOTE: the revision is kept the same to just test on the application being removed
info = s.UpdateSnap(c, info, opts, ifacetest.SnapWithComponentsYaml, 1)

expectedName := info.InstanceName()

updateNSProfile := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("snap-update-ns.%s", expectedName))
hookProfile := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("snap.%s+comp.hook.install", expectedName))
appProfile := filepath.Join(dirs.SnapAppArmorDir, fmt.Sprintf("snap.%s.app", expectedName))

c.Check(appProfile, testutil.FilePresent)
c.Check(updateNSProfile, testutil.FilePresent)

// verify profile component hook profile was removed
c.Check(hookProfile, testutil.FileAbsent)

// apparmor_parser was used to remove the unused profile
c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{
{[]string{updateNSProfile, appProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0},
})
s.RemoveSnap(c, info)
}
}

func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) {
for _, opts := range testedConfinementOpts {
snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1WithNmbd, 1)
Expand Down Expand Up @@ -1358,6 +1533,43 @@ profile "snap.foo.hook.configure" flags=(attach_disconnected,mediate_deleted) {
s.RemoveSnap(c, snapInfo)
}

func (s *backendSuite) TestTemplateVarsWithComponentHook(c *C) {
restore := apparmor_sandbox.MockLevel(apparmor_sandbox.Full)
defer restore()
restore = osutil.MockIsHomeUsingRemoteFS(func() (bool, error) { return false, nil })
defer restore()
restore = osutil.MockIsRootWritableOverlay(func() (string, error) { return "", nil })
defer restore()
// NOTE: replace the real template with a shorter variant
restoreTemplate := apparmor.MockTemplate("\n" +
"###VAR###\n" +
"###PROFILEATTACH### ###FLAGS### {\n" +
"###SNIPPETS###\n" +
"}\n")
defer restoreTemplate()

expected := `
# This is a snap name without the instance key
@{SNAP_NAME}="snap"
# This is a snap name with instance key
@{SNAP_INSTANCE_NAME}="snap"
@{SNAP_INSTANCE_DESKTOP}="snap"
@{SNAP_COMMAND_NAME}="snap+comp.hook.install"
@{SNAP_REVISION}="1"
@{PROFILE_DBUS}="snap_2esnap_2bcomp_2ehook_2einstall"
@{INSTALL_DIR}="/{,var/lib/snapd/}snap"
profile "snap.snap+comp.hook.install" flags=(attach_disconnected,mediate_deleted) {
}
`
info := s.InstallSnapWithComponents(c, interfaces.ConfinementOptions{}, "", ifacetest.SnapWithComponentsYaml, 1, []string{ifacetest.ComponentYaml})

profile := filepath.Join(dirs.SnapAppArmorDir, "snap.snap+comp.hook.install")
c.Check(profile, testutil.FileEquals, expected)

s.RemoveSnap(c, info)
}

const coreYaml = `name: core
version: 1
type: os
Expand Down Expand Up @@ -2227,7 +2439,7 @@ func (s *backendSuite) TestCasperOverlaySnippets(c *C) {

func (s *backendSuite) TestProfileGlobs(c *C) {
globs := apparmor.ProfileGlobs("foo")
c.Assert(globs, DeepEquals, []string{"snap.foo.*", "snap-update-ns.foo"})
c.Assert(globs, DeepEquals, []string{"snap.foo.*", "snap.foo+*.hook.*", "snap-update-ns.foo"})
}

func (s *backendSuite) TestNsProfile(c *C) {
Expand Down
15 changes: 7 additions & 8 deletions interfaces/apparmor/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,8 @@ func (spec *Specification) emitLayout(si *snap.Info, layout *snap.Layout) {
//
// Importantly, the above mount operations are happening within the per-snap
// mount namespace.
func (spec *Specification) AddLayout(snapInfo *snap.Info) {
func (spec *Specification) AddLayout(appSet *interfaces.SnapAppSet) {
snapInfo := appSet.Info()
if len(snapInfo.Layout) == 0 {
return
}
Expand All @@ -334,13 +335,11 @@ func (spec *Specification) AddLayout(snapInfo *snap.Info) {
}
sort.Strings(paths)

// Get tags describing all apps and hooks.
tags := make([]string, 0, len(snapInfo.Apps)+len(snapInfo.Hooks))
for _, app := range snapInfo.Apps {
tags = append(tags, app.SecurityTag())
}
for _, hook := range snapInfo.Hooks {
tags = append(tags, hook.SecurityTag())
// Get tags describing all runnables (apps, hooks, component hooks)
runnables := appSet.Runnables()
tags := make([]string, 0, len(runnables))
for _, r := range runnables {
tags = append(tags, r.SecurityTag)
}

// Append layout snippets to all tags; the layout applies equally to the
Expand Down
5 changes: 4 additions & 1 deletion interfaces/apparmor/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,10 @@ func (s *specSuite) TestApparmorSnippetsFromLayout(c *C) {
restore := apparmor.SetSpecScope(s.spec, []string{"snap.vanguard.vanguard"})
defer restore()

s.spec.AddLayout(snapInfo)
appSet, err := interfaces.NewSnapAppSet(snapInfo, nil)
c.Assert(err, IsNil)

s.spec.AddLayout(appSet)

c.Assert(s.spec.Snippets(), DeepEquals, map[string][]string{
"snap.vanguard.vanguard": {
Expand Down
Loading

0 comments on commit c1cf798

Please sign in to comment.