Skip to content

Commit

Permalink
interfaces/apparmor: add function for probing / mocking
Browse files Browse the repository at this point in the history
This patch adds a new function for probing for supported apparmor
features as well as for mocking that. Unlike the current code in the
release module (where it doesn't really belong as it is no longer based
on looking at the /etc/os-release file).

The improvement from the old code is that the "feature level" is not a
boolean, there are three values and more may be added if desired. They
are "none" (apparmor not enabled at all), "partial" (apparmor enabled
but some features are missing) and "full" (all required features
availalble).

In the next few patches I will transition the old release interface over
to this and will start using it to make better decisions as to how
apparmor backend should be loaded and how it should operate.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
  • Loading branch information
zyga committed Aug 24, 2017
1 parent 34d5bd0 commit ef40971
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 0 deletions.
109 changes: 109 additions & 0 deletions interfaces/apparmor/probe.go
@@ -0,0 +1,109 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2017 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 apparmor

import (
"io/ioutil"
"os"
"path/filepath"
)

// FeatureLevel encodes the kind of support for apparmor found on this system.
type FeatureLevel int

const (
// None indicates that apparmor is not enabled.
None FeatureLevel = iota
// Partial indicates that apparmor is enabled but some features are missing.
Partial
// Full indicates that all features are supported.
Full
)

var (
// featureSysPath points to the sysfs directory where apparmor features are listed.
featuresSysPath = "/sys/kernel/security/apparmor/features"

This comment has been minimized.

Copy link
@jdstrand

jdstrand Aug 24, 2017

Elsewhere we use dirs/dirs.go for this sort of thing.

This comment has been minimized.

Copy link
@jdstrand

jdstrand Aug 24, 2017

I think using dirs/dirs.go will make the mocking a little easier.

This comment has been minimized.

Copy link
@zyga

zyga Aug 24, 2017

Author Owner

Ironically it's also a chore because of cyclic imports. I'm untangling that and I may end up being able to actually import dirs here as well. I'll try.

// requiredFeatures are the apparmor features needed for strict confinement.
requiredFeatures = []string{
"caps",
"dbus",
"domain",
"file",
"mount",
"namespaces",
"network",
"ptrace",
"signal",

This comment has been minimized.

Copy link
@jdstrand

jdstrand Aug 24, 2017

While not currently used in our policy, we should maybe list rlimit here for completeness. rlimit is an old feature that was added in apparmor 2.3 (the same time the /sys/kernel/security/apparmor/features was added). I confirmed that Ubuntu 12.04 3.2 kernels have it and unpatched 4.9 Debian has it.

This comment has been minimized.

Copy link
@zyga

zyga Aug 24, 2017

Author Owner

I'll add that! Thank you for noticing.

This comment has been minimized.

Copy link
@zyga

zyga Aug 24, 2017

Author Owner

Done

}
)

// Probe checks which apparmor features are available.
func Probe() FeatureLevel {
_, err := os.Stat(featuresSysPath)
if err != nil {
return None
}

This comment has been minimized.

Copy link
@jdstrand

jdstrand Aug 24, 2017

It is probably sufficient to say if the features directory isn't present, then to return None, but aa_is_enabled (http://bazaar.launchpad.net/~apparmor-dev/apparmor/master/view/head:/libraries/libapparmor/src/kernel.c#L106) is a little more robust and might be useful for logging why we are returning None. Note that aa-enabled is a small C program wrapper around aa_is_enabled, so calling it directly from the core snap might be best.

This comment has been minimized.

Copy link
@zyga

zyga Aug 24, 2017

Author Owner

I've added logging (well, errors that can be logged). Looking at the referenced apparmor library code I can easily check for /sys/module/apparmor/parameters/enabled. I want to balance correctness and simplicity here and I'd rather not call into a C program to get the answer.

for _, feature := range requiredFeatures {
p := filepath.Join(featuresSysPath, feature)
if _, err := os.Stat(p); err != nil {
return Partial
}
}
return Full

This comment has been minimized.

Copy link
@jdstrand

jdstrand Aug 24, 2017

It might be nice to return the 'why' here for future logging. Eg:

    return None, fmt.Errof("...")
    return Partial, fmt.Errof("missing x, y, z")
    return Full, nil

This comment has been minimized.

Copy link
@zyga

zyga Aug 24, 2017

Author Owner

That's a good idea, I'll make that so

This comment has been minimized.

Copy link
@zyga

zyga Aug 24, 2017

Author Owner

Done

}

// MockFeatureLevel fakes the desired apparmor feature level.
func MockFeatureLevel(level FeatureLevel) (restore func()) {
oldFeaturesSysPath := featuresSysPath

temp, err := ioutil.TempDir("", "mock-apparmor-feature-level")
if err != nil {
panic(err)
}
fakeFeaturesSysPath := filepath.Join(temp, "apparmor")

switch level {
case None:
// create no directory at all (apparmor not available).
break
case Partial:
// create just the empty directory with no features.
if err := os.MkdirAll(fakeFeaturesSysPath, 0755); err != nil {
panic(err)
}
break
case Full:
// create all the feature directories.
for _, feature := range requiredFeatures {
if err := os.MkdirAll(filepath.Join(fakeFeaturesSysPath, feature), 0755); err != nil {
panic(err)
}
}
break
}

featuresSysPath = fakeFeaturesSysPath
return func() {
if err := os.RemoveAll(temp); err != nil {
panic(err)
}
featuresSysPath = oldFeaturesSysPath
}
}
38 changes: 38 additions & 0 deletions interfaces/apparmor/probe_test.go
@@ -0,0 +1,38 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2017 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 apparmor_test

import (
. "gopkg.in/check.v1"

"github.com/snapcore/snapd/interfaces/apparmor"
)

type probeSuite struct{}

var _ = Suite(&probeSuite{})

func (s *probeSuite) TestProbe(c *C) {
for _, l := range []apparmor.FeatureLevel{apparmor.None, apparmor.Partial, apparmor.Full} {
restore := apparmor.MockFeatureLevel(l)
defer restore()
c.Assert(apparmor.Probe(), Equals, l, Commentf("was hoping for %q", l))
}
}

1 comment on commit ef40971

@jdstrand
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direction of this commit is fine. Couple of small comments.

Please sign in to comment.