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