Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
Already on GitHub? Sign in to your account
interfaces/apparmor: add apparmor support code #635
Merged
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
67a2afd
interfaces/apparmor: add apparmor support code
zyga 9a528b5
Merge branch 'exec-test' into apparmor
zyga e7d10c7
Merge branch 'exec-test' into apparmor
zyga 6a38496
interfaces/apparmor: rename LoadOrReplaceProfile to LoadProfile
zyga 1b18fc0
interfaces/apparmor: fix golint comment
zyga a1147cd
interfaces/apparmor: use io.EOF
zyga 033134b
interfaces/apparmor: simplify LoadedProfile() loop
zyga e381e0e
interfaces/apparmor: simplify Trim in LoadedProfiles()
zyga c5003bf
interfaces/apparmor: drop named return values from LoadProfile()
zyga c8e8e54
interfaces/apparmor: make the parser more robust
zyga 388ae07
Merge remote-tracking branch 'upstream/master' into apparmor
zyga d26df19
interfaces/apparmor: improve error handling
zyga 7e023f8
interfaces/apparmor: fix enforce/complain vs enforicng/complaining
zyga dc75395
interfaces/apparmor: change how apparmor profiles are loaded
zyga f664d97
Merge branch 'master' into apparmor
zyga 28e7eb5
Merge remote-tracking branch 'upstream/master' into apparmor
zyga 05aa822
Merge remote-tracking branch 'origin/apparmor' into apparmor
zyga 939d1d5
interfaces/apparmor: use snap.* as mask
zyga e1b18d8
Merge remote-tracking branch 'upstream/master' into apparmor
zyga 60b4695
interfaces/apparmor: add note on no-expr-simplify
zyga
Jump to file or symbol
Failed to load files and symbols.
| @@ -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. | ||
|
|
||
| +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 | ||
| + }) | ||
| +} |
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)