1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 15:25:22 +02:00

feat(endpoint): relocate docker security settings (#4657)

* feat(endpoint): migrate security settings to endpoint

* feat(endpoint): check for specific endpoint settings

* feat(endpoint): check security settings

* feat(docker): add config page

* feat(endpoint): save settings page

* feat(endpoints): disable features when not agent

* feat(sidebar): hide docker settings for regular user

* fix(docker): small fixes in configs

* fix(volumes): hide browse button for non admins

* refactor(docker): introduce switch component

* refactor(components/switch): seprate label from switch

* feat(app/components): align switch label

* refactor(app/components): move switch css

* fix(docker/settings): add ngijnect

* feat(endpoints): set default security values

* style(portainer): sort types

* fix(endpoint): rename security heading

* fix(endpoints): update endpoints settings
This commit is contained in:
Chaim Lev-Ari 2021-02-09 10:09:06 +02:00 committed by GitHub
parent e401724d43
commit 46dec01fe3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 714 additions and 461 deletions

View file

@ -0,0 +1,52 @@
/* switch box */
.switch {
--switch-size: 24px;
}
.switch.small {
--switch-size: 12px;
}
.switch input {
display: none;
}
.switch i,
.bootbox-form .checkbox i {
display: inline-block;
vertical-align: middle;
cursor: pointer;
padding-right: var(--switch-size);
transition: all ease 0.2s;
-webkit-transition: all ease 0.2s;
-moz-transition: all ease 0.2s;
-o-transition: all ease 0.2s;
border-radius: var(--switch-size);
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5);
}
.switch i:before,
.bootbox-form .checkbox i:before {
display: block;
content: '';
width: var(--switch-size);
height: var(--switch-size);
border-radius: var(--switch-size);
background: white;
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5);
}
.switch :checked + i,
.bootbox-form .checkbox :checked ~ i {
padding-right: 0;
padding-left: var(--switch-size);
-webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
-moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
}
.switch :disabled + i {
opacity: 0.5;
cursor: not-allowed;
}

View file

@ -0,0 +1,14 @@
<label style="display: flex; align-items: center; margin: 0;">
<span for="toggle_{{::$ctrl.name}}" class="control-label text-left space-right" ng-class="$ctrl.labelClass" style="padding: 0;">
{{::$ctrl.label}}
<portainer-tooltip ng-if="$ctrl.tooltip" position="bottom" message="{{::$ctrl.tooltip}}"></portainer-tooltip>
</span>
<por-switch
class-name="space-right"
name="toggle_{{::$ctrl.name}}"
id="toggle_{{::$ctrl.name}}"
ng-model="$ctrl.ngModel"
disabled="$ctrl.disabled"
on-change="($ctrl.onChange)"
></por-switch>
</label>

View file

@ -0,0 +1,18 @@
import angular from 'angular';
import './por-switch-field.css';
export const porSwitchField = {
templateUrl: './por-switch-field.html',
bindings: {
tooltip: '@',
ngModel: '=',
label: '@',
name: '@',
labelClass: '@',
disabled: '<',
onChange: '<',
},
};
angular.module('portainer.app').component('porSwitchField', porSwitchField);

View file

@ -0,0 +1,3 @@
<label class="switch" ng-class="$ctrl.className" style="margin-bottom: 0;">
<input type="checkbox" name="{{::$ctrl.name}}" id="{{::$ctrl.id}}" ng-model="$ctrl.ngModel" ng-disabled="$ctrl.disabled" ng-change="$ctrl.onChange($ctrl.ngModel)" /><i></i>
</label>

View file

@ -0,0 +1,15 @@
import angular from 'angular';
const porSwitch = {
templateUrl: './por-switch.html',
bindings: {
ngModel: '=',
id: '@',
className: '@',
name: '@',
disabled: '<',
onChange: '<',
},
};
angular.module('portainer.app').component('porSwitch', porSwitch);

View file

@ -4,16 +4,8 @@ export function SettingsViewModel(data) {
this.AuthenticationMethod = data.AuthenticationMethod;
this.LDAPSettings = data.LDAPSettings;
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;
this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers;
this.AllowVolumeBrowserForRegularUsers = data.AllowVolumeBrowserForRegularUsers;
this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers;
this.AllowDeviceMappingForRegularUsers = data.AllowDeviceMappingForRegularUsers;
this.AllowStackManagementForRegularUsers = data.AllowStackManagementForRegularUsers;
this.AllowContainerCapabilitiesForRegularUsers = data.AllowContainerCapabilitiesForRegularUsers;
this.SnapshotInterval = data.SnapshotInterval;
this.TemplatesURL = data.TemplatesURL;
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures;
this.UserSessionTimeout = data.UserSessionTimeout;
@ -21,15 +13,7 @@ export function SettingsViewModel(data) {
}
export function PublicSettingsViewModel(settings) {
this.AllowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers;
this.AllowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers;
this.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers;
this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers;
this.AllowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers;
this.AllowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers;
this.AllowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers;
this.AuthenticationMethod = settings.AuthenticationMethod;
this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures;
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
this.LogoURL = settings.LogoURL;
this.OAuthLoginURI = settings.OAuthLoginURI;

View file

@ -21,6 +21,7 @@ angular.module('portainer.app').factory('Endpoints', [
snapshots: { method: 'POST', params: { action: 'snapshot' } },
snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' } },
status: { method: 'GET', params: { id: '@id', action: 'status' } },
updateSecuritySettings: { method: 'PUT', params: { id: '@id', action: 'settings' } },
}
);
},

View file

@ -6,7 +6,9 @@ angular.module('portainer.app').factory('EndpointService', [
'FileUploadService',
function EndpointServiceFactory($q, Endpoints, FileUploadService) {
'use strict';
var service = {};
var service = {
updateSecuritySettings,
};
service.endpoint = function (endpointID) {
return Endpoints.get({ id: endpointID }).$promise;
@ -146,5 +148,9 @@ angular.module('portainer.app').factory('EndpointService', [
};
return service;
function updateSecuritySettings(id, securitySettings) {
return Endpoints.updateSecuritySettings({ id }, securitySettings).$promise;
}
},
]);

View file

@ -72,51 +72,11 @@ angular.module('portainer.app').factory('StateManager', [
LocalStorage.storeApplicationState(state.application);
};
manager.updateEnableHostManagementFeatures = function (enableHostManagementFeatures) {
state.application.enableHostManagementFeatures = enableHostManagementFeatures;
LocalStorage.storeApplicationState(state.application);
};
manager.updateEnableVolumeBrowserForNonAdminUsers = function (enableVolumeBrowserForNonAdminUsers) {
state.application.enableVolumeBrowserForNonAdminUsers = enableVolumeBrowserForNonAdminUsers;
LocalStorage.storeApplicationState(state.application);
};
manager.updateEnableEdgeComputeFeatures = function updateEnableEdgeComputeFeatures(enableEdgeComputeFeatures) {
state.application.enableEdgeComputeFeatures = enableEdgeComputeFeatures;
LocalStorage.storeApplicationState(state.application);
};
manager.updateAllowHostNamespaceForRegularUsers = function (allowHostNamespaceForRegularUsers) {
state.application.allowHostNamespaceForRegularUsers = allowHostNamespaceForRegularUsers;
LocalStorage.storeApplicationState(state.application);
};
manager.updateAllowDeviceMappingForRegularUsers = function updateAllowDeviceMappingForRegularUsers(allowDeviceMappingForRegularUsers) {
state.application.allowDeviceMappingForRegularUsers = allowDeviceMappingForRegularUsers;
LocalStorage.storeApplicationState(state.application);
};
manager.updateAllowStackManagementForRegularUsers = function updateAllowStackManagementForRegularUsers(allowStackManagementForRegularUsers) {
state.application.allowStackManagementForRegularUsers = allowStackManagementForRegularUsers;
LocalStorage.storeApplicationState(state.application);
};
manager.updateAllowContainerCapabilitiesForRegularUsers = function updateAllowContainerCapabilitiesForRegularUsers(allowContainerCapabilitiesForRegularUsers) {
state.application.allowContainerCapabilitiesForRegularUsers = allowContainerCapabilitiesForRegularUsers;
LocalStorage.storeApplicationState(state.application);
};
manager.updateAllowBindMountsForRegularUsers = function updateAllowBindMountsForRegularUsers(allowBindMountsForRegularUsers) {
state.application.allowBindMountsForRegularUsers = allowBindMountsForRegularUsers;
LocalStorage.storeApplicationState(state.application);
};
manager.updateAllowPrivilegedModeForRegularUsers = function (AllowPrivilegedModeForRegularUsers) {
state.application.allowPrivilegedModeForRegularUsers = AllowPrivilegedModeForRegularUsers;
LocalStorage.storeApplicationState(state.application);
};
manager.updateEnableTelemetry = function updateEnableTelemetry(enableTelemetry) {
state.application.enableTelemetry = enableTelemetry;
$analytics.setOptOut(!enableTelemetry);
@ -128,15 +88,7 @@ angular.module('portainer.app').factory('StateManager', [
state.application.enableTelemetry = settings.EnableTelemetry;
state.application.logo = settings.LogoURL;
state.application.snapshotInterval = settings.SnapshotInterval;
state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers;
state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
state.application.allowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers;
state.application.allowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers;
state.application.allowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers;
state.application.allowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers;
state.application.allowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers;
state.application.allowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers;
state.application.validity = moment().unix();
}

View file

@ -76,100 +76,7 @@
</div>
<!-- !templates -->
<!-- host-filesystem -->
<div class="col-sm-12 form-section-title">
Host and Filesystem
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_enableHostManagementFeatures" class="control-label text-left">
Enable host management features
<portainer-tooltip position="bottom" message="Enable host management features: host system browsing and advanced host details."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_enableHostManagementFeatures" ng-model="formValues.enableHostManagementFeatures" /><i></i>
</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_allowvolumebrowser" class="control-label text-left">
Enable volume management for non-administrators
<portainer-tooltip position="bottom" message="When enabled, regular users will be able to use Portainer volume management features."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_allowvolumebrowser" ng-model="formValues.enableVolumeBrowser" /><i></i> </label>
</div>
</div>
<!-- !host-filesystem -->
<!-- security -->
<div class="col-sm-12 form-section-title">
Docker Endpoint Security Options
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_allowbindmounts" class="control-label text-left">
Disable bind mounts for non-administrators
<portainer-tooltip position="bottom" message="When enabled, regular users will not be able to use bind mounts when creating containers."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_allowbindmounts" ng-model="formValues.restrictBindMounts" /><i></i> </label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_allowbindmounts" class="control-label text-left">
Disable privileged mode for non-administrators
<portainer-tooltip position="bottom" message="When enabled, regular users will not be able to use privileged mode when creating containers."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_allowbindmounts" ng-model="formValues.restrictPrivilegedMode" /><i></i> </label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_allowHostNamespaceForRegularUsers" class="control-label text-left">
Disable the use of host PID 1 for non-administrators
<portainer-tooltip position="bottom" message="Prevent users from accessing the host filesystem through the host PID namespace."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_allowHostNamespaceForRegularUsers" ng-model="formValues.restrictHostNamespaceForRegularUsers" /><i></i>
</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_disableStackManagementForRegularUsers" class="control-label text-left">
Disable the use of Stacks for non-administrators
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_disableStackManagementForRegularUsers" ng-model="formValues.disableStackManagementForRegularUsers" /><i></i>
</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_disableDeviceMappingForRegularUsers" class="control-label text-left">
Disable device mappings for non-administrators
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_disableDeviceMappingForRegularUsers" ng-model="formValues.disableDeviceMappingForRegularUsers" /><i></i>
</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_disableContainerCapabilitiesForRegularUsers" class="control-label text-left">
Disable container capabilities for non-administrators
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_disableContainerCapabilitiesForRegularUsers" ng-model="formValues.disableContainerCapabilitiesForRegularUsers" /><i></i>
</label>
</div>
</div>
<div class="form-group" ng-if="isContainerEditDisabled()">
<span class="col-sm-12 text-muted small">
Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings.
</span>
</div>
<!-- !security -->
<!-- edge -->
<div class="col-sm-12 form-section-title">
Edge Compute

View file

@ -25,33 +25,12 @@ angular.module('portainer.app').controller('SettingsController', [
$scope.formValues = {
customLogo: false,
restrictBindMounts: false,
restrictPrivilegedMode: false,
labelName: '',
labelValue: '',
enableHostManagementFeatures: false,
enableVolumeBrowser: false,
enableEdgeComputeFeatures: false,
restrictHostNamespaceForRegularUsers: false,
allowDeviceMappingForRegularUsers: false,
allowStackManagementForRegularUsers: false,
disableContainerCapabilitiesForRegularUsers: false,
enableTelemetry: false,
};
$scope.isContainerEditDisabled = function isContainerEditDisabled() {
const {
restrictBindMounts,
restrictHostNamespaceForRegularUsers,
restrictPrivilegedMode,
disableDeviceMappingForRegularUsers,
disableContainerCapabilitiesForRegularUsers,
} = this.formValues;
return (
restrictBindMounts || restrictHostNamespaceForRegularUsers || restrictPrivilegedMode || disableDeviceMappingForRegularUsers || disableContainerCapabilitiesForRegularUsers
);
};
$scope.removeFilteredContainerLabel = function (index) {
var settings = $scope.settings;
settings.BlackListedLabels.splice(index, 1);
@ -77,15 +56,7 @@ angular.module('portainer.app').controller('SettingsController', [
settings.LogoURL = '';
}
settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts;
settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode;
settings.AllowVolumeBrowserForRegularUsers = $scope.formValues.enableVolumeBrowser;
settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures;
settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures;
settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers;
settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers;
settings.AllowStackManagementForRegularUsers = !$scope.formValues.disableStackManagementForRegularUsers;
settings.AllowContainerCapabilitiesForRegularUsers = !$scope.formValues.disableContainerCapabilitiesForRegularUsers;
settings.EnableTelemetry = $scope.formValues.enableTelemetry;
$scope.state.actionInProgress = true;
@ -98,15 +69,7 @@ angular.module('portainer.app').controller('SettingsController', [
Notifications.success('Settings updated');
StateManager.updateLogo(settings.LogoURL);
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
StateManager.updateEnableHostManagementFeatures(settings.EnableHostManagementFeatures);
StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers);
StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers);
StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures);
StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers);
StateManager.updateAllowStackManagementForRegularUsers(settings.AllowStackManagementForRegularUsers);
StateManager.updateAllowContainerCapabilitiesForRegularUsers(settings.AllowContainerCapabilitiesForRegularUsers);
StateManager.updateAllowPrivilegedModeForRegularUsers(settings.AllowPrivilegedModeForRegularUsers);
StateManager.updateAllowBindMountsForRegularUsers(settings.AllowBindMountsForRegularUsers);
StateManager.updateEnableTelemetry(settings.EnableTelemetry);
$state.reload();
})
@ -127,15 +90,7 @@ angular.module('portainer.app').controller('SettingsController', [
if (settings.LogoURL !== '') {
$scope.formValues.customLogo = true;
}
$scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers;
$scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers;
$scope.formValues.enableVolumeBrowser = settings.AllowVolumeBrowserForRegularUsers;
$scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
$scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
$scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers;
$scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers;
$scope.formValues.disableStackManagementForRegularUsers = !settings.AllowStackManagementForRegularUsers;
$scope.formValues.disableContainerCapabilitiesForRegularUsers = !settings.AllowContainerCapabilitiesForRegularUsers;
$scope.formValues.enableTelemetry = settings.EnableTelemetry;
})
.catch(function error(err) {

View file

@ -46,9 +46,17 @@ angular.module('portainer.app').controller('SidebarController', [
async function shouldShowStacks() {
const isAdmin = Authentication.isAdmin();
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
return isAdmin || allowStackManagementForRegularUsers;
if (isAdmin) {
return true;
}
const endpoint = EndpointProvider.currentEndpoint();
if (!endpoint || !endpoint.SecuritySettings) {
return false;
}
return endpoint.SecuritySettings.allowStackManagementForRegularUsers;
}
$transitions.onEnter({}, async () => {

View file

@ -1,7 +1,7 @@
angular.module('portainer.app').controller('StacksController', StacksController);
/* @ngInject */
function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, StateManager) {
function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, endpoint) {
$scope.removeAction = function (selectedItems) {
ModalService.confirmDeletion('Do you want to remove the selected stack(s)? Associated services will be removed as well.', function onConfirm(confirmed) {
if (!confirmed) {
@ -55,8 +55,7 @@ function StacksController($scope, $state, Notifications, StackService, ModalServ
}
async function loadCreateEnabled() {
const appState = StateManager.getState().application;
return appState.allowStackManagementForRegularUsers || Authentication.isAdmin();
return endpoint.SecuritySettings.allowStackManagementForRegularUsers || Authentication.isAdmin();
}
async function initView() {

View file

@ -16,8 +16,8 @@ angular.module('portainer.app').controller('TemplatesController', [
'ResourceControlService',
'Authentication',
'FormValidator',
'SettingsService',
'StackService',
'endpoint',
function (
$scope,
$q,
@ -33,8 +33,8 @@ angular.module('portainer.app').controller('TemplatesController', [
ResourceControlService,
Authentication,
FormValidator,
SettingsService,
StackService
StackService,
endpoint
) {
$scope.state = {
selectedTemplate: null,
@ -263,7 +263,6 @@ angular.module('portainer.app').controller('TemplatesController', [
false,
endpointMode.provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
),
settings: SettingsService.publicSettings(),
})
.then(function success(data) {
var templates = data.templates;
@ -271,8 +270,7 @@ angular.module('portainer.app').controller('TemplatesController', [
$scope.availableVolumes = _.orderBy(data.volumes.Volumes, [(volume) => volume.Name.toLowerCase()], ['asc']);
var networks = data.networks;
$scope.availableNetworks = networks;
var settings = data.settings;
$scope.allowBindMounts = settings.AllowBindMountsForRegularUsers;
$scope.allowBindMounts = endpoint.SecuritySettings.allowBindMountsForRegularUsers;
})
.catch(function error(err) {
$scope.templates = [];