From 07efd4bddae3460e12bf7fb5517d6273806fb4fb Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 27 Jul 2020 00:31:14 +0300 Subject: [PATCH] feat(settings): add setting to disable device mapping for regular users (#4099) * feat(settings): add setting to disable device mapping for regular users * 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 * style(server): sort imports --- api/bolt/init.go | 1 + api/bolt/migrator/migrate_dbversion23.go | 1 + api/http/handler/settings/settings_public.go | 4 +++- api/http/handler/settings/settings_update.go | 5 +++++ .../handler/stacks/create_compose_stack.go | 3 ++- api/http/handler/stacks/stack_create.go | 6 +++++- api/http/proxy/factory/docker/containers.go | 13 +++++++++--- api/portainer.go | 3 ++- .../create/createContainerController.js | 20 +++++++++++++++++-- .../containers/create/createcontainer.html | 2 +- app/portainer/models/settings.js | 4 +++- app/portainer/services/stateManager.js | 6 ++++++ app/portainer/views/settings/settings.html | 10 ++++++++++ .../views/settings/settingsController.js | 4 ++++ 14 files changed, 71 insertions(+), 11 deletions(-) diff --git a/api/bolt/init.go b/api/bolt/init.go index 982702071..1369e6a1e 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -29,6 +29,7 @@ func (store *Store) Init() error { AllowPrivilegedModeForRegularUsers: true, AllowVolumeBrowserForRegularUsers: false, AllowHostNamespaceForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, EnableHostManagementFeatures: false, EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, TemplatesURL: portainer.DefaultTemplatesURL, diff --git a/api/bolt/migrator/migrate_dbversion23.go b/api/bolt/migrator/migrate_dbversion23.go index 74f6436cf..ba38987c5 100644 --- a/api/bolt/migrator/migrate_dbversion23.go +++ b/api/bolt/migrator/migrate_dbversion23.go @@ -7,6 +7,7 @@ func (m *Migrator) updateSettingsToDB24() error { } legacySettings.AllowHostNamespaceForRegularUsers = true + legacySettings.AllowDeviceMappingForRegularUsers = true return m.settingsService.UpdateSettings(legacySettings) } diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 3dfcd5325..097c14676 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -15,10 +15,11 @@ type publicSettingsResponse struct { AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` OAuthLoginURI string `json:"OAuthLoginURI"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` } // GET request on /api/settings/public @@ -35,6 +36,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, + AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, EnableHostManagementFeatures: settings.EnableHostManagementFeatures, EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index a0a05c9e8..d92307b08 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -24,6 +24,7 @@ type settingsUpdatePayload struct { AllowPrivilegedModeForRegularUsers *bool AllowHostNamespaceForRegularUsers *bool AllowVolumeBrowserForRegularUsers *bool + AllowDeviceMappingForRegularUsers *bool EnableHostManagementFeatures *bool SnapshotInterval *string TemplatesURL *string @@ -149,6 +150,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * handler.JWTService.SetUserSessionDuration(userSessionDuration) } + 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 0e7f1c2e3..af79889f4 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -338,7 +338,8 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers || - !settings.AllowHostNamespaceForRegularUsers) && + !settings.AllowHostNamespaceForRegularUsers || + !settings.AllowDeviceMappingForRegularUsers) && !isAdminOrEndpointAdmin { composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 2da04589e..ba3b5388d 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -10,7 +10,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" @@ -146,6 +146,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 cdf1fdb8a..d957be030 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -158,8 +158,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"` } @@ -188,7 +189,9 @@ 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.dataStore.Settings().Settings() if err != nil { return nil, err @@ -213,6 +216,10 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return forbiddenResponse, errors.New("forbidden to use pid host namespace") } + if !settings.AllowDeviceMappingForRegularUsers && 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 8ff870800..d503e0bbf 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -518,13 +518,14 @@ type ( AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` SnapshotInterval string `json:"SnapshotInterval"` TemplatesURL string `json:"TemplatesURL"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` UserSessionTimeout string `json:"UserSessionTimeout"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index bebf468de..965364b72 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; @@ -604,7 +606,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ }); } - function initView() { + async function initView() { var nodeName = $transition$.params().nodeName; $scope.formValues.NodeName = nodeName; HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); @@ -685,6 +687,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ }); $scope.isAdmin = Authentication.isAdmin(); + $scope.showDeviceMapping = await shouldShowDevices(); } function validateForm(accessControlData, isAdmin) { @@ -897,6 +900,19 @@ angular.module('portainer.docker').controller('CreateContainerController', [ } } + async function shouldShowDevices() { + const isAdmin = 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 e6d1ea848..39ba8be71 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -625,7 +625,7 @@
-
+
diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 49cdd0faa..6d4abe08d 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -7,13 +7,14 @@ export function SettingsViewModel(data) { this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers; this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers; this.AllowVolumeBrowserForRegularUsers = data.AllowVolumeBrowserForRegularUsers; + this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers; + this.AllowDeviceMappingForRegularUsers = data.AllowDeviceMappingForRegularUsers; this.SnapshotInterval = data.SnapshotInterval; this.TemplatesURL = data.TemplatesURL; this.EnableHostManagementFeatures = data.EnableHostManagementFeatures; this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; this.UserSessionTimeout = data.UserSessionTimeout; - this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers; } export function PublicSettingsViewModel(settings) { @@ -25,6 +26,7 @@ export function PublicSettingsViewModel(settings) { this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; this.LogoURL = settings.LogoURL; this.OAuthLoginURI = settings.OAuthLoginURI; + this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; } export function LDAPSettingsViewModel(data) { diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 9f57c2959..f7ebc9ea5 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -79,6 +79,11 @@ angular.module('portainer.app').factory('StateManager', [ 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); }; function assignStateFromStatusAndSettings(status, settings) { @@ -89,6 +94,7 @@ angular.module('portainer.app').factory('StateManager', [ state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures; state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers; state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; + 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 0b3f4a848..b742af0fb 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -119,6 +119,16 @@
+
+
+ + +
+
diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index 7328d1479..135bfb8a9 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -33,6 +33,7 @@ angular.module('portainer.app').controller('SettingsController', [ enableVolumeBrowser: false, enableEdgeComputeFeatures: false, restrictHostNamespaceForRegularUsers: false, + allowDeviceMappingForRegularUsers: false, }; $scope.removeFilteredContainerLabel = function (index) { @@ -66,6 +67,7 @@ angular.module('portainer.app').controller('SettingsController', [ settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures; settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures; settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers; + settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers; $scope.state.actionInProgress = true; updateSettings(settings); @@ -81,6 +83,7 @@ angular.module('portainer.app').controller('SettingsController', [ StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers); StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers); StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures); + StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers); $state.reload(); }) .catch(function error(err) { @@ -106,6 +109,7 @@ angular.module('portainer.app').controller('SettingsController', [ $scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures; $scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; $scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers; + $scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings');