From 808ceba8482189bdae1bf53ade1d812dd4cfa2b8 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 11 Mar 2026 08:56:42 +0200 Subject: [PATCH] feat(docker): allow user to specify security-opts (#2022) Co-authored-by: dylan Co-authored-by: jerry-yuan --- .../test_data/output_24_to_latest.json | 1 + .../endpoints/endpoint_settings_update.go | 10 +++- api/http/proxy/factory/docker/containers.go | 23 +++++--- api/portainer.go | 3 + api/stacks/stackutils/validation.go | 4 ++ ...ocker-features-configuration.controller.js | 8 ++- .../docker-features-configuration.html | 11 ++++ app/react-tools/test-mocks.ts | 1 + .../containers/CreateView/CreateInnerForm.tsx | 5 ++ .../CreateView/ResourcesTab/ResourcesTab.tsx | 11 ++++ .../ResourcesTab/SecurityOptField.tsx | 58 +++++++++++++++++++ .../CreateView/ResourcesTab/toRequest.ts | 1 + .../CreateView/ResourcesTab/toViewModel.ts | 2 + .../CreateView/ResourcesTab/validation.ts | 2 + .../ContainerActionsSection.test.tsx | 1 + .../SecondaryActions.test.tsx | 1 + .../useCanDuplicateEditContainer.test.tsx | 1 + .../useCanRecreateContainer.test.tsx | 1 + .../SecondaryActions/utils.ts | 1 + .../ContainerDetailsSection.tsx | 3 + .../SecurityOptRow.tsx | 25 ++++++++ .../ContainerStatusSection.test.tsx | 1 + app/react/portainer/environments/types.ts | 2 + 23 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 app/react/docker/containers/CreateView/ResourcesTab/SecurityOptField.tsx create mode 100644 app/react/docker/containers/ItemView/ContainerDetailsSection/SecurityOptRow.tsx diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index aa3390f97..0606a602a 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -89,6 +89,7 @@ "allowDeviceMappingForRegularUsers": true, "allowHostNamespaceForRegularUsers": true, "allowPrivilegedModeForRegularUsers": true, + "allowSecurityOptForRegularUsers": false, "allowStackManagementForRegularUsers": true, "allowSysctlSettingForRegularUsers": false, "allowVolumeBrowserForRegularUsers": false, diff --git a/api/http/handler/endpoints/endpoint_settings_update.go b/api/http/handler/endpoints/endpoint_settings_update.go index 91b668b0f..e960b1f05 100644 --- a/api/http/handler/endpoints/endpoint_settings_update.go +++ b/api/http/handler/endpoints/endpoint_settings_update.go @@ -26,6 +26,8 @@ type endpointSettingsUpdatePayload struct { AllowContainerCapabilitiesForRegularUsers *bool `json:"allowContainerCapabilitiesForRegularUsers" example:"true"` // Whether non-administrator should be able to use sysctl settings AllowSysctlSettingForRegularUsers *bool `json:"allowSysctlSettingForRegularUsers" example:"true"` + // Whether non-administrator should be able to use security-opt settings + AllowSecurityOptForRegularUsers *bool `json:"allowSecurityOptForRegularUsers" example:"true"` // Whether host management features are enabled EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures" example:"true"` @@ -111,6 +113,12 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures } + if payload.AllowSecurityOptForRegularUsers != nil { + securitySettings.AllowSecurityOptForRegularUsers = *payload.AllowSecurityOptForRegularUsers + } + + endpoint.SecuritySettings = securitySettings + if payload.EnableGPUManagement != nil { endpoint.EnableGPUManagement = *payload.EnableGPUManagement } @@ -119,8 +127,6 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re endpoint.Gpus = payload.Gpus } - endpoint.SecuritySettings = securitySettings - err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint) if err != nil { return httperror.InternalServerError("Failed persisting environment in database", err) diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index 765f5f47b..0c7788357 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -25,6 +25,7 @@ var ( ErrPIDHostNamespaceForbidden = errors.New("forbidden to use pid host namespace") ErrDeviceMappingForbidden = errors.New("forbidden to use device mapping") ErrSysCtlSettingsForbidden = errors.New("forbidden to use sysctl settings") + ErrSecurityOptSettingsForbidden = errors.New("forbidden to use security-opt settings") ErrContainerCapabilitiesForbidden = errors.New("forbidden to use container capabilities") ErrBindMountsForbidden = errors.New("forbidden to use bind mounts") ) @@ -90,7 +91,7 @@ func (transport *Transport) containerListOperation(response *http.Response, exec // containerInspectOperation extracts the response as a JSON object, verify that the user // has access to the container based on resource control and either rewrite an access denied response or a decorated container. func (transport *Transport) containerInspectOperation(response *http.Response, executor *operationExecutor) error { - //ContainerInspect response is a JSON object + // ContainerInspect response is a JSON object // https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect responseObject, err := utils.GetResponseAsJSONObject(response) if err != nil { @@ -116,6 +117,7 @@ func selectorContainerLabelsFromContainerInspectOperation(responseObject map[str containerConfigObject := utils.GetJSONObject(responseObject, "Config") if containerConfigObject != nil { containerLabelsObject := utils.GetJSONObject(containerConfigObject, "Labels") + return containerLabelsObject } @@ -170,13 +172,14 @@ func containerHasBlackListedLabel(containerLabels map[string]any, labelBlackList func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) { type PartialContainer struct { HostConfig struct { - Privileged bool `json:"Privileged"` - PidMode string `json:"PidMode"` - Devices []any `json:"Devices"` - Sysctls map[string]any `json:"Sysctls"` - CapAdd []string `json:"CapAdd"` - CapDrop []string `json:"CapDrop"` - Binds []string `json:"Binds"` + Privileged bool `json:"Privileged"` + PidMode string `json:"PidMode"` + Devices []any `json:"Devices"` + Sysctls map[string]any `json:"Sysctls"` + SecurityOpt []string `json:"SecurityOpt"` + CapAdd []string `json:"CapAdd"` + CapDrop []string `json:"CapDrop"` + Binds []string `json:"Binds"` } `json:"HostConfig"` } @@ -226,6 +229,10 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return forbiddenResponse, ErrSysCtlSettingsForbidden } + if !securitySettings.AllowSecurityOptForRegularUsers && len(partialContainer.HostConfig.SecurityOpt) > 0 { + return forbiddenResponse, ErrSecurityOptSettingsForbidden + } + if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { return nil, ErrContainerCapabilitiesForbidden } diff --git a/api/portainer.go b/api/portainer.go index 7b48dc2e6..8a2215781 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -645,6 +645,8 @@ type ( AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers" example:"true"` // Whether non-administrator should be able to use sysctl settings AllowSysctlSettingForRegularUsers bool `json:"allowSysctlSettingForRegularUsers" example:"true"` + // Whether non-administrator should be able to use security-opt settings + AllowSecurityOptForRegularUsers bool `json:"allowSecurityOptForRegularUsers" example:"true"` // Whether host management features are enabled EnableHostManagementFeatures bool `json:"enableHostManagementFeatures" example:"true"` } @@ -2477,6 +2479,7 @@ func DefaultEndpointSecuritySettings() EndpointSecuritySettings { AllowHostNamespaceForRegularUsers: false, AllowPrivilegedModeForRegularUsers: false, AllowSysctlSettingForRegularUsers: false, + AllowSecurityOptForRegularUsers: false, AllowVolumeBrowserForRegularUsers: false, EnableHostManagementFeatures: false, diff --git a/api/stacks/stackutils/validation.go b/api/stacks/stackutils/validation.go index 1c4c71162..d9f58a533 100644 --- a/api/stacks/stackutils/validation.go +++ b/api/stacks/stackutils/validation.go @@ -56,6 +56,10 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo return errors.New("sysctl setting disabled for non administrator users") } + if !securitySettings.AllowSecurityOptForRegularUsers && len(service.SecurityOpt) > 0 { + return errors.New("security-opt setting disabled for non administrator users") + } + if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) { return errors.New("container capabilities disabled for non administrator users") } diff --git a/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js b/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js index 9db6919d1..e339626d7 100644 --- a/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js +++ b/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js @@ -24,6 +24,7 @@ export default class DockerFeaturesConfigurationController { disableDeviceMappingForRegularUsers: false, disableContainerCapabilitiesForRegularUsers: false, disableSysctlSettingForRegularUsers: false, + disableSecurityOptForRegularUsers: false, }; this.isAgent = false; @@ -48,6 +49,7 @@ export default class DockerFeaturesConfigurationController { this.onChangeDisableDeviceMappingForRegularUsers = this.onChangeField('disableDeviceMappingForRegularUsers'); this.onChangeDisableContainerCapabilitiesForRegularUsers = this.onChangeField('disableContainerCapabilitiesForRegularUsers'); this.onChangeDisableSysctlSettingForRegularUsers = this.onChangeField('disableSysctlSettingForRegularUsers'); + this.onChangeDisableSecurityOptForRegularUsers = this.onChangeField('disableSecurityOptForRegularUsers'); } onToggleAutoUpdate(value) { @@ -93,6 +95,7 @@ export default class DockerFeaturesConfigurationController { disableDeviceMappingForRegularUsers, disableContainerCapabilitiesForRegularUsers, disableSysctlSettingForRegularUsers, + disableSecurityOptForRegularUsers, } = this.formValues; return ( disableBindMountsForRegularUsers || @@ -100,7 +103,8 @@ export default class DockerFeaturesConfigurationController { disablePrivilegedModeForRegularUsers || disableDeviceMappingForRegularUsers || disableContainerCapabilitiesForRegularUsers || - disableSysctlSettingForRegularUsers + disableSysctlSettingForRegularUsers || + disableSecurityOptForRegularUsers ); } @@ -122,6 +126,7 @@ export default class DockerFeaturesConfigurationController { allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers, allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers, allowSysctlSettingForRegularUsers: !this.formValues.disableSysctlSettingForRegularUsers, + allowSecurityOptForRegularUsers: !this.formValues.disableSecurityOptForRegularUsers, enableGPUManagement: this.state.enableGPUManagement, gpus, }; @@ -159,6 +164,7 @@ export default class DockerFeaturesConfigurationController { disableStackManagementForRegularUsers: !securitySettings.allowStackManagementForRegularUsers, disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers, disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers, + disableSecurityOptForRegularUsers: !securitySettings.allowSecurityOptForRegularUsers, }; // this.endpoint.Gpus could be null as it is Gpus: []Pair in the API diff --git a/app/docker/views/docker-features-configuration/docker-features-configuration.html b/app/docker/views/docker-features-configuration/docker-features-configuration.html index 8edd5843f..ac325b4bf 100644 --- a/app/docker/views/docker-features-configuration/docker-features-configuration.html +++ b/app/docker/views/docker-features-configuration/docker-features-configuration.html @@ -142,6 +142,17 @@ > +
+
+ +
+
diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts index 5b49a9217..6b98c729f 100644 --- a/app/react-tools/test-mocks.ts +++ b/app/react-tools/test-mocks.ts @@ -135,6 +135,7 @@ export function createMockEnvironment( allowHostNamespaceForRegularUsers: false, allowStackManagementForRegularUsers: false, allowSysctlSettingForRegularUsers: false, + allowSecurityOptForRegularUsers: false, allowVolumeBrowserForRegularUsers: false, enableHostManagementFeatures: false, }, diff --git a/app/react/docker/containers/CreateView/CreateInnerForm.tsx b/app/react/docker/containers/CreateView/CreateInnerForm.tsx index f5523a656..7f2a30fe4 100644 --- a/app/react/docker/containers/CreateView/CreateInnerForm.tsx +++ b/app/react/docker/containers/CreateView/CreateInnerForm.tsx @@ -176,6 +176,11 @@ export function CreateInnerForm({ environment.SecuritySettings .allowSysctlSettingForRegularUsers } + isSecurityOptFieldVisible={ + isEnvironmentAdminQuery.authorized || + environment.SecuritySettings + .allowSecurityOptForRegularUsers + } renderLimits={ isDuplicate ? (values) => ( diff --git a/app/react/docker/containers/CreateView/ResourcesTab/ResourcesTab.tsx b/app/react/docker/containers/CreateView/ResourcesTab/ResourcesTab.tsx index aae2b72d8..2ee3fb5a4 100644 --- a/app/react/docker/containers/CreateView/ResourcesTab/ResourcesTab.tsx +++ b/app/react/docker/containers/CreateView/ResourcesTab/ResourcesTab.tsx @@ -13,6 +13,7 @@ import { GpuFieldset, GpuFieldsetValues } from './GpuFieldset'; import { Values as RuntimeValues, RuntimeSection } from './RuntimeSection'; import { DevicesField, Values as Devices } from './DevicesField'; import { SysctlsField, Values as Sysctls } from './SysctlsField'; +import { SecurityOptField, Values as SecurityOpt } from './SecurityOptField'; import { ResourceFieldset, Values as ResourcesValues, @@ -24,6 +25,7 @@ export interface Values { devices: Devices; sysctls: Sysctls; + securityOpt: SecurityOpt; sharedMemorySize: number; @@ -40,6 +42,7 @@ export function ResourcesTab({ isInitFieldVisible, isDevicesFieldVisible, isSysctlFieldVisible, + isSecurityOptFieldVisible, renderLimits, }: { values: Values; @@ -49,6 +52,7 @@ export function ResourcesTab({ isInitFieldVisible: boolean; isDevicesFieldVisible: boolean; isSysctlFieldVisible: boolean; + isSecurityOptFieldVisible: boolean; renderLimits?: (values: ResourcesValues) => ReactNode; }) { const environmentId = useEnvironmentId(); @@ -88,6 +92,13 @@ export function ResourcesTab({ /> )} + {isSecurityOptFieldVisible && ( + setFieldValue('securityOpt', securityOpt)} + /> + )} +
; + +export function SecurityOptField({ + values, + onChange, + errors, +}: { + values: Values; + onChange: (value: Values) => void; + errors?: FormikErrors[]; +}) { + return ( + ''} + data-cy="docker-container-securityopts" + /> + ); +} + +function Item({ item, onChange, error, index }: ItemProps) { + return ( +
+
+ onChange(e.target.value)} + label="Security Option" + placeholder="e.g. seccomp=unconfined" + className="w-full" + size="small" + data-cy={`docker-container-securityopt-name_${index}`} + /> +
+ {error && ( + + {typeof error === 'string' ? error : Object.values(error)[0]} + + )} +
+ ); +} + +export function securityOptValidation(): SchemaOf { + return array(string().required('Security option is required')); +} diff --git a/app/react/docker/containers/CreateView/ResourcesTab/toRequest.ts b/app/react/docker/containers/CreateView/ResourcesTab/toRequest.ts index 11fcd2adc..2c6b59b5e 100644 --- a/app/react/docker/containers/CreateView/ResourcesTab/toRequest.ts +++ b/app/react/docker/containers/CreateView/ResourcesTab/toRequest.ts @@ -24,6 +24,7 @@ export function toRequest( Sysctls: Object.fromEntries( values.sysctls.map((sysctl) => [sysctl.name, sysctl.value]) ), + SecurityOpt: values.securityOpt, ShmSize: toConfigMemory(values.sharedMemorySize), DeviceRequests: gpuFieldsetUtils.toRequest( oldConfig.HostConfig.DeviceRequests || [], diff --git a/app/react/docker/containers/CreateView/ResourcesTab/toViewModel.ts b/app/react/docker/containers/CreateView/ResourcesTab/toViewModel.ts index 9f15d5e63..c512890fa 100644 --- a/app/react/docker/containers/CreateView/ResourcesTab/toViewModel.ts +++ b/app/react/docker/containers/CreateView/ResourcesTab/toViewModel.ts @@ -19,6 +19,7 @@ export function toViewModel(config: ContainerDetailsJSON): Values { value, }) ), + securityOpt: config.HostConfig?.SecurityOpt || [], gpu: gpuFieldsetUtils.toViewModel(config.HostConfig?.DeviceRequests || []), sharedMemorySize: toViewModelMemory(config.HostConfig?.ShmSize), resources: { @@ -38,6 +39,7 @@ export function getDefaultViewModel(): Values { }, devices: [], sysctls: [], + securityOpt: [], sharedMemorySize: 64, gpu: gpuFieldsetUtils.getDefaultViewModel(), resources: { diff --git a/app/react/docker/containers/CreateView/ResourcesTab/validation.ts b/app/react/docker/containers/CreateView/ResourcesTab/validation.ts index 7a0547e73..2255281fd 100644 --- a/app/react/docker/containers/CreateView/ResourcesTab/validation.ts +++ b/app/react/docker/containers/CreateView/ResourcesTab/validation.ts @@ -6,6 +6,7 @@ import { resourcesValidation } from './ResourcesFieldset'; import { Values } from './ResourcesTab'; import { runtimeValidation } from './RuntimeSection'; import { sysctlsValidation } from './SysctlsField'; +import { securityOptValidation } from './SecurityOptField'; export function validation({ maxMemory, @@ -18,6 +19,7 @@ export function validation({ runtime: runtimeValidation(), devices: devicesValidation(), sysctls: sysctlsValidation(), + securityOpt: securityOptValidation(), sharedMemorySize: number().min(0).default(0), gpu: gpuFieldsetUtils.validation(), resources: resourcesValidation({ maxMemory, maxCpu }), diff --git a/app/react/docker/containers/ItemView/ContainerActionsSection/ContainerActionsSection.test.tsx b/app/react/docker/containers/ItemView/ContainerActionsSection/ContainerActionsSection.test.tsx index 16569f1c9..4ea0b39b7 100644 --- a/app/react/docker/containers/ItemView/ContainerActionsSection/ContainerActionsSection.test.tsx +++ b/app/react/docker/containers/ItemView/ContainerActionsSection/ContainerActionsSection.test.tsx @@ -93,6 +93,7 @@ describe('ContainerActionsSection', () => { allowPrivilegedModeForRegularUsers: true, allowVolumeBrowserForRegularUsers: true, allowStackManagementForRegularUsers: true, + allowSecurityOptForRegularUsers: true, enableHostManagementFeatures: false, }, }) diff --git a/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions.test.tsx b/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions.test.tsx index ee68cf49b..f65da7413 100644 --- a/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions.test.tsx +++ b/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions.test.tsx @@ -76,6 +76,7 @@ describe('SecondaryActions', () => { allowPrivilegedModeForRegularUsers: true, allowVolumeBrowserForRegularUsers: true, allowStackManagementForRegularUsers: true, + allowSecurityOptForRegularUsers: true, enableHostManagementFeatures: false, }, }) diff --git a/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/useCanDuplicateEditContainer.test.tsx b/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/useCanDuplicateEditContainer.test.tsx index 1fc805e24..5029838ee 100644 --- a/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/useCanDuplicateEditContainer.test.tsx +++ b/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/useCanDuplicateEditContainer.test.tsx @@ -33,6 +33,7 @@ describe('useCanDuplicateEditContainer', () => { allowBindMountsForRegularUsers: true, allowDeviceMappingForRegularUsers: true, allowSysctlSettingForRegularUsers: true, + allowSecurityOptForRegularUsers: true, allowHostNamespaceForRegularUsers: true, allowPrivilegedModeForRegularUsers: true, allowVolumeBrowserForRegularUsers: true, diff --git a/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/useCanRecreateContainer.test.tsx b/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/useCanRecreateContainer.test.tsx index 91d4c1d6d..1cb414bf4 100644 --- a/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/useCanRecreateContainer.test.tsx +++ b/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/useCanRecreateContainer.test.tsx @@ -33,6 +33,7 @@ describe('useCanRecreateContainer', () => { allowBindMountsForRegularUsers: true, allowDeviceMappingForRegularUsers: true, allowSysctlSettingForRegularUsers: true, + allowSecurityOptForRegularUsers: true, allowHostNamespaceForRegularUsers: true, allowPrivilegedModeForRegularUsers: true, allowVolumeBrowserForRegularUsers: true, diff --git a/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/utils.ts b/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/utils.ts index 2df008fd9..c49149f17 100644 --- a/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/utils.ts +++ b/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/utils.ts @@ -11,6 +11,7 @@ export function isRegularUserRestricted( !securitySettings.allowBindMountsForRegularUsers || !securitySettings.allowDeviceMappingForRegularUsers || !securitySettings.allowSysctlSettingForRegularUsers || + !securitySettings.allowSecurityOptForRegularUsers || !securitySettings.allowHostNamespaceForRegularUsers || !securitySettings.allowPrivilegedModeForRegularUsers ); diff --git a/app/react/docker/containers/ItemView/ContainerDetailsSection/ContainerDetailsSection.tsx b/app/react/docker/containers/ItemView/ContainerDetailsSection/ContainerDetailsSection.tsx index 98f47928a..6b13966ac 100644 --- a/app/react/docker/containers/ItemView/ContainerDetailsSection/ContainerDetailsSection.tsx +++ b/app/react/docker/containers/ItemView/ContainerDetailsSection/ContainerDetailsSection.tsx @@ -15,6 +15,7 @@ import { PortConfigurationRow } from './PortConfigurationRow'; import { EnvironmentVariablesRow } from './EnvironmentVariablesRow'; import { LabelsRow } from './LabelsRow'; import { SysctlsRow } from './SysctlsRow'; +import { SecurityOptRow } from './SecurityOptRow'; import { GpuRow } from './GpuRow'; interface Props { @@ -81,6 +82,8 @@ export function ContainerDetailsSection({ + + diff --git a/app/react/docker/containers/ItemView/ContainerDetailsSection/SecurityOptRow.tsx b/app/react/docker/containers/ItemView/ContainerDetailsSection/SecurityOptRow.tsx new file mode 100644 index 000000000..736db0255 --- /dev/null +++ b/app/react/docker/containers/ItemView/ContainerDetailsSection/SecurityOptRow.tsx @@ -0,0 +1,25 @@ +import { DetailsTable } from '@@/DetailsTable'; + +interface SecurityOptRowProps { + securityOpts?: Array; +} + +export function SecurityOptRow({ securityOpts }: SecurityOptRowProps) { + if (!securityOpts || securityOpts.length === 0) { + return null; + } + + return ( + + + + {securityOpts.map((value, index) => ( + + + + ))} + +
{value}
+
+ ); +} diff --git a/app/react/docker/containers/ItemView/ContainerStatusSection/ContainerStatusSection.test.tsx b/app/react/docker/containers/ItemView/ContainerStatusSection/ContainerStatusSection.test.tsx index d2ff0101c..272acfe77 100644 --- a/app/react/docker/containers/ItemView/ContainerStatusSection/ContainerStatusSection.test.tsx +++ b/app/react/docker/containers/ItemView/ContainerStatusSection/ContainerStatusSection.test.tsx @@ -39,6 +39,7 @@ describe('ContainerStatusSection', () => { allowPrivilegedModeForRegularUsers: true, allowStackManagementForRegularUsers: true, allowVolumeBrowserForRegularUsers: true, + allowSecurityOptForRegularUsers: true, enableHostManagementFeatures: true, }, }) diff --git a/app/react/portainer/environments/types.ts b/app/react/portainer/environments/types.ts index 47dbd068e..57895fa3e 100644 --- a/app/react/portainer/environments/types.ts +++ b/app/react/portainer/environments/types.ts @@ -119,6 +119,8 @@ export interface EnvironmentSecuritySettings { allowContainerCapabilitiesForRegularUsers: boolean; // Whether non-administrator should be able to use sysctl settings allowSysctlSettingForRegularUsers: boolean; + // Whether non-administrator should be able to use security-opt settings + allowSecurityOptForRegularUsers: boolean; // Whether host management features are enabled enableHostManagementFeatures: boolean; }