interfaces/apparmor: clean up template parsing #759

Merged
merged 16 commits into from Mar 31, 2016
@@ -42,6 +42,7 @@ import (
"bytes"
"fmt"
"path/filepath"
+ "regexp"
"github.com/ubuntu-core/snappy/dirs"
"github.com/ubuntu-core/snappy/interfaces"
@@ -51,11 +52,18 @@ import (
// Backend is responsible for maintaining apparmor profiles for ubuntu-core-launcher.
type Backend struct {
- // CustomTemplate exists to support old-security which goes
+ // legacyTemplate exists to support old-security which goes
// beyond what is possible with pure security snippets.
//
// If non-empty then it overrides the built-in template.
- CustomTemplate string
+ legacyTemplate []byte
+}
+
+// UseLegacyTemplate switches from default apparmor template to a custom
+// template. This also implies that a fixed set of apparmor variables will be
+// injected into this template. The set is compatible with Ubuntu core 15.04.
+func (b *Backend) UseLegacyTemplate(template []byte) {
+ b.legacyTemplate = template
}
// Configure creates and loads apparmor security profiles specific to a given
@@ -105,22 +113,46 @@ func (b *Backend) Deconfigure(snapInfo *snap.Info) error {
return nil
}
+var (
+ templatePattern = regexp.MustCompile("(###[A-Z]+###)")
+ placeholderVar = []byte("###VAR###")
+ placeholderSnippets = []byte("###SNIPPETS###")
+ placeholderProfileAttach = []byte("###PROFILEATTACH###")
+ attachPattern = regexp.MustCompile(`\(attach_disconnected\)`)
+ attachComplain = []byte("(attach_disconnected,complain)")
+)
+
// combineSnippets combines security snippets collected from all the interfaces
// affecting a given snap into a content map applicable to EnsureDirState. The
// backend delegates writing those files to higher layers.
func (b *Backend) combineSnippets(snapInfo *snap.Info, developerMode bool, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) {
for _, appInfo := range snapInfo.Apps {
- s := make([][]byte, 0, len(snippets[appInfo.Name])+2)
- s = append(s, b.aaHeader(appInfo, developerMode))
- s = append(s, snippets[appInfo.Name]...)
- s = append(s, []byte("}\n"))
- fileContent := bytes.Join(s, []byte("\n"))
+ policy := b.legacyTemplate
+ if policy == nil {
+ policy = defaultTemplate
+ }
+ if developerMode {
+ policy = attachPattern.ReplaceAll(policy, attachComplain)
+ }
+ policy = templatePattern.ReplaceAllFunc(policy, func(placeholder []byte) []byte {
+ switch {
+ case bytes.Equal(placeholder, placeholderVar):
+ // TODO: use modern variables when default template is compatible
+ // with them and the custom template is not used.
+ return legacyVariables(appInfo)
+ case bytes.Equal(placeholder, placeholderProfileAttach):
+ return []byte(fmt.Sprintf("profile \"%s\"", interfaces.SecurityTag(appInfo)))
+ case bytes.Equal(placeholder, placeholderSnippets):
+ return bytes.Join(snippets[appInfo.Name], []byte("\n"))
+ }
+ return nil
+ })
if content == nil {
content = make(map[string]*osutil.FileState)
}
fname := interfaces.SecurityTag(appInfo)
content[fname] = &osutil.FileState{
- Content: fileContent,
+ Content: policy,
Mode: 0644,
}
}
@@ -47,12 +47,14 @@ var _ = Suite(&backendSuite{
})
func (s *backendSuite) SetUpTest(c *C) {
- s.backend.CustomTemplate = ""
+ s.backend.UseLegacyTemplate(nil)
// Isolate this test to a temporary directory
s.rootDir = c.MkDir()
dirs.SetRootDir(s.rootDir)
// Mock away any real apparmor interaction
- s.cmds = apparmor.MockExternalCommands(c)
+ s.cmds = map[string]*testutil.MockCmd{
+ "apparmor_parser": testutil.MockCommand(c, "apparmor_parser", ""),
+ }
// Prepare a directory for apparmor profiles.
// NOTE: Normally this is a part of the OS snap.
err := os.MkdirAll(dirs.SnapAppArmorDir, 0700)
@@ -210,14 +212,15 @@ func (s *backendSuite) TestRealDefaultTemplateIsNormallyUsed(c *C) {
}
func (s *backendSuite) TestCustomTemplateUsedOnRequest(c *C) {
- s.backend.CustomTemplate = `
+ s.backend.UseLegacyTemplate([]byte(`
# Description: Custom template for testing
###VAR###
###PROFILEATTACH### (attach_disconnected) {
+ ###SNIPPETS###
FOO
}
-`
+`))
snapInfo, err := snap.InfoFromSnapYaml([]byte(sambaYamlV1))
c.Assert(err, IsNil)
err = s.backend.Configure(snapInfo, false, s.repo)
@@ -253,6 +256,7 @@ const commonPrefix = `
var combineSnippetsScenarios = []combineSnippetsScenario{{
content: commonPrefix + `
profile "snap.samba.smbd" (attach_disconnected) {
+
}
`,
}, {
@@ -266,6 +270,7 @@ snippet
developerMode: true,
content: commonPrefix + `
profile "snap.samba.smbd" (attach_disconnected,complain) {
+
}
`,
}, {
@@ -279,10 +284,11 @@ snippet
func (s *backendSuite) TestCombineSnippets(c *C) {
// NOTE: replace the real template with a shorter variant
- restore := apparmor.MockTemplate("\n" +
+ restore := apparmor.MockTemplate([]byte("\n" +
"###VAR###\n" +
"###PROFILEATTACH### (attach_disconnected) {\n" +
- "}\n")
+ "###SNIPPETS###\n" +
+ "}\n"))
defer restore()
for _, scenario := range combineSnippetsScenarios {
s.iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
@@ -20,8 +20,6 @@
package apparmor
import (
- "gopkg.in/check.v1"
-
"github.com/ubuntu-core/snappy/testutil"
)
@@ -37,16 +35,8 @@ func MockProfilesPath(t *testutil.BaseTest, profiles string) {
//
// NOTE: The real apparmor template is long. For testing it is convenient for
// replace it with a shorter snippet.
-func MockTemplate(fakeTemplate string) (restore func()) {
+func MockTemplate(fakeTemplate []byte) (restore func()) {
orig := defaultTemplate
defaultTemplate = fakeTemplate
return func() { defaultTemplate = orig }
}
-
-// MockExternalCommands mocks and returns MockCmd for each of the external
-// commands used in this package.
-func MockExternalCommands(c *check.C) map[string]*testutil.MockCmd {
- return map[string]*testutil.MockCmd{
- "apparmor_parser": testutil.MockCommand(c, "apparmor_parser", ""),
- }
-}
@@ -24,7 +24,7 @@ package apparmor
// It can be overridden for testing using MockTemplate().
//
// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/templates/ubuntu-core/16.04/default
-var defaultTemplate = `
+var defaultTemplate = []byte(`
# Description: Allows access to app-specific directories and basic runtime
# Usage: common
@@ -286,5 +286,7 @@ var defaultTemplate = `
/sys/devices/**/ r,
/sys/class/ r,
/sys/class/**/ r,
+
+###SNIPPETS###
}
-`
+`)
@@ -22,7 +22,6 @@ package apparmor
import (
"bytes"
"fmt"
- "strings"
"github.com/ubuntu-core/snappy/interfaces"
"github.com/ubuntu-core/snappy/interfaces/dbus"
@@ -44,7 +43,7 @@ import (
// In addition, the set of variables listed here interacts with old-security
// interface since there the base template is provided by a particular 3rd
// party snap, not by snappy.
-func legacyVariables(appInfo *snap.AppInfo) string {
+func legacyVariables(appInfo *snap.AppInfo) []byte {
var buf bytes.Buffer
fmt.Fprintf(&buf, "@{APP_APPNAME}=\"%s\"\n", appInfo.Name)
// TODO: replace with app.SecurityTag()
@@ -58,8 +57,8 @@ func legacyVariables(appInfo *snap.AppInfo) string {
fmt.Fprintf(&buf, "@{APP_PKGNAME}=\"%s.%s\"\n", appInfo.Snap.Name, appInfo.Snap.Developer)
// TODO: switch to .Revision
fmt.Fprintf(&buf, "@{APP_VERSION}=\"%s\"\n", appInfo.Snap.Version)
- fmt.Fprintf(&buf, "@{INSTALL_DIR}=\"{/snaps,/gadget}\"\n")
- return buf.String()
+ fmt.Fprintf(&buf, "@{INSTALL_DIR}=\"{/snaps,/gadget}\"")
+ return buf.Bytes()
}
// modenVariables returns text defining some apparmor variables that
@@ -72,38 +71,11 @@ func legacyVariables(appInfo *snap.AppInfo) string {
// @{SNAP_NAME}=app.SnapName
//
// ...have everything work correctly?
-func modernVariables(appInfo *snap.AppInfo) string {
+func modernVariables(appInfo *snap.AppInfo) []byte {
var buf bytes.Buffer
fmt.Fprintf(&buf, "@{APP_NAME}=\"%s\"\n", appInfo.Name)
fmt.Fprintf(&buf, "@{APP_SECURITY_TAG}=\"%s\"\n", interfaces.SecurityTag(appInfo))
fmt.Fprintf(&buf, "@{SNAP_NAME}=\"%s\"\n", appInfo.Snap.Name)
- fmt.Fprintf(&buf, "@{INSTALL_DIR}=\"{/snaps,/gadget}\"\n")
- return buf.String()
-}
-
-// aaHeader returns the topmost part of the generated apparmor profile.
-//
-// The header contains a few lines of apparmor variables that are referenced by
-// the template as well as the syntax that begins the content of the actual
-// profile. That same content also decides if the profile is enforcing or
-// advisory (complain). This is used to implement developer mode.
-func (b *Backend) aaHeader(appInfo *snap.AppInfo, developerMode bool) []byte {
- var text string
- // Use a different template when requested.
- if len(b.CustomTemplate) == 0 {
- text = strings.TrimRight(defaultTemplate, "\n}")
- // TODO: switch to modernVariables() after the default template is
- // compatible with them.
- text = strings.Replace(text, "###VAR###\n", legacyVariables(appInfo), 1)
- } else {
- text = strings.TrimRight(b.CustomTemplate, "\n}")
- text = strings.Replace(text, "###VAR###\n", legacyVariables(appInfo), 1)
- }
- if developerMode {
- // XXX: This needs to be verified by security team.
- text = strings.Replace(text, "(attach_disconnected)", "(attach_disconnected,complain)", 1)
- }
- text = strings.Replace(text, "###PROFILEATTACH###",
- fmt.Sprintf("profile \"%s\"", interfaces.SecurityTag(appInfo)), 1)
- return []byte(text)
+ fmt.Fprintf(&buf, "@{INSTALL_DIR}=\"{/snaps,/gadget}\"")
+ return buf.Bytes()
}