From 5ebb03cb4e404560c0f3232e779c2f2e38d447a9 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 13 Jul 2020 07:32:56 +0300 Subject: [PATCH] feat(settings): add setting to disable device mapping for regular users (#4017) * feat(settings): introduce device mapping service * feat(containers): hide devices field when setting is on * feat(containers): prevent passing of devices when not allowed * feat(stacks): prevent non admin from device mapping * feat(stacks): disallow swarm stack creation for user * refactor(settings): replace disableDeviceMapping with allow * fix(stacks): remove check for disable device mappings from swarm * feat(settings): rename field to disable * feat(settings): supply default value for disableDeviceMapping * feat(container): check for endpoint admin --- api/bolt/migrator/migrate_dbversion22.go | 15 +++++++++++++- api/bolt/migrator/migrator.go | 5 +++++ api/cmd/portainer/main.go | 1 + api/http/handler/settings/settings_public.go | 2 ++ api/http/handler/settings/settings_update.go | 5 +++++ .../handler/stacks/create_compose_stack.go | 6 +++++- api/http/handler/stacks/create_swarm_stack.go | 1 + api/http/handler/stacks/stack_create.go | 4 ++++ api/http/proxy/factory/docker/containers.go | 17 ++++++++++++---- api/portainer.go | 1 + .../create/createContainerController.js | 20 +++++++++++++++++-- .../containers/create/createcontainer.html | 2 +- app/portainer/models/settings.js | 2 ++ app/portainer/services/stateManager.js | 6 ++++++ app/portainer/views/settings/settings.html | 11 ++++++++++ .../views/settings/settingsController.js | 4 ++++ 16 files changed, 93 insertions(+), 9 deletions(-) diff --git a/api/bolt/migrator/migrate_dbversion22.go b/api/bolt/migrator/migrate_dbversion22.go index 4a132c348..25ee3fc7f 100644 --- a/api/bolt/migrator/migrate_dbversion22.go +++ b/api/bolt/migrator/migrate_dbversion22.go @@ -1,6 +1,19 @@ package migrator -import "github.com/portainer/portainer/api" +import ( + "github.com/portainer/portainer/api" +) + +func (m *Migrator) updateSettingsToDBVersion23() error { + legacySettings, err := m.settingsService.Settings() + if err != nil { + return err + } + + legacySettings.AllowDeviceMappingForRegularUsers = true + + return m.settingsService.UpdateSettings(legacySettings) +} func (m *Migrator) updateTagsToDBVersion23() error { tags, err := m.tagService.Tags() diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 1ebd63389..c0dca0cfd 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -320,6 +320,11 @@ func (m *Migrator) Migrate() error { if err != nil { return err } + + err = m.updateSettingsToDBVersion23() + if err != nil { + return err + } } return m.versionService.StoreDBVersion(portainer.DBVersion) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 246faddbe..aa7a5bbc3 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -273,6 +273,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL AllowBindMountsForRegularUsers: true, AllowPrivilegedModeForRegularUsers: true, AllowVolumeBrowserForRegularUsers: false, + AllowDeviceMappingForRegularUsers: true, EnableHostManagementFeatures: false, AllowHostNamespaceForRegularUsers: true, SnapshotInterval: *flags.SnapshotInterval, diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 07f696a7f..b57ad8682 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -21,6 +21,7 @@ type publicSettingsResponse struct { OAuthLoginURI string `json:"OAuthLoginURI"` DisableStackManagementForRegularUsers bool `json:"DisableStackManagementForRegularUsers"` AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` } // GET request on /api/settings/public @@ -45,6 +46,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * settings.OAuthSettings.ClientID, settings.OAuthSettings.RedirectURI, settings.OAuthSettings.Scopes), + AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, DisableStackManagementForRegularUsers: settings.DisableStackManagementForRegularUsers, } diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 49d2098e0..7611fb7f6 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -27,6 +27,7 @@ type settingsUpdatePayload struct { EnableEdgeComputeFeatures *bool DisableStackManagementForRegularUsers *bool AllowHostNamespaceForRegularUsers *bool + AllowDeviceMappingForRegularUsers *bool } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -135,6 +136,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval } + if payload.AllowDeviceMappingForRegularUsers != nil { + settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers + } + tlsError := handler.updateTLS(settings) if tlsError != nil { return tlsError diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 153ee53aa..f5e424385 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -335,7 +335,11 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) return err } - if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers) && !isAdminOrEndpointAdmin { + if (!settings.AllowBindMountsForRegularUsers || + !settings.AllowPrivilegedModeForRegularUsers || + !settings.AllowHostNamespaceForRegularUsers || + !settings.AllowDeviceMappingForRegularUsers) && !isAdminOrEndpointAdmin { + composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) stackContent, err := handler.FileService.GetFileContent(composeFilePath) diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 08a566a4d..504437372 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -341,6 +341,7 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err } if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin { + composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) stackContent, err := handler.FileService.GetFileContent(composeFilePath) diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 88eacc148..e89708d5d 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -160,6 +160,10 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" { return errors.New("pid host disabled for non administrator users") } + + if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 { + return errors.New("device mapping disabled for non administrator users") + } } return nil diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index ac01bb453..bda4db4d6 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -156,8 +156,9 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB 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"` + Privileged bool `json:"Privileged"` + PidMode string `json:"PidMode"` + Devices []interface{} `json:"Devices"` } `json:"HostConfig"` } @@ -186,14 +187,18 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req endpointResourceAccess = true } - if (rbacExtension != nil && !endpointResourceAccess && tokenData.Role != portainer.AdministratorRole) || (rbacExtension == nil && tokenData.Role != portainer.AdministratorRole) { + isAdmin := (rbacExtension != nil && endpointResourceAccess) || tokenData.Role == portainer.AdministratorRole + if !isAdmin { settings, err := transport.settingsService.Settings() if err != nil { return nil, err } - if !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers { + if !settings.AllowPrivilegedModeForRegularUsers || + !settings.AllowHostNamespaceForRegularUsers || + !settings.AllowDeviceMappingForRegularUsers { + body, err := ioutil.ReadAll(request.Body) if err != nil { return nil, err @@ -213,6 +218,10 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return forbiddenResponse, errors.New("forbidden to use pid host namespace") } + if len(partialContainer.HostConfig.Devices) > 0 { + return nil, errors.New("forbidden to use device mapping") + } + request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) } } diff --git a/api/portainer.go b/api/portainer.go index 6c725f95f..882a69344 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -435,6 +435,7 @@ type ( EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` DisableStackManagementForRegularUsers bool `json:"DisableStackManagementForRegularUsers"` AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 4f33bf3dd..22889b928 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -30,6 +30,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ 'SettingsService', 'PluginService', 'HttpRequestHelper', + 'ExtensionService', function ( $q, $scope, @@ -55,7 +56,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [ SystemService, SettingsService, PluginService, - HttpRequestHelper + HttpRequestHelper, + ExtensionService ) { $scope.create = create; @@ -603,7 +605,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ }); } - function initView() { + async function initView() { var nodeName = $transition$.params().nodeName; $scope.formValues.NodeName = nodeName; HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); @@ -682,6 +684,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ }); $scope.isAdmin = Authentication.isAdmin(); + $scope.showDeviceMapping = await shouldShowDevices(); } function validateForm(accessControlData, isAdmin) { @@ -894,6 +897,19 @@ angular.module('portainer.docker').controller('CreateContainerController', [ } } + async function shouldShowDevices() { + const isAdmin = !$scope.applicationState.application.authentication || Authentication.isAdmin(); + const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application; + + if (isAdmin || allowDeviceMappingForRegularUsers) { + return true; + } + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + if (rbacEnabled) { + return Authentication.hasAuthorizations(['EndpointResourcesAccess']); + } + } + initView(); }, ]); diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index f339a9090..266ee7e40 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -629,7 +629,7 @@
-
+
diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index bce96696a..d78d1d7e8 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -15,6 +15,7 @@ export function SettingsViewModel(data) { this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; this.DisableStackManagementForRegularUsers = data.DisableStackManagementForRegularUsers; this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers; + this.AllowDeviceMappingForRegularUsers = data.AllowDeviceMappingForRegularUsers; } export function PublicSettingsViewModel(settings) { @@ -28,6 +29,7 @@ export function PublicSettingsViewModel(settings) { this.LogoURL = settings.LogoURL; this.OAuthLoginURI = settings.OAuthLoginURI; this.DisableStackManagementForRegularUsers = settings.DisableStackManagementForRegularUsers; + this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; } export function LDAPSettingsViewModel(data) { diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index ac6b5b472..481c9aac6 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -81,6 +81,11 @@ angular.module('portainer.app').factory('StateManager', [ LocalStorage.storeApplicationState(state.application); }; + manager.updateAllowDeviceMappingForRegularUsers = function updateAllowDeviceMappingForRegularUsers(allowDeviceMappingForRegularUsers) { + state.application.allowDeviceMappingForRegularUsers = allowDeviceMappingForRegularUsers; + LocalStorage.storeApplicationState(state.application); + }; + manager.updateAllowHostNamespaceForRegularUsers = function (allowHostNamespaceForRegularUsers) { state.application.allowHostNamespaceForRegularUsers = allowHostNamespaceForRegularUsers; LocalStorage.storeApplicationState(state.application); @@ -98,6 +103,7 @@ angular.module('portainer.app').factory('StateManager', [ state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers; state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; state.application.disableStackManagementForRegularUsers = settings.DisableStackManagementForRegularUsers; + state.application.allowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; state.application.validity = moment().unix(); } diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index f7b0906d8..a45d1e316 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -141,6 +141,17 @@
+ +
+
+ + +
+
diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index ddb167d1d..d9f403abc 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -35,6 +35,7 @@ angular.module('portainer.app').controller('SettingsController', [ enableEdgeComputeFeatures: false, disableStackManagementForRegularUsers: false, restrictHostNamespaceForRegularUsers: false, + allowDeviceMappingForRegularUsers: false, }; $scope.removeFilteredContainerLabel = function (index) { @@ -73,6 +74,7 @@ angular.module('portainer.app').controller('SettingsController', [ settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures; settings.DisableStackManagementForRegularUsers = $scope.formValues.disableStackManagementForRegularUsers; settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers; + settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers; $scope.state.actionInProgress = true; updateSettings(settings); @@ -89,6 +91,7 @@ angular.module('portainer.app').controller('SettingsController', [ StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures); StateManager.updateDisableStackManagementForRegularUsers(settings.DisableStackManagementForRegularUsers); StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers); + StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers); $state.reload(); }) .catch(function error(err) { @@ -117,6 +120,7 @@ angular.module('portainer.app').controller('SettingsController', [ $scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; $scope.formValues.disableStackManagementForRegularUsers = settings.DisableStackManagementForRegularUsers; $scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers; + $scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings');