From 87c96eed1109f7a76b42abe92b5ef98e0292a2e3 Mon Sep 17 00:00:00 2001 From: Seth Hoenig Date: Sat, 15 May 2021 14:48:01 -0600 Subject: [PATCH] drivers/docker: reuse capabilities plumbing in docker driver This changeset does not introduce any functional change for the docker driver, but rather cleans up the implementation around computing configured capabilities by re-using code written for the exec/java task drivers. --- drivers/docker/driver.go | 122 +----------- drivers/docker/driver_test.go | 191 ++----------------- drivers/exec/driver.go | 4 +- drivers/java/driver.go | 4 +- drivers/shared/capabilities/defaults.go | 100 ++++++++-- drivers/shared/capabilities/defaults_test.go | 178 ++++++++++++++++- drivers/shared/capabilities/set.go | 27 ++- drivers/shared/capabilities/set_test.go | 52 +++++ 8 files changed, 353 insertions(+), 325 deletions(-) diff --git a/drivers/docker/driver.go b/drivers/docker/driver.go index c8a2981ec..7234d3467 100644 --- a/drivers/docker/driver.go +++ b/drivers/docker/driver.go @@ -10,7 +10,6 @@ import ( "os" "path/filepath" "runtime" - "sort" "strconv" "strings" "sync" @@ -23,10 +22,9 @@ import ( plugin "github.com/hashicorp/go-plugin" "github.com/hashicorp/nomad/client/taskenv" "github.com/hashicorp/nomad/drivers/docker/docklog" + "github.com/hashicorp/nomad/drivers/shared/capabilities" "github.com/hashicorp/nomad/drivers/shared/eventer" - "github.com/hashicorp/nomad/drivers/shared/executor" "github.com/hashicorp/nomad/drivers/shared/resolvconf" - "github.com/hashicorp/nomad/helper" nstructs "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/plugins/base" "github.com/hashicorp/nomad/plugins/drivers" @@ -913,8 +911,9 @@ func (d *Driver) createContainerConfig(task *drivers.TaskConfig, driverConfig *T hostConfig.Privileged = driverConfig.Privileged // set add/drop capabilities - hostConfig.CapAdd, hostConfig.CapDrop, err = d.getCaps(driverConfig) - if err != nil { + if hostConfig.CapAdd, hostConfig.CapDrop, err = capabilities.Delta( + capabilities.DockerDefaults(), d.config.AllowCaps, driverConfig.CapAdd, driverConfig.CapDrop, + ); err != nil { return c, err } @@ -1184,119 +1183,6 @@ func (d *Driver) createContainerConfig(task *drivers.TaskConfig, driverConfig *T }, nil } -// getCaps computes the capabilities to supply to the --add-cap and --drop-cap -// options to the docker driver, which override the default capabilities enabled -// by docker itself. -func (d *Driver) getCaps(taskConfig *TaskConfig) ([]string, []string, error) { - - // capabilities allowable by client docker plugin configuration - allowCaps := expandAllowCaps(d.config.AllowCaps) - - // capabilities the task docker config is asking for based on the default - // capabilities allowable by nomad - desiredCaps, err := tweakCapabilities(nomadDefaultCaps(), taskConfig.CapAdd, taskConfig.CapDrop) - if err != nil { - return nil, nil, err - } - - // capabilities the task is requesting that are NOT allowed by the docker plugin - if missing := missingCaps(allowCaps, desiredCaps); len(missing) > 0 { - return nil, nil, fmt.Errorf("Docker driver does not have the following caps allow-listed on this Nomad agent: %s", missing) - } - - // capabilities that should be dropped relative to the docker default capabilities - dropCaps := capDrops(taskConfig.CapDrop, allowCaps) - - return taskConfig.CapAdd, dropCaps, nil -} - -// capDrops will compute the total dropped capabilities set -// -// {task cap_drop} U ({docker defaults} \ {driver allow caps}) -func capDrops(dropCaps []string, allowCaps []string) []string { - dropSet := make(map[string]struct{}) - - for _, c := range normalizeCaps(dropCaps) { - dropSet[c] = struct{}{} - } - - // if dropCaps includes ALL, no need to iterate every capability - if _, exists := dropSet["ALL"]; exists { - return []string{"ALL"} - } - - dockerDefaults := helper.SliceStringToSet(normalizeCaps(dockerDefaultCaps())) - allowedCaps := helper.SliceStringToSet(normalizeCaps(allowCaps)) - - // find the docker default caps not in allowed caps - for dCap := range dockerDefaults { - if _, exists := allowedCaps[dCap]; !exists { - dropSet[dCap] = struct{}{} - } - } - - drops := make([]string, 0, len(dropSet)) - for c := range dropSet { - drops = append(drops, c) - } - sort.Strings(drops) - return drops -} - -// expandAllowCaps returns the normalized set of allowable capabilities set -// for the docker plugin configuration. -func expandAllowCaps(allowCaps []string) []string { - if len(allowCaps) == 0 { - return nil - } - - set := make(map[string]struct{}, len(allowCaps)) - - for _, rawCap := range allowCaps { - capability := strings.ToUpper(rawCap) - if capability == "ALL" { - for _, defCap := range normalizeCaps(executor.SupportedCaps(true)) { - set[defCap] = struct{}{} - } - } else { - set[capability] = struct{}{} - } - } - - result := make([]string, 0, len(set)) - for capability := range set { - result = append(result, capability) - } - sort.Strings(result) - return result -} - -// missingCaps returns the set of elements in desired that are not present in -// allowed. The elements in desired are first upper-cased before comparison. -// The elements in allowed are assumed to be upper-cased. -func missingCaps(allowed, desired []string) []string { - _, missing := helper.SliceStringIsSubset(allowed, normalizeCaps(desired)) - sort.Strings(missing) - return missing -} - -// normalizeCaps returns a copy of caps with duplicate elements removed and all -// elements upper-cased. -func normalizeCaps(caps []string) []string { - set := make(map[string]struct{}, len(caps)) - for _, c := range caps { - normal := strings.TrimPrefix(strings.ToUpper(c), "CAP_") - set[strings.ToUpper(normal)] = struct{}{} - } - - result := make([]string, 0, len(set)) - for c := range set { - result = append(result, c) - } - sort.Strings(result) - return result -} - func (d *Driver) toDockerMount(m *DockerMount, task *drivers.TaskConfig) (*docker.HostMount, error) { hm, err := m.toDockerHostMount() if err != nil { diff --git a/drivers/docker/driver_test.go b/drivers/docker/driver_test.go index 67c22e88e..f51b8b3f0 100644 --- a/drivers/docker/driver_test.go +++ b/drivers/docker/driver_test.go @@ -19,7 +19,6 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/nomad/client/taskenv" "github.com/hashicorp/nomad/client/testutil" - "github.com/hashicorp/nomad/drivers/shared/executor" "github.com/hashicorp/nomad/helper/freeport" "github.com/hashicorp/nomad/helper/pluginutils/hclspecutils" "github.com/hashicorp/nomad/helper/pluginutils/hclutils" @@ -1387,44 +1386,44 @@ func TestDockerDriver_Capabilities(t *testing.T) { { Name: "default-allowlist-add-allowed", CapAdd: []string{"fowner", "mknod"}, - CapDrop: []string{"ALL"}, + CapDrop: []string{"all"}, }, { Name: "default-allowlist-add-forbidden", CapAdd: []string{"net_admin"}, - StartError: "NET_ADMIN", + StartError: "net_admin", }, { Name: "default-allowlist-drop-existing", - CapDrop: []string{"FOWNER", "MKNOD", "NET_RAW"}, + CapDrop: []string{"fowner", "mknod", "net_raw"}, }, { Name: "restrictive-allowlist-drop-all", - CapDrop: []string{"ALL"}, - Allowlist: "FOWNER,MKNOD", + CapDrop: []string{"all"}, + Allowlist: "fowner,mknod", }, { Name: "restrictive-allowlist-add-allowed", CapAdd: []string{"fowner", "mknod"}, - CapDrop: []string{"ALL"}, - Allowlist: "fowner,mknod", + CapDrop: []string{"all"}, + Allowlist: "mknod,fowner", }, { Name: "restrictive-allowlist-add-forbidden", CapAdd: []string{"net_admin", "mknod"}, - CapDrop: []string{"ALL"}, + CapDrop: []string{"all"}, Allowlist: "fowner,mknod", - StartError: "NET_ADMIN", + StartError: "net_admin", }, { Name: "permissive-allowlist", - CapAdd: []string{"net_admin", "mknod"}, - Allowlist: "ALL", + CapAdd: []string{"mknod", "net_admin"}, + Allowlist: "all", }, { Name: "permissive-allowlist-add-all", CapAdd: []string{"all"}, - Allowlist: "ALL", + Allowlist: "all", }, } @@ -3064,169 +3063,3 @@ func TestDockerDriver_StopSignal(t *testing.T) { }) } } - -func TestDockerCaps_normalizeCaps(t *testing.T) { - t.Run("empty", func(t *testing.T) { - result := normalizeCaps(nil) - require.Len(t, result, 0) - }) - - t.Run("mixed", func(t *testing.T) { - result := normalizeCaps([]string{ - "DAC_OVERRIDE", "sys_chroot", "kill", "KILL", - }) - require.Equal(t, []string{ - "DAC_OVERRIDE", "KILL", "SYS_CHROOT", - }, result) - }) -} - -func TestDockerCaps_missingCaps(t *testing.T) { - allowed := []string{ - "DAC_OVERRIDE", "SYS_CHROOT", "KILL", "CHOWN", - } - - t.Run("none missing", func(t *testing.T) { - result := missingCaps(allowed, []string{ - "SYS_CHROOT", "chown", "KILL", - }) - require.Equal(t, []string(nil), result) - }) - - t.Run("some missing", func(t *testing.T) { - result := missingCaps(allowed, []string{ - "chown", "audit_write", "SETPCAP", "dac_override", - }) - require.Equal(t, []string{"AUDIT_WRITE", "SETPCAP"}, result) - }) -} - -func TestDockerCaps_expandAllowCaps(t *testing.T) { - t.Run("empty", func(t *testing.T) { - result := expandAllowCaps(nil) - require.Empty(t, result) - }) - - t.Run("manual", func(t *testing.T) { - result := expandAllowCaps([]string{ - "DAC_OVERRIDE", "SYS_CHROOT", "KILL", "CHOWN", - }) - require.Equal(t, []string{ - "CHOWN", "DAC_OVERRIDE", "KILL", "SYS_CHROOT", - }, result) - }) - - t.Run("all", func(t *testing.T) { - result := expandAllowCaps([]string{"all"}) - exp := normalizeCaps(executor.SupportedCaps(true)) - sort.Strings(exp) - require.Equal(t, exp, result) - }) -} - -func TestDockerCaps_capDrops(t *testing.T) { - // docker default caps is always the same, task configured drop_caps and - // plugin config allow_caps may be altered - - // This is the 90% use case, where NET_RAW is dropped, as Nomad's default - // capability allow-list is a subset of the docker default cap list. - t.Run("defaults", func(t *testing.T) { - result := capDrops(nil, nomadDefaultCaps()) - require.Equal(t, []string{"NET_RAW"}, result) - }) - - // Users want to use ICMP (ping). - t.Run("enable net_raw", func(t *testing.T) { - result := capDrops(nil, append(nomadDefaultCaps(), "net_raw")) - require.Empty(t, result) - }) - - // The plugin is reduced in ability. - t.Run("enable minimal", func(t *testing.T) { - allow := []string{"setgid", "setuid", "chown", "kill"} - exp := []string{"AUDIT_WRITE", "DAC_OVERRIDE", "FOWNER", "FSETID", "MKNOD", "NET_BIND_SERVICE", "NET_RAW", "SETFCAP", "SETPCAP", "SYS_CHROOT"} - result := capDrops(nil, allow) - require.Equal(t, exp, result) - }) - - // The task drops abilities. - t.Run("task drops", func(t *testing.T) { - drops := []string{"audit_write", "fowner", "kill", "chown"} - exp := []string{"AUDIT_WRITE", "CHOWN", "FOWNER", "KILL", "NET_RAW"} - result := capDrops(drops, nomadDefaultCaps()) - require.Equal(t, exp, result) - }) - - // Drop all mixed with others. - t.Run("task drops mix", func(t *testing.T) { - drops := []string{"audit_write", "all", "chown"} - exp := []string{"ALL"} // minimized - result := capDrops(drops, nomadDefaultCaps()) - require.Equal(t, exp, result) - }) -} - -func TestDockerCaps_getCaps(t *testing.T) { - testutil.ExecCompatible(t) // tests require linux - - t.Run("defaults", func(t *testing.T) { - d := Driver{config: &DriverConfig{ - AllowCaps: nomadDefaultCaps(), - }} - add, drop, err := d.getCaps(&TaskConfig{ - CapAdd: nil, CapDrop: nil, - }) - require.NoError(t, err) - require.Empty(t, add) - require.Equal(t, []string{"NET_RAW"}, drop) - }) - - t.Run("enable net_raw", func(t *testing.T) { - d := Driver{config: &DriverConfig{ - AllowCaps: append(nomadDefaultCaps(), "net_raw"), - }} - add, drop, err := d.getCaps(&TaskConfig{ - CapAdd: nil, CapDrop: nil, - }) - require.NoError(t, err) - require.Empty(t, add) - require.Empty(t, drop) - }) - - t.Run("block sys_time", func(t *testing.T) { - d := Driver{config: &DriverConfig{ - AllowCaps: nomadDefaultCaps(), - }} - _, _, err := d.getCaps(&TaskConfig{ - CapAdd: []string{"SYS_TIME"}, - CapDrop: nil, - }) - require.EqualError(t, err, `Docker driver does not have the following caps allow-listed on this Nomad agent: [SYS_TIME]`) - }) - - t.Run("enable sys_time", func(t *testing.T) { - d := Driver{config: &DriverConfig{ - AllowCaps: append(nomadDefaultCaps(), "sys_time"), - }} - add, drop, err := d.getCaps(&TaskConfig{ - CapAdd: []string{"SYS_TIME"}, - CapDrop: nil, - }) - require.NoError(t, err) - require.Equal(t, []string{"SYS_TIME"}, add) - require.Equal(t, []string{"NET_RAW"}, drop) - }) - - t.Run("task drops chown", func(t *testing.T) { - d := Driver{config: &DriverConfig{ - AllowCaps: nomadDefaultCaps(), - }} - add, drop, err := d.getCaps(&TaskConfig{ - CapAdd: nil, - CapDrop: []string{"chown"}, - }) - require.NoError(t, err) - require.Empty(t, add) - require.Equal(t, []string{"CHOWN", "NET_RAW"}, drop) - }) -} diff --git a/drivers/exec/driver.go b/drivers/exec/driver.go index 0bd6864c6..ef95a374c 100644 --- a/drivers/exec/driver.go +++ b/drivers/exec/driver.go @@ -473,7 +473,9 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive cfg.Mounts = append(cfg.Mounts, dnsMount) } - caps, err := capabilities.Calculate(d.config.AllowCaps, driverConfig.CapAdd, driverConfig.CapDrop) + caps, err := capabilities.Calculate( + capabilities.NomadDefaults(), d.config.AllowCaps, driverConfig.CapAdd, driverConfig.CapDrop, + ) if err != nil { return nil, nil, err } diff --git a/drivers/java/driver.go b/drivers/java/driver.go index a34a56af4..d3272cef9 100644 --- a/drivers/java/driver.go +++ b/drivers/java/driver.go @@ -485,7 +485,9 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive cfg.Mounts = append(cfg.Mounts, dnsMount) } - caps, err := capabilities.Calculate(d.config.AllowCaps, driverConfig.CapAdd, driverConfig.CapDrop) + caps, err := capabilities.Calculate( + capabilities.NomadDefaults(), d.config.AllowCaps, driverConfig.CapAdd, driverConfig.CapDrop, + ) if err != nil { return nil, nil, err } diff --git a/drivers/shared/capabilities/defaults.go b/drivers/shared/capabilities/defaults.go index 30a69b65b..827f41019 100644 --- a/drivers/shared/capabilities/defaults.go +++ b/drivers/shared/capabilities/defaults.go @@ -39,6 +39,10 @@ func DockerDefaults() *Set { // Supported returns the set of capabilities supported by the operating system. // +// This set will expand over time as new capabilities are introduced to the kernel +// and the capability library is updated (which tends to happen to keep up with +// run-container libraries). +// // Defers to a library generated from // https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h func Supported() *Set { @@ -114,32 +118,92 @@ func LegacySupported() *Set { }) } -// Calculate the reduced set of linux capabilities to enable for driver, taking -// into account the capabilities allowed by the driver and the capabilities -// explicitly requested / removed by the task configuration. +// Calculate the resulting set of linux capabilities to enable for a task, taking +// into account: +// - default capability basis +// - driver allowable capabilities +// - task capability drops +// - task capability adds // -// capAdd if set indicates the minimal set of capabilities that should be enabled. -// capDrop if set indicates capabilities that should be dropped from the driver defaults +// Nomad establishes a standard set of enabled capabilities allowed by the task +// driver if allow_caps is not set. This is the same set that the task will be +// enabled with by default if allow_caps does not further reduce permissions, +// in which case the task capabilities will also be reduced accordingly. // -// If the task requests a capability not allowed by the driver, an error is -// returned. -func Calculate(allowCaps, capAdd, capDrop []string) ([]string, error) { - driverAllowed := New(allowCaps) +// The task will drop any capabilities specified in cap_drop, and add back +// capabilities specified in cap_add. The task will not be allowed to add capabilities +// not set in the the allow_caps setting (which by default is the same as the basis). +// +// cap_add takes precedence over cap_drop, enabling the common pattern of dropping +// all capabilities, then adding back the desired smaller set. e.g. +// cap_drop = ["all"] +// cap_add = ["chown", "kill"] +// +// Note that the resulting capability names are upper-cased and prefixed with +// "CAP_", which is the expected input for the exec/java driver implementation. +func Calculate(basis *Set, allowCaps, capAdd, capDrop []string) ([]string, error) { + allow := New(allowCaps) + adds := New(capAdd) // determine caps the task wants that are not allowed - taskCaps := New(capAdd) - missing := driverAllowed.Difference(taskCaps) + missing := allow.Difference(adds) if !missing.Empty() { return nil, fmt.Errorf("driver does not allow the following capabilities: %s", missing) } - // if task did not specify allowed caps, use nomad defaults minus task drops - if len(capAdd) == 0 { - driverAllowed.Remove(capDrop) - return driverAllowed.Slice(true), nil + // the realized enabled capabilities starts with what is allowed both by driver + // config AND is a member of the basis (i.e. nomad defaults) + result := basis.Intersect(allow) + + // then remove capabilities the task explicitly drops + result.Remove(capDrop) + + // then add back capabilities the task explicitly adds + return result.Union(adds).Slice(true), nil +} + +// Delta calculates the set of capabilities that must be added and dropped relative +// to a basis to achieve a desired result. The use case is that the docker driver +// assumes a default set (DockerDefault), and we must calculate what to pass into +// --cap-add and --cap-drop on container creation given the inputs of the docker +// plugin config for allow_caps, and the docker task configuration for cap_add and +// cap_drop. Note that the user provided cap_add and cap_drop settings are always +// included, even if they are redundant with the basis (maintaining existing +// behavior, working with existing tests). +// +// Note that the resulting capability names are lower-cased and not prefixed with +// "CAP_", which is the existing style used with the docker driver implementation. +func Delta(basis *Set, allowCaps, capAdd, capDrop []string) ([]string, []string, error) { + all := func(caps []string) bool { + for _, c := range caps { + if normalize(c) == "all" { + return true + } + } + return false } - // otherwise task did specify allowed caps, enable exactly those - taskAdd := New(capAdd) - return taskAdd.Slice(true), nil + // set of caps allowed by driver + allow := New(allowCaps) + + // determine caps the task wants that are not allowed + missing := allow.Difference(New(capAdd)) + if !missing.Empty() { + return nil, nil, fmt.Errorf("driver does not allow the following capabilities: %s", missing) + } + + // add what the task is asking for + add := New(capAdd).Slice(false) + if all(capAdd) { + add = []string{"all"} + } + + // drop what the task removes plus whatever is in the basis that is not + // in the driver allow configuration + drop := New(allowCaps).Difference(basis).Union(New(capDrop)).Slice(false) + if all(capDrop) { + drop = []string{"all"} + } + + return add, drop, nil } diff --git a/drivers/shared/capabilities/defaults_test.go b/drivers/shared/capabilities/defaults_test.go index 2fd9f70d4..408f954ea 100644 --- a/drivers/shared/capabilities/defaults_test.go +++ b/drivers/shared/capabilities/defaults_test.go @@ -45,27 +45,59 @@ func TestCaps_Calculate(t *testing.T) { err: nil, }, { - name: "allow all", + name: "allow all no mods", allowCaps: []string{"all"}, capAdd: nil, capDrop: nil, - exp: Supported().Slice(true), + exp: NomadDefaults().Slice(true), err: nil, }, { - name: "allow selection", + name: "allow selection no mods", allowCaps: []string{"cap_net_raw", "chown", "SYS_TIME"}, capAdd: nil, capDrop: nil, + exp: []string{"CAP_CHOWN"}, + err: nil, + }, + { + name: "allow selection and add them", + allowCaps: []string{"cap_net_raw", "chown", "SYS_TIME"}, + capAdd: []string{"net_raw", "sys_time"}, + capDrop: nil, exp: []string{"CAP_CHOWN", "CAP_NET_RAW", "CAP_SYS_TIME"}, err: nil, }, { - name: "add allowed", + name: "allow defaults and add redundant", allowCaps: NomadDefaults().Slice(false), capAdd: []string{"chown", "KILL"}, capDrop: nil, - exp: []string{"CAP_CHOWN", "CAP_KILL"}, + exp: NomadDefaults().Slice(true), + err: nil, + }, + { + name: "allow defaults and add all", + allowCaps: NomadDefaults().Slice(false), + capAdd: []string{"all"}, + capDrop: nil, + exp: nil, + err: errors.New("driver does not allow the following capabilities: audit_control, audit_read, block_suspend, bpf, dac_read_search, ipc_lock, ipc_owner, lease, linux_immutable, mac_admin, mac_override, net_admin, net_broadcast, net_raw, perfmon, sys_admin, sys_boot, sys_module, sys_nice, sys_pacct, sys_ptrace, sys_rawio, sys_resource, sys_time, sys_tty_config, syslog, wake_alarm"), + }, + { + name: "allow defaults and drop all", + allowCaps: NomadDefaults().Slice(false), + capAdd: nil, + capDrop: []string{"all"}, + exp: []string{}, + err: nil, + }, + { + name: "allow defaults and drop all and add back some", + allowCaps: NomadDefaults().Slice(false), + capAdd: []string{"chown", "fowner"}, + capDrop: []string{"all"}, + exp: []string{"CAP_CHOWN", "CAP_FOWNER"}, err: nil, }, { @@ -92,11 +124,145 @@ func TestCaps_Calculate(t *testing.T) { exp: []string{}, err: nil, }, + { + name: "drop all and add back", + allowCaps: NomadDefaults().Slice(false), + capAdd: []string{"chown", "mknod"}, + capDrop: []string{"all"}, + exp: []string{"CAP_CHOWN", "CAP_MKNOD"}, + err: nil, + }, } { t.Run(tc.name, func(t *testing.T) { - caps, err := Calculate(tc.allowCaps, tc.capAdd, tc.capDrop) + caps, err := Calculate(NomadDefaults(), tc.allowCaps, tc.capAdd, tc.capDrop) require.Equal(t, tc.err, err) require.Equal(t, tc.exp, caps) }) } } + +func TestCaps_Delta(t *testing.T) { + for _, tc := range []struct { + name string + + // input + allowCaps []string // driver config + capAdd []string // task config + capDrop []string // task config + + // output + expAdd []string + expDrop []string + err error + }{ + { + name: "the default setting", + allowCaps: NomadDefaults().Slice(false), + capAdd: nil, + capDrop: nil, + expAdd: []string{}, + expDrop: []string{"net_raw"}, + err: nil, + }, + { + name: "allow all no mods", + allowCaps: []string{"all"}, + capAdd: nil, + capDrop: nil, + expAdd: []string{}, + expDrop: []string{}, + err: nil, + }, + { + name: "allow non-default no mods", + allowCaps: []string{"cap_net_raw", "chown", "SYS_TIME"}, + capAdd: nil, + capDrop: nil, + expAdd: []string{}, + expDrop: []string{ + "audit_write", "dac_override", "fowner", "fsetid", + "kill", "mknod", "net_bind_service", "setfcap", + "setgid", "setpcap", "setuid", "sys_chroot"}, + err: nil, + }, + { + name: "allow default add from default", + allowCaps: NomadDefaults().Slice(false), + capAdd: []string{"chown", "KILL"}, + capDrop: nil, + expAdd: []string{"chown", "kill"}, + expDrop: []string{"net_raw"}, + err: nil, + }, + { + name: "allow default add disallowed", + allowCaps: NomadDefaults().Slice(false), + capAdd: []string{"chown", "net_raw"}, + capDrop: nil, + expAdd: nil, + expDrop: nil, + err: errors.New("driver does not allow the following capabilities: net_raw"), + }, + { + name: "allow default drop from default", + allowCaps: NomadDefaults().Slice(false), + capAdd: nil, + capDrop: []string{"chown", "fowner", "CAP_KILL", "SYS_CHROOT", "mknod", "dac_override"}, + expAdd: []string{}, + expDrop: []string{"chown", "dac_override", "fowner", "kill", "mknod", "net_raw", "sys_chroot"}, + err: nil, + }, + { + name: "allow default drop all", + allowCaps: NomadDefaults().Slice(false), + capAdd: nil, + capDrop: []string{"all"}, + expAdd: []string{}, + expDrop: []string{"all"}, + err: nil, + }, + { + name: "task drop all and add back", + allowCaps: NomadDefaults().Slice(false), + capAdd: []string{"chown", "fowner"}, + capDrop: []string{"all"}, + expAdd: []string{"chown", "fowner"}, + expDrop: []string{"all"}, + err: nil, + }, + { + name: "add atop allow all", + allowCaps: []string{"all"}, + capAdd: []string{"chown", "fowner"}, + capDrop: nil, + expAdd: []string{"chown", "fowner"}, + expDrop: []string{}, + err: nil, + }, + { + name: "add all atop all", + allowCaps: []string{"all"}, + capAdd: []string{"all"}, + capDrop: nil, + expAdd: []string{"all"}, + expDrop: []string{}, + err: nil, + }, + { + name: "add all atop defaults", + allowCaps: NomadDefaults().Slice(false), + capAdd: []string{"all"}, + capDrop: nil, + expAdd: nil, + expDrop: nil, + err: errors.New("driver does not allow the following capabilities: audit_control, audit_read, block_suspend, bpf, dac_read_search, ipc_lock, ipc_owner, lease, linux_immutable, mac_admin, mac_override, net_admin, net_broadcast, net_raw, perfmon, sys_admin, sys_boot, sys_module, sys_nice, sys_pacct, sys_ptrace, sys_rawio, sys_resource, sys_time, sys_tty_config, syslog, wake_alarm"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + add, drop, err := Delta(DockerDefaults(), tc.allowCaps, tc.capAdd, tc.capDrop) + require.Equal(t, tc.err, err) + require.Equal(t, tc.expAdd, add) + require.Equal(t, tc.expDrop, drop) + }) + } +} diff --git a/drivers/shared/capabilities/set.go b/drivers/shared/capabilities/set.go index 046573e2a..916e3d4d3 100644 --- a/drivers/shared/capabilities/set.go +++ b/drivers/shared/capabilities/set.go @@ -14,7 +14,7 @@ var null = nothing{} // operations, taking care of name normalization, and sentinel value expansions. // // Linux capabilities can be expressed in multiple ways when working with docker -// and/or libcontainer, along with Nomad. +// and/or executor, along with Nomad configuration. // // Capability names may be upper or lower case, and may or may not be prefixed // with "CAP_" or "cap_". On top of that, Nomad interprets the special name "all" @@ -61,6 +61,18 @@ func (s *Set) Remove(caps []string) { } } +// Union returns of Set of elements of both s and b. +func (s *Set) Union(b *Set) *Set { + data := make(map[string]nothing) + for c := range s.data { + data[c] = null + } + for c := range b.data { + data[c] = null + } + return &Set{data: data} +} + // Difference returns the Set of elements of b not in s. func (s *Set) Difference(b *Set) *Set { data := make(map[string]nothing) @@ -72,6 +84,17 @@ func (s *Set) Difference(b *Set) *Set { return &Set{data: data} } +// Intersect returns the Set of elements in both s and b. +func (s *Set) Intersect(b *Set) *Set { + data := make(map[string]nothing) + for c := range s.data { + if _, exists := b.data[c]; exists { + data[c] = null + } + } + return &Set{data: data} +} + // Empty return true if no capabilities exist in s. func (s *Set) Empty() bool { return len(s.data) == 0 @@ -84,7 +107,7 @@ func (s *Set) String() string { // Slice returns a sorted slice of capabilities in s. // -// big - indicates whether to uppercase and prefix capabilities with CAP_ +// upper - indicates whether to uppercase and prefix capabilities with CAP_ func (s *Set) Slice(upper bool) []string { caps := make([]string, 0, len(s.data)) for c := range s.data { diff --git a/drivers/shared/capabilities/set_test.go b/drivers/shared/capabilities/set_test.go index 8e072e8b0..2134719f2 100644 --- a/drivers/shared/capabilities/set_test.go +++ b/drivers/shared/capabilities/set_test.go @@ -160,3 +160,55 @@ func TestSet_Difference(t *testing.T) { require.Equal(t, "x, y", result.String()) }) } + +func TestSet_Intersect(t *testing.T) { + t.Parallel() + + t.Run("empty", func(t *testing.T) { + a := New(nil) + b := New([]string{"a", "b"}) + + result := a.Intersect(b) + require.True(t, result.Empty()) + + result2 := b.Intersect(a) + require.True(t, result2.Empty()) + }) + + t.Run("intersect", func(t *testing.T) { + a := New([]string{"A", "b", "C", "d", "e", "f", "G"}) + b := New([]string{"Z", "B", "E", "f", "y"}) + + result := a.Intersect(b) + require.Equal(t, "b, e, f", result.String()) + + result2 := b.Intersect(a) + require.Equal(t, "b, e, f", result2.String()) + }) +} + +func TestSet_Union(t *testing.T) { + t.Parallel() + + t.Run("empty", func(t *testing.T) { + a := New(nil) + b := New([]string{"a", "b"}) + + result := a.Union(b) + require.Equal(t, "a, b", result.String()) + + result2 := b.Union(a) + require.Equal(t, "a, b", result2.String()) + }) + + t.Run("union", func(t *testing.T) { + a := New([]string{"A", "b", "C", "d", "e", "f", "G"}) + b := New([]string{"Z", "B", "E", "f", "y"}) + + result := a.Union(b) + require.Equal(t, "a, b, c, d, e, f, g, y, z", result.String()) + + result2 := b.Union(a) + require.Equal(t, "a, b, c, d, e, f, g, y, z", result2.String()) + }) +}