diff --git a/.eslintrc.yml b/.eslintrc.yml index 5891d1965..bd801a518 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -23,6 +23,8 @@ parserOptions: modules: true rules: + no-console: error + no-alert: error no-control-regex: 'off' no-empty: warn no-empty-function: warn @@ -86,8 +88,8 @@ overrides: no-plusplus: off func-style: [error, 'declaration'] import/prefer-default-export: off - no-use-before-define: "off" - '@typescript-eslint/no-use-before-define': ['error', { functions: false, "allowNamedExports": true }] + no-use-before-define: 'off' + '@typescript-eslint/no-use-before-define': ['error', { functions: false, 'allowNamedExports': true }] no-shadow: 'off' '@typescript-eslint/no-shadow': off jsx-a11y/no-autofocus: warn diff --git a/app/docker/components/container-capabilities/container-capabilities.controller.js b/app/docker/components/container-capabilities/container-capabilities.controller.js deleted file mode 100644 index a743f5a50..000000000 --- a/app/docker/components/container-capabilities/container-capabilities.controller.js +++ /dev/null @@ -1,23 +0,0 @@ -export default class ContainerCapabilitiesController { - /* @ngInject */ - constructor($scope) { - this.$scope = $scope; - - this.oldCapabilities = []; - } - - createOnChangeHandler(capability) { - return (checked) => { - this.$scope.$evalAsync(() => { - capability.allowed = checked; - }); - }; - } - - $doCheck() { - if (this.oldCapabilities.length !== this.capabilities.length) { - this.oldCapabilities = this.capabilities; - this.capabilitiesOnChange = Object.fromEntries(this.capabilities.map((cap) => [cap.capability, this.createOnChangeHandler(cap)])); - } - } -} diff --git a/app/docker/components/container-capabilities/container-capabilities.js b/app/docker/components/container-capabilities/container-capabilities.js deleted file mode 100644 index aa13b5ac8..000000000 --- a/app/docker/components/container-capabilities/container-capabilities.js +++ /dev/null @@ -1,9 +0,0 @@ -import controller from './container-capabilities.controller'; - -angular.module('portainer.docker').component('containerCapabilities', { - templateUrl: './containerCapabilities.html', - bindings: { - capabilities: '=', - }, - controller, -}); diff --git a/app/docker/components/container-capabilities/containerCapabilities.html b/app/docker/components/container-capabilities/containerCapabilities.html deleted file mode 100644 index d2e0e47b2..000000000 --- a/app/docker/components/container-capabilities/containerCapabilities.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
Container capabilities
-
-
- -
-
-
diff --git a/app/docker/models/containerCapabilities.js b/app/docker/models/containerCapabilities.js deleted file mode 100644 index dadf14eac..000000000 --- a/app/docker/models/containerCapabilities.js +++ /dev/null @@ -1,90 +0,0 @@ -var capDesc = { - SETPCAP: 'Modify process capabilities.', - MKNOD: 'Create special files using mknod(2).', - AUDIT_WRITE: 'Write records to kernel auditing log.', - CHOWN: 'Make arbitrary changes to file UIDs and GIDs (see chown(2)).', - NET_RAW: 'Use RAW and PACKET sockets.', - DAC_OVERRIDE: 'Bypass file read, write, and execute permission checks.', - FOWNER: 'Bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file.', - FSETID: 'Don’t clear set-user-ID and set-group-ID permission bits when a file is modified.', - KILL: 'Bypass permission checks for sending signals.', - SETGID: 'Make arbitrary manipulations of process GIDs and supplementary GID list.', - SETUID: 'Make arbitrary manipulations of process UIDs.', - NET_BIND_SERVICE: 'Bind a socket to internet domain privileged ports (port numbers less than 1024).', - SYS_CHROOT: 'Use chroot(2), change root directory.', - SETFCAP: 'Set file capabilities.', - SYS_MODULE: 'Load and unload kernel modules.', - SYS_RAWIO: 'Perform I/O port operations (iopl(2) and ioperm(2)).', - SYS_PACCT: 'Use acct(2), switch process accounting on or off.', - SYS_ADMIN: 'Perform a range of system administration operations.', - SYS_NICE: 'Raise process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes.', - SYS_RESOURCE: 'Override resource Limits.', - SYS_TIME: 'Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock.', - SYS_TTY_CONFIG: 'Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals.', - AUDIT_CONTROL: 'Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules.', - MAC_ADMIN: 'Allow MAC configuration or state changes. Implemented for the Smack LSM.', - MAC_OVERRIDE: 'Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM).', - NET_ADMIN: 'Perform various network-related operations.', - SYSLOG: 'Perform privileged syslog(2) operations.', - DAC_READ_SEARCH: 'Bypass file read permission checks and directory read and execute permission checks.', - LINUX_IMMUTABLE: 'Set the FS_APPEND_FL and FS_IMMUTABLE_FL i-node flags.', - NET_BROADCAST: 'Make socket broadcasts, and listen to multicasts.', - IPC_LOCK: 'Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2)).', - IPC_OWNER: 'Bypass permission checks for operations on System V IPC objects.', - SYS_PTRACE: 'Trace arbitrary processes using ptrace(2).', - SYS_BOOT: 'Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution.', - LEASE: 'Establish leases on arbitrary files (see fcntl(2)).', - WAKE_ALARM: 'Trigger something that will wake up the system.', - BLOCK_SUSPEND: 'Employ features that can block system suspend.', -}; - -export function ContainerCapabilities() { - // all capabilities can be found at https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities - return [ - new ContainerCapability('SETPCAP', true), - new ContainerCapability('MKNOD', true), - new ContainerCapability('AUDIT_WRITE', true), - new ContainerCapability('CHOWN', true), - new ContainerCapability('NET_RAW', true), - new ContainerCapability('DAC_OVERRIDE', true), - new ContainerCapability('FOWNER', true), - new ContainerCapability('FSETID', true), - new ContainerCapability('KILL', true), - new ContainerCapability('SETGID', true), - new ContainerCapability('SETUID', true), - new ContainerCapability('NET_BIND_SERVICE', true), - new ContainerCapability('SYS_CHROOT', true), - new ContainerCapability('SETFCAP', true), - new ContainerCapability('SYS_MODULE', false), - new ContainerCapability('SYS_RAWIO', false), - new ContainerCapability('SYS_PACCT', false), - new ContainerCapability('SYS_ADMIN', false), - new ContainerCapability('SYS_NICE', false), - new ContainerCapability('SYS_RESOURCE', false), - new ContainerCapability('SYS_TIME', false), - new ContainerCapability('SYS_TTY_CONFIG', false), - new ContainerCapability('AUDIT_CONTROL', false), - new ContainerCapability('MAC_ADMIN', false), - new ContainerCapability('MAC_OVERRIDE', false), - new ContainerCapability('NET_ADMIN', false), - new ContainerCapability('SYSLOG', false), - new ContainerCapability('DAC_READ_SEARCH', false), - new ContainerCapability('LINUX_IMMUTABLE', false), - new ContainerCapability('NET_BROADCAST', false), - new ContainerCapability('IPC_LOCK', false), - new ContainerCapability('IPC_OWNER', false), - new ContainerCapability('SYS_PTRACE', false), - new ContainerCapability('SYS_BOOT', false), - new ContainerCapability('LEASE', false), - new ContainerCapability('WAKE_ALARM', false), - new ContainerCapability('BLOCK_SUSPEND', false), - ].sort(function (a, b) { - return a.capability < b.capability ? -1 : 1; - }); -} - -export function ContainerCapability(cap, allowed) { - this.capability = cap; - this.allowed = allowed; - this.description = capDesc[cap]; -} diff --git a/app/docker/react/components/containers.ts b/app/docker/react/components/containers.ts index 1f573b4eb..d562205ce 100644 --- a/app/docker/react/components/containers.ts +++ b/app/docker/react/components/containers.ts @@ -30,6 +30,10 @@ import { resourcesTabUtils, type ResourcesTabValues, } from '@/react/docker/containers/CreateView/ResourcesTab'; +import { + CapabilitiesTab, + capabilitiesTabUtils, +} from '@/react/docker/containers/CreateView/CapabilitiesTab'; const ngModule = angular .module('portainer.docker.react.components.containers', []) @@ -91,3 +95,11 @@ withFormValidation, ResourcesTabValues>( ], resourcesTabUtils.validation ); + +withFormValidation( + ngModule, + CapabilitiesTab, + 'dockerCreateContainerCapabilitiesTab', + [], + capabilitiesTabUtils.validation +); diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 14b7a0092..d17067cef 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -9,7 +9,7 @@ import { buildConfirmButton } from '@@/modals/utils'; import { commandsTabUtils } from '@/react/docker/containers/CreateView/CommandsTab'; import { volumesTabUtils } from '@/react/docker/containers/CreateView/VolumesTab'; import { networkTabUtils } from '@/react/docker/containers/CreateView/NetworkTab'; -import { ContainerCapabilities, ContainerCapability } from '@/docker/models/containerCapabilities'; +import { capabilitiesTabUtils } from '@/react/docker/containers/CreateView/CapabilitiesTab'; import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel'; import { ContainerDetailsViewModel } from '@/docker/models/container'; @@ -85,13 +85,13 @@ angular.module('portainer.docker').controller('CreateContainerController', [ DnsSecondary: '', AccessControlData: new AccessControlFormData(), NodeName: null, - capabilities: [], RegistryModel: new PorImageRegistryModel(), commands: commandsTabUtils.getDefaultViewModel(), envVars: envVarsTabUtils.getDefaultViewModel(), volumes: volumesTabUtils.getDefaultViewModel(), network: networkTabUtils.getDefaultViewModel(), resources: resourcesTabUtils.getDefaultViewModel(), + capabilities: capabilitiesTabUtils.getDefaultViewModel(), }; $scope.state = { @@ -140,6 +140,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [ }); }; + $scope.onCapabilitiesChange = function (capabilities) { + return $scope.$evalAsync(() => { + $scope.formValues.capabilities = capabilities; + }); + }; + function onAlwaysPullChange(checked) { return $scope.$evalAsync(() => { $scope.formValues.alwaysPull = checked; @@ -301,21 +307,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ config.Labels = labels; } - function prepareCapabilities(config) { - var allowed = $scope.formValues.capabilities.filter(function (item) { - return item.allowed === true; - }); - var notAllowed = $scope.formValues.capabilities.filter(function (item) { - return item.allowed === false; - }); - - var getCapName = function (item) { - return item.capability; - }; - config.HostConfig.CapAdd = allowed.map(getCapName); - config.HostConfig.CapDrop = notAllowed.map(getCapName); - } - function prepareConfiguration() { var config = angular.copy($scope.config); config = commandsTabUtils.toRequest(config, $scope.formValues.commands); @@ -323,11 +314,11 @@ angular.module('portainer.docker').controller('CreateContainerController', [ config = volumesTabUtils.toRequest(config, $scope.formValues.volumes); config = networkTabUtils.toRequest(config, $scope.formValues.network, $scope.fromContainer.Id); config = resourcesTabUtils.toRequest(config, $scope.formValues.resources); + config = capabilitiesTabUtils.toRequest(config, $scope.formValues.capabilities); prepareImageConfig(config); preparePortBindings(config); prepareLabels(config); - prepareCapabilities(config); return config; } @@ -354,35 +345,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ }); } - function loadFromContainerCapabilities(d) { - if (d.HostConfig.CapAdd) { - d.HostConfig.CapAdd.forEach(function (cap) { - $scope.formValues.capabilities.push(new ContainerCapability(cap, true)); - }); - } - if (d.HostConfig.CapDrop) { - d.HostConfig.CapDrop.forEach(function (cap) { - $scope.formValues.capabilities.push(new ContainerCapability(cap, false)); - }); - } - - function hasCapability(item) { - return item.capability === cap.capability; - } - - var capabilities = new ContainerCapabilities(); - for (var i = 0; i < capabilities.length; i++) { - var cap = capabilities[i]; - if (!_.find($scope.formValues.capabilities, hasCapability)) { - $scope.formValues.capabilities.push(cap); - } - } - - $scope.formValues.capabilities.sort(function (a, b) { - return a.capability < b.capability ? -1 : 1; - }); - } - function loadFromContainerSpec() { // Get container Container.get({ id: $transition$.params().from }) @@ -408,12 +370,11 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.formValues.volumes = volumesTabUtils.toViewModel(d); $scope.formValues.network = networkTabUtils.toViewModel(d, $scope.availableNetworks, $scope.runningContainers); $scope.formValues.resources = resourcesTabUtils.toViewModel(d); + $scope.formValues.capabilities = capabilitiesTabUtils.toViewModel(d); loadFromContainerPortBindings(d); loadFromContainerLabels(d); loadFromContainerImageConfig(d); - - loadFromContainerCapabilities(d); }) .then(() => { $scope.state.containerIsLoaded = true; @@ -456,7 +417,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [ } else { $scope.state.containerIsLoaded = true; $scope.fromContainer = {}; - $scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : []; + if ($scope.areContainerCapabilitiesEnabled) { + $scope.formValues.capabilities = capabilitiesTabUtils.getDefaultViewModel(); + } } }) .catch((e) => { diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index a5acb7786..5cbe87427 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -302,7 +302,7 @@
- +
diff --git a/app/react/docker/containers/CreateView/CapabilitiesTab/CapabilitiesTab.tsx b/app/react/docker/containers/CreateView/CapabilitiesTab/CapabilitiesTab.tsx new file mode 100644 index 000000000..cf8b0fc09 --- /dev/null +++ b/app/react/docker/containers/CreateView/CapabilitiesTab/CapabilitiesTab.tsx @@ -0,0 +1,39 @@ +import { FormSection } from '@@/form-components/FormSection'; +import { SwitchField } from '@@/form-components/SwitchField'; + +import { capabilities } from './types'; + +export type Values = string[]; + +export function CapabilitiesTab({ + values, + onChange, +}: { + values: Values; + onChange: (values: Values) => void; +}) { + return ( + +
+ {capabilities.map((cap) => ( +
+ { + if (value) { + onChange([...values, cap.key]); + } else { + onChange(values.filter((v) => v !== cap.key)); + } + }} + /> +
+ ))} +
+
+ ); +} diff --git a/app/react/docker/containers/CreateView/CapabilitiesTab/index.ts b/app/react/docker/containers/CreateView/CapabilitiesTab/index.ts new file mode 100644 index 000000000..8c70ca8a5 --- /dev/null +++ b/app/react/docker/containers/CreateView/CapabilitiesTab/index.ts @@ -0,0 +1,15 @@ +import { validation } from './validation'; +import { toViewModel, getDefaultViewModel } from './toViewModel'; +import { toRequest } from './toRequest'; + +export { + CapabilitiesTab, + type Values as CapabilitiesTabValues, +} from './CapabilitiesTab'; + +export const capabilitiesTabUtils = { + toRequest, + toViewModel, + validation, + getDefaultViewModel, +}; diff --git a/app/react/docker/containers/CreateView/CapabilitiesTab/toRequest.ts b/app/react/docker/containers/CreateView/CapabilitiesTab/toRequest.ts new file mode 100644 index 000000000..3db379c9f --- /dev/null +++ b/app/react/docker/containers/CreateView/CapabilitiesTab/toRequest.ts @@ -0,0 +1,20 @@ +import { CreateContainerRequest } from '@/react/docker/containers/CreateView/types'; + +import { capabilities } from './types'; +import { Values } from './CapabilitiesTab'; + +export function toRequest( + oldConfig: CreateContainerRequest, + values: Values +): CreateContainerRequest { + return { + ...oldConfig, + HostConfig: { + ...oldConfig.HostConfig, + CapAdd: values, + CapDrop: capabilities + .filter((cap) => !values.includes(cap.key)) + .map((cap) => cap.key), + }, + }; +} diff --git a/app/react/docker/containers/CreateView/CapabilitiesTab/toViewModel.ts b/app/react/docker/containers/CreateView/CapabilitiesTab/toViewModel.ts new file mode 100644 index 000000000..2f1fe3e4d --- /dev/null +++ b/app/react/docker/containers/CreateView/CapabilitiesTab/toViewModel.ts @@ -0,0 +1,28 @@ +import { ContainerJSON } from '@/react/docker/containers/queries/container'; + +import { capabilities } from './types'; +import { Values } from './CapabilitiesTab'; + +export function toViewModel(config: ContainerJSON): Values { + const { CapAdd, CapDrop } = getDefaults(config); + + const missingCaps = capabilities + .filter( + (cap) => + cap.default && !CapAdd.includes(cap.key) && !CapDrop.includes(cap.key) + ) + .map((cap) => cap.key); + + return [...CapAdd, ...missingCaps]; + + function getDefaults(config: ContainerJSON) { + return { + CapAdd: config.HostConfig?.CapAdd || [], + CapDrop: config.HostConfig?.CapDrop || [], + }; + } +} + +export function getDefaultViewModel(): Values { + return capabilities.filter((cap) => cap.default).map((cap) => cap.key); +} diff --git a/app/react/docker/containers/CreateView/CapabilitiesTab/types.ts b/app/react/docker/containers/CreateView/CapabilitiesTab/types.ts new file mode 100644 index 000000000..843c2278a --- /dev/null +++ b/app/react/docker/containers/CreateView/CapabilitiesTab/types.ts @@ -0,0 +1,185 @@ +export interface Capability { + key: string; + description: string; + default?: boolean; +} + +const capDesc: Array = [ + { + key: 'SETPCAP', + description: 'Modify process capabilities.', + default: true, + }, + { + key: 'MKNOD', + description: 'Create special files using mknod(2).', + default: true, + }, + { + key: 'AUDIT_WRITE', + description: 'Write records to kernel auditing log.', + default: true, + }, + { + key: 'CHOWN', + description: 'Make arbitrary changes to file UIDs and GIDs (see chown(2)).', + default: true, + }, + { + key: 'NET_RAW', + description: 'Use RAW and PACKET sockets.', + default: true, + }, + { + key: 'DAC_OVERRIDE', + description: 'Bypass file read, write, and execute permission checks.', + default: true, + }, + { + key: 'FOWNER', + description: + 'Bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file.', + default: true, + }, + { + key: 'FSETID', + description: + 'Don’t clear set-user-ID and set-group-ID permission bits when a file is modified.', + default: true, + }, + { + key: 'KILL', + description: 'Bypass permission checks for sending signals.', + default: true, + }, + { + key: 'SETGID', + description: + 'Make arbitrary manipulations of process GIDs and supplementary GID list.', + default: true, + }, + { + key: 'SETUID', + description: 'Make arbitrary manipulations of process UIDs.', + default: true, + }, + { + key: 'NET_BIND_SERVICE', + description: + 'Bind a socket to internet domain privileged ports (port numbers less than 1024).', + default: true, + }, + { + key: 'SYS_CHROOT', + description: 'Use chroot(2), change root directory.', + default: true, + }, + { + key: 'SETFCAP', + description: 'Set file capabilities.', + default: true, + }, + { + key: 'SYS_MODULE', + description: 'Load and unload kernel modules.', + }, + { + key: 'SYS_RAWIO', + description: 'Perform I/O port operations (iopl(2) and ioperm(2)).', + }, + { + key: 'SYS_PACCT', + description: 'Use acct(2), switch process accounting on or off.', + }, + { + key: 'SYS_ADMIN', + description: 'Perform a range of system administration operations.', + }, + { + key: 'SYS_NICE', + description: + 'Raise process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes.', + }, + { + key: 'SYS_RESOURCE', + description: 'Override resource Limits.', + }, + { + key: 'SYS_TIME', + description: + 'Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock.', + }, + { + key: 'SYS_TTY_CONFIG', + description: + 'Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals.', + }, + { + key: 'AUDIT_CONTROL', + description: + 'Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules.', + }, + { + key: 'MAC_ADMIN', + description: + 'Allow MAC configuration or state changes. Implemented for the Smack LSM.', + }, + { + key: 'MAC_OVERRIDE', + description: + 'Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM).', + }, + { + key: 'NET_ADMIN', + description: 'Perform various network-related operations.', + }, + { + key: 'SYSLOG', + description: 'Perform privileged syslog(2) operations.', + }, + { + key: 'DAC_READ_SEARCH', + description: + 'Bypass file read permission checks and directory read and execute permission checks.', + }, + { + key: 'LINUX_IMMUTABLE', + description: 'Set the FS_APPEND_FL and FS_IMMUTABLE_FL i-node flags.', + }, + { + key: 'NET_BROADCAST', + description: 'Make socket broadcasts, and listen to multicasts.', + }, + { + key: 'IPC_LOCK', + description: 'Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2)).', + }, + { + key: 'IPC_OWNER', + description: + 'Bypass permission checks for operations on System V IPC objects.', + }, + { + key: 'SYS_PTRACE', + description: 'Trace arbitrary processes using ptrace(2).', + }, + { + key: 'SYS_BOOT', + description: + 'Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution.', + }, + { + key: 'LEASE', + description: 'Establish leases on arbitrary files (see fcntl(2)).', + }, + { + key: 'WAKE_ALARM', + description: 'Trigger something that will wake up the system.', + }, + { + key: 'BLOCK_SUSPEND', + description: 'Employ features that can block system suspend.', + }, +]; + +export const capabilities = capDesc.sort((a, b) => (a.key < b.key ? -1 : 1)); diff --git a/app/react/docker/containers/CreateView/CapabilitiesTab/validation.ts b/app/react/docker/containers/CreateView/CapabilitiesTab/validation.ts new file mode 100644 index 000000000..3d679fc20 --- /dev/null +++ b/app/react/docker/containers/CreateView/CapabilitiesTab/validation.ts @@ -0,0 +1,7 @@ +import { array, SchemaOf, string } from 'yup'; + +import { Values } from './CapabilitiesTab'; + +export function validation(): SchemaOf { + return array(string().default('')).default([]); +}