refactor(containers): migrate caps tab to react [EE-5215] (#10366)

pull/10347/head
Chaim Lev-Ari 2023-09-25 19:36:50 +03:00 committed by GitHub
parent 9dde610da3
commit 57e04c3544
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 324 additions and 190 deletions

View File

@ -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

View File

@ -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)]));
}
}
}

View File

@ -1,9 +0,0 @@
import controller from './container-capabilities.controller';
angular.module('portainer.docker').component('containerCapabilities', {
templateUrl: './containerCapabilities.html',
bindings: {
capabilities: '=',
},
controller,
});

View File

@ -1,15 +0,0 @@
<form class="form-horizontal" style="margin-top: 15px">
<div class="col-sm-12 form-section-title"> Container capabilities </div>
<div class="form-group flex flex-wrap gap-y-2 px-5">
<div ng-repeat="cap in $ctrl.capabilities" class="w-1/3 text-center">
<por-switch-field
label-class="'col-sm-6'"
tooltip="cap.description"
checked="cap.allowed"
label="cap.capability"
name="'capability'"
on-change="($ctrl.capabilitiesOnChange[cap.capability])"
></por-switch-field>
</div>
</div>
</form>

View File

@ -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: 'Dont 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];
}

View File

@ -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<ComponentProps<typeof ResourcesTab>, ResourcesTabValues>(
],
resourcesTabUtils.validation
);
withFormValidation(
ngModule,
CapabilitiesTab,
'dockerCreateContainerCapabilitiesTab',
[],
capabilitiesTabUtils.validation
);

View File

@ -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) => {

View File

@ -302,7 +302,7 @@
<!-- !tab-runtime-resources -->
<!-- tab-container-capabilities -->
<div class="tab-pane" id="container-capabilities">
<container-capabilities capabilities="formValues.capabilities"></container-capabilities>
<docker-create-container-capabilities-tab values="formValues.capabilities" on-change="(onCapabilitiesChange)"></docker-create-container-capabilities-tab>
</div>
<!-- !tab-container-capabilities -->
</div>

View File

@ -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 (
<FormSection title="Container capabilities">
<div className="form-group flex flex-wrap gap-y-2 px-5">
{capabilities.map((cap) => (
<div key={cap.key} className="w-1/3 text-center">
<SwitchField
labelClass="col-sm-6"
tooltip={cap.description}
checked={values.includes(cap.key)}
label={cap.key}
name={`${cap.key}-capability`}
onChange={(value) => {
if (value) {
onChange([...values, cap.key]);
} else {
onChange(values.filter((v) => v !== cap.key));
}
}}
/>
</div>
))}
</div>
</FormSection>
);
}

View File

@ -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,
};

View File

@ -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),
},
};
}

View File

@ -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);
}

View File

@ -0,0 +1,185 @@
export interface Capability {
key: string;
description: string;
default?: boolean;
}
const capDesc: Array<Capability> = [
{
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:
'Dont 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));

View File

@ -0,0 +1,7 @@
import { array, SchemaOf, string } from 'yup';
import { Values } from './CapabilitiesTab';
export function validation(): SchemaOf<Values> {
return array(string().default('')).default([]);
}