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