interfaces/apparmor: add apparmor support code #635

Merged
merged 20 commits into from Mar 22, 2016
Commits
Jump to file or symbol
Failed to load files and symbols.
+287 −0
Split
@@ -0,0 +1,106 @@
+// -*- 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 apparmor contains primitives for working with apparmor.
+//
+// References:
+// - http://wiki.apparmor.net/index.php/Kernel_interfaces
+// - http://apparmor.wiki.kernel.org/
+// - http://manpages.ubuntu.com/manpages/xenial/en/man7/apparmor.7.html
+package apparmor
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "strings"
+)
+
+// LoadProfile loads an apparmor profile from the given file.
+//
+// If no such profile was previously loaded then it is simply added to the kernel.
+// If there was a profile with the same name before, that profile is replaced.
@jdstrand

jdstrand Mar 22, 2016

Contributor

This is fine. A comment saying why we are using no-expr-simplify would be good. Eg:
// Use no-expr-simplify since expr-simplify is actually slower on armhf (LP: #1383858)

+func LoadProfile(fname string) error {
+ // Use no-expr-simplify since expr-simplify is actually slower on armhf (LP: #1383858)
+ output, err := exec.Command(
+ "apparmor_parser", "--replace", "--write-cache", "-O",
+ "no-expr-simplify", "--cache-loc=/var/cache/apparmor",
+ fname).CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("cannot load apparmor profile: %s\napparmor_parser output:\n%s", err, string(output))
+ }
+ return nil
+}
+
+// Profile contains the name and mode of an apparmor profile loaded into the kernel.
+type Profile struct {
+ // Name of the profile. This is is either full path of the executable or an
+ // arbitrary string without spaces.
+ Name string
+ // Mode is either "enforce" or "complain".
+ Mode string
+}
+
+// Unload removes a profile from the running kernel.
+//
+// The operation is done with: apparmor_parser --remove $name
+func (profile *Profile) Unload() error {
+ output, err := exec.Command("apparmor_parser", "--remove", profile.Name).CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("cannot unload apparmor profile: %s\napparmor_parser output:\n%s", err, string(output))
+ }
+ return nil
+}
+
+// profilesPath contains information about the currently loaded apparmor profiles.
+const realProfilesPath = "/sys/kernel/security/apparmor/profiles"
+
+var profilesPath = realProfilesPath
+
+// LoadedProfiles interrogates the kernel and returns a list of loaded apparmor profiles.
+//
+// Snappy manages apparmor profiles named "snap.*". Other profiles might exist on
+// the system (via snappy dimension) and those are filtered-out.
+func LoadedProfiles() ([]Profile, error) {
+ file, err := os.Open(profilesPath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ var profiles []Profile
+ for {
+ var name, mode string
+ n, err := fmt.Fscanf(file, "%s %s\n", &name, &mode)
+ if n > 0 && n != 2 {
+ return nil, fmt.Errorf("syntax error, expected: name (mode)")
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ mode = strings.Trim(mode, "()")
+ if strings.HasPrefix(name, "snap.") {
+ profiles = append(profiles, Profile{Name: name, Mode: mode})
+ }
+ }
+ return profiles, nil
+}
@@ -0,0 +1,149 @@
+// -*- 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 apparmor_test
+
+import (
+ "io/ioutil"
+ "path"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/ubuntu-core/snappy/interfaces/apparmor"
+ "github.com/ubuntu-core/snappy/testutil"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type appArmorSuite struct {
+ testutil.BaseTest
+ profilesFilename string
+}
+
+var _ = Suite(&appArmorSuite{})
+
+func (s *appArmorSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+ // Mock the list of profiles in the running kernel
+ s.profilesFilename = path.Join(c.MkDir(), "profiles")
+ apparmor.MockProfilesPath(&s.BaseTest, s.profilesFilename)
+}
+
+// Tests for LoadProfile()
+
+func (s *appArmorSuite) TestLoadProfileRunsAppArmorParserReplace(c *C) {
+ cmd := testutil.MockCommand(c, "apparmor_parser", "")
+ defer cmd.Restore()
+ err := apparmor.LoadProfile("foo.snap")
+ c.Assert(err, IsNil)
+ c.Assert(cmd.Calls(), DeepEquals, []string{
+ "--replace --write-cache -O no-expr-simplify --cache-loc=/var/cache/apparmor foo.snap"})
+}
+
+func (s *appArmorSuite) TestLoadProfileReportsErrors(c *C) {
+ cmd := testutil.MockCommand(c, "apparmor_parser", "exit 42")
+ defer cmd.Restore()
+ err := apparmor.LoadProfile("foo.snap")
+ c.Assert(err.Error(), Equals, `cannot load apparmor profile: exit status 42
+apparmor_parser output:
+`)
+ c.Assert(cmd.Calls(), DeepEquals, []string{
+ "--replace --write-cache -O no-expr-simplify --cache-loc=/var/cache/apparmor foo.snap"})
+}
+
+// Tests for Profile.Unload()
+
+func (s *appArmorSuite) TestUnloadProfileRunsAppArmorParserRemove(c *C) {
+ cmd := testutil.MockCommand(c, "apparmor_parser", "")
+ defer cmd.Restore()
+ profile := apparmor.Profile{Name: "foo.snap"}
+ err := profile.Unload()
+ c.Assert(err, IsNil)
+ c.Assert(cmd.Calls(), DeepEquals, []string{"--remove foo.snap"})
+}
+
+func (s *appArmorSuite) TestUnloadProfileReportsErrors(c *C) {
+ cmd := testutil.MockCommand(c, "apparmor_parser", "exit 42")
+ defer cmd.Restore()
+ profile := apparmor.Profile{Name: "foo.snap"}
+ err := profile.Unload()
+ c.Assert(err.Error(), Equals, `cannot unload apparmor profile: exit status 42
+apparmor_parser output:
+`)
+}
+
+// Tests for LoadedProfiles()
+
+func (s *appArmorSuite) TestLoadedApparmorProfilesReturnsErrorOnMissingFile(c *C) {
+ profiles, err := apparmor.LoadedProfiles()
+ c.Assert(err, ErrorMatches, "open .*: no such file or directory")
+ c.Check(profiles, IsNil)
+}
+
+func (s *appArmorSuite) TestLoadedApparmorProfilesCanParseEmptyFile(c *C) {
+ ioutil.WriteFile(s.profilesFilename, []byte(""), 0600)
+ profiles, err := apparmor.LoadedProfiles()
+ c.Assert(err, IsNil)
+ c.Check(profiles, HasLen, 0)
+}
+
+func (s *appArmorSuite) TestLoadedApparmorProfilesParsesAndFiltersData(c *C) {
+ ioutil.WriteFile(s.profilesFilename, []byte(
+ // The output contains some of the snappy-specific elements
+ // and some non-snappy elements pulled from Ubuntu 16.04 desktop
+ //
+ // The pi2-piglow.{background,foreground}.snap entries are the only
+ // ones that should be reported by the function.
+ `/sbin/dhclient (enforce)
+/usr/bin/ubuntu-core-launcher (enforce)
+/usr/bin/ubuntu-core-launcher (enforce)
+/usr/lib/NetworkManager/nm-dhcp-client.action (enforce)
+/usr/lib/NetworkManager/nm-dhcp-helper (enforce)
+/usr/lib/connman/scripts/dhclient-script (enforce)
+/usr/lib/lightdm/lightdm-guest-session (enforce)
+/usr/lib/lightdm/lightdm-guest-session//chromium (enforce)
+/usr/lib/telepathy/telepathy-* (enforce)
+/usr/lib/telepathy/telepathy-*//pxgsettings (enforce)
+/usr/lib/telepathy/telepathy-*//sanitized_helper (enforce)
+snap.pi2-piglow.background (enforce)
+snap.pi2-piglow.foreground (enforce)
+webbrowser-app (enforce)
+webbrowser-app//oxide_helper (enforce)
+`), 0600)
+ profiles, err := apparmor.LoadedProfiles()
+ c.Assert(err, IsNil)
+ c.Check(profiles, DeepEquals, []apparmor.Profile{
+ {"snap.pi2-piglow.background", "enforce"},
+ {"snap.pi2-piglow.foreground", "enforce"},
+ })
+}
+
+func (s *appArmorSuite) TestLoadedApparmorProfilesHandlesParsingErrors(c *C) {
+ ioutil.WriteFile(s.profilesFilename, []byte("broken stuff here\n"), 0600)
+ profiles, err := apparmor.LoadedProfiles()
+ c.Assert(err, ErrorMatches, "newline in format does not match input")
+ c.Check(profiles, IsNil)
+ ioutil.WriteFile(s.profilesFilename, []byte("truncated"), 0600)
+ profiles, err = apparmor.LoadedProfiles()
+ c.Assert(err, ErrorMatches, `syntax error, expected: name \(mode\)`)
+ c.Check(profiles, IsNil)
+}
@@ -0,0 +1,32 @@
+// -*- 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 apparmor
+
+import (
+ "github.com/ubuntu-core/snappy/testutil"
+)
+
+// MockProfilesPath mocks the file read by LoadedProfiles()
+func MockProfilesPath(t *testutil.BaseTest, profiles string) {
+ profilesPath = profiles
+ t.AddCleanup(func() {
+ profilesPath = realProfilesPath
+ })
+}