Repository: appc/spec
Pull request #695 (2017-12-09 20:35:05)

spec: add os/linux/apparmor-profile isolator
AppArmor is a very widely used isolator in the GNU/Linux community,
providing an "easier to configure" alternative to SELinux. From the
perspective of AppC this is a fairly simple addition (profile data is
not included in the spec and must already be loaded into the kernel).

This feature is required for feature parity for ACI containers with OCI
ones (as AppArmor is also supported as a first-class citizen in OCI).

/cc @flavio @vrothberg
Signed-off-by: Aleksa Sarai <asarai@suse.de>


Patch:
diff --git a/schema/types/isolator_linux_specific.go b/schema/types/isolator_linux_specific.go
index ad3441a..0d9cc6e 100644
--- a/schema/types/isolator_linux_specific.go
+++ b/schema/types/isolator_linux_specific.go
@@ -31,6 +31,7 @@ const (
 	LinuxOOMScoreAdjName           = "os/linux/oom-score-adj"
 	LinuxCPUSharesName             = "os/linux/cpu-shares"
 	LinuxSELinuxContextName        = "os/linux/selinux-context"
+	LinuxAppArmorProfileName       = "os/linux/apparmor-profile"
 )
 
 var LinuxIsolatorNames = make(map[ACIdentifier]struct{})
@@ -45,6 +46,7 @@ func init() {
 		LinuxSeccompRemoveSetName:      func() IsolatorValue { return &LinuxSeccompRemoveSet{} },
 		LinuxSeccompRetainSetName:      func() IsolatorValue { return &LinuxSeccompRetainSet{} },
 		LinuxSELinuxContextName:        func() IsolatorValue { return &LinuxSELinuxContext{} },
+		LinuxAppArmorProfileName:       func() IsolatorValue { return &LinuxAppArmorProfile{} },
 	} {
 		AddIsolatorName(name, LinuxIsolatorNames)
 		AddIsolatorValueConstructor(name, con)
@@ -531,3 +533,67 @@ func (l LinuxSELinuxContext) AsIsolator() (*Isolator, error) {
 		value:    &l,
 	}, nil
 }
+
+type LinuxAppArmorProfileProfile string
+
+type linuxAppArmorValue struct {
+	Profile LinuxAppArmorProfileProfile `json:"profile"`
+}
+
+type LinuxAppArmorProfile struct {
+	val linuxAppArmorValue
+}
+
+func (l LinuxAppArmorProfile) AssertValid() error {
+	if l.val.Profile == "" {
+		return fmt.Errorf("invalid profile value %q", l.val.Profile)
+	}
+	return nil
+}
+
+func (l *LinuxAppArmorProfile) UnmarshalJSON(b []byte) error {
+	var v linuxAppArmorValue
+	err := json.Unmarshal(b, &v)
+	if err != nil {
+		return err
+	}
+	l.val = v
+	return nil
+}
+
+func (l LinuxAppArmorProfile) Profile() LinuxAppArmorProfileProfile {
+	return l.val.Profile
+}
+
+func (l LinuxAppArmorProfile) multipleAllowed() bool {
+	return false
+}
+
+func (l LinuxAppArmorProfile) Conflicts() []ACIdentifier {
+	return nil
+}
+
+func NewLinuxAppArmorProfile(apparmorProfile string) (*LinuxAppArmorProfile, error) {
+	l := LinuxAppArmorProfile{
+		linuxAppArmorValue{
+			LinuxAppArmorProfileProfile(apparmorProfile),
+		},
+	}
+	if err := l.AssertValid(); err != nil {
+		return nil, err
+	}
+	return &l, nil
+}
+
+func (l LinuxAppArmorProfile) AsIsolator() (*Isolator, error) {
+	b, err := json.Marshal(l.val)
+	if err != nil {
+		return nil, err
+	}
+	rm := json.RawMessage(b)
+	return &Isolator{
+		Name:     LinuxAppArmorProfileName,
+		ValueRaw: &rm,
+		value:    &l,
+	}, nil
+}
diff --git a/spec/ace.md b/spec/ace.md
index 61842e3..3d2fd3a 100644
--- a/spec/ace.md
+++ b/spec/ace.md
@@ -232,6 +232,33 @@ In the example above, the process will be only allowed to invoke syscalls specif
 
 In the example above, the SELinux security context `system_u:system_r:dhcpc_t:s0` will be applied to a pod or to a single application, depending on where this isolator is specified.
 
+#### os/linux/apparmor-profile
+
+* Scope: app/pod
+
+**Parameters:**
+
+* **profile** case-sensitive string containing the name of the (already loaded) AppArmor profile to apply to the current pod or application.
+
+**Notes:**
+ 1. Only a single `os/linux/apparmor-profile` isolator can be specified per-pod.
+ 2. Only a single `os/linux/apparmor-profile` isolator can be specified per-app.
+ 3. If a AppArmor security context is specified at pod level, it applies to all processes involved in running the pod.
+ 4. If specified both at pod and app level, app values override pod ones.
+ 5. Implementations MAY ignore this isolator if the host does not support AppArmor labeling.
+ 6. However, implementations MUST refuse to run the pod or application if the provided AppArmor profile is not available on the host.
+
+**Example:**
+
+```json
+"name": "os/linux/apparmor-profile",
+"value": {
+	"profile": "rkt-default"
+}
+```
+
+In the example above, the AppArmor profile `rkt-default` will be applied to a pod or to a single application, depending on where this isolator is specified.
+
 #### os/linux/capabilities-remove-set
 
 * Scope: app


Test patch:
diff --git a/schema/types/isolator_linux_specific_test.go b/schema/types/isolator_linux_specific_test.go
index 360dc42..e79b7fe 100644
--- a/schema/types/isolator_linux_specific_test.go
+++ b/schema/types/isolator_linux_specific_test.go
@@ -127,6 +127,39 @@ func TestNewLinuxSELinuxContext(t *testing.T) {
 
 }
 
+func TestNewLinuxAppArmorContext(t *testing.T) {
+	tests := []struct {
+		inProfile string
+
+		wprofile LinuxAppArmorProfileProfile
+		werr     bool
+	}{
+		{
+			"rkt-default",
+			LinuxAppArmorProfileProfile("rkt-default"),
+			false,
+		},
+		{
+			"",
+			LinuxAppArmorProfileProfile(""),
+			true,
+		},
+	}
+	for i, tt := range tests {
+		c, err := NewLinuxAppArmorProfile(tt.inProfile)
+		if tt.werr {
+			if err == nil {
+				t.Errorf("#%d: did not get expected error", i)
+			}
+			continue
+		}
+		if gprofile := c.Profile(); !reflect.DeepEqual(gprofile, tt.wprofile) {
+			t.Errorf("#%d: got profile %#v, want profile %#v", i, gprofile, tt.wprofile)
+		}
+	}
+
+}
+
 func TestNewLinuxCapabilitiesRevokeSet(t *testing.T) {
 	tests := []struct {
 		in []string
diff --git a/schema/types/isolator_test.go b/schema/types/isolator_test.go
index ba54cc6..85ec78c 100644
--- a/schema/types/isolator_test.go
+++ b/schema/types/isolator_test.go
@@ -284,6 +284,20 @@ func TestIsolatorUnmarshal(t *testing.T) {
 			}`,
 			true,
 		},
+		{
+			`{
+				"name": "os/linux/apparmor-profile",
+				"value": {"profile": "rkt-default"}
+			}`,
+			false,
+		},
+		{
+			`{
+				"name": "os/linux/apparmor-profile",
+				"value": {"profile": ""}
+			}`,
+			true,
+		},
 	}
 
 	for i, tt := range tests {