diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index a606fefd4..246faddbe 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -9,7 +9,7 @@ import ( "github.com/portainer/portainer/api/chisel" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt" "github.com/portainer/portainer/api/cli" "github.com/portainer/portainer/api/cron" @@ -274,6 +274,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL AllowPrivilegedModeForRegularUsers: true, AllowVolumeBrowserForRegularUsers: false, EnableHostManagementFeatures: false, + AllowHostNamespaceForRegularUsers: true, SnapshotInterval: *flags.SnapshotInterval, EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, } diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 0f207b240..07f696a7f 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type publicSettingsResponse struct { @@ -20,6 +20,7 @@ type publicSettingsResponse struct { ExternalTemplates bool `json:"ExternalTemplates"` OAuthLoginURI string `json:"OAuthLoginURI"` DisableStackManagementForRegularUsers bool `json:"DisableStackManagementForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` } // GET request on /api/settings/public @@ -37,6 +38,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, EnableHostManagementFeatures: settings.EnableHostManagementFeatures, EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, + AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, ExternalTemplates: false, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", settings.OAuthSettings.AuthorizationURI, diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 242a115c3..49d2098e0 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -7,7 +7,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" "github.com/portainer/portainer/api/filesystem" ) @@ -26,6 +26,7 @@ type settingsUpdatePayload struct { EdgeAgentCheckinInterval *int EnableEdgeComputeFeatures *bool DisableStackManagementForRegularUsers *bool + AllowHostNamespaceForRegularUsers *bool } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -119,6 +120,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.DisableStackManagementForRegularUsers = *payload.DisableStackManagementForRegularUsers } + if payload.AllowHostNamespaceForRegularUsers != nil { + settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers + } + if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval { err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval) if err != nil { diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index feaab135e..153ee53aa 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -1,7 +1,6 @@ package stacks import ( - "errors" "net/http" "path" "regexp" @@ -331,29 +330,12 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) return err } - rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension) - if err != nil && err != portainer.ErrObjectNotFound { - return errors.New("Unable to verify if RBAC extension is loaded") + isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID) + if err != nil { + return err } - endpointResourceAccess := false - _, ok := config.user.EndpointAuthorizations[portainer.EndpointID(config.endpoint.ID)][portainer.EndpointResourcesAccess] - if ok { - endpointResourceAccess = true - } - - mustBeChecked := false - if rbacExtension != nil { - if !config.isAdmin && !endpointResourceAccess { - mustBeChecked = true - } - } else { - if !config.isAdmin { - mustBeChecked = true - } - } - - if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers) && mustBeChecked { + if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers) && !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 0dbd6e183..08a566a4d 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -291,6 +291,7 @@ type swarmStackDeploymentConfig struct { registries []portainer.Registry prune bool isAdmin bool + user *portainer.User } func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) { @@ -310,6 +311,11 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine } filteredRegistries := security.FilterRegistries(registries, securityContext) + user, err := handler.UserService.User(securityContext.UserID) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err} + } + config := &swarmStackDeploymentConfig{ stack: stack, endpoint: endpoint, @@ -317,6 +323,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine registries: filteredRegistries, prune: prune, isAdmin: securityContext.IsAdmin, + user: user, } return config, nil @@ -328,7 +335,12 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err return err } - if !settings.AllowBindMountsForRegularUsers && !config.isAdmin { + isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID) + if err != nil { + return 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/handler.go b/api/http/handler/stacks/handler.go index e9c6c241c..253acb7e4 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -1,12 +1,13 @@ package stacks import ( + "errors" "net/http" "sync" "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" ) @@ -111,3 +112,30 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR } return false, nil } + +func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) { + isAdmin := user.Role == portainer.AdministratorRole + + rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension) + if err != nil && err != portainer.ErrObjectNotFound { + return false, errors.New("Unable to verify if RBAC extension is loaded") + } + + endpointResourceAccess := false + _, ok := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess] + if ok { + endpointResourceAccess = true + } + + if rbacExtension != nil { + if isAdmin || endpointResourceAccess { + return true, nil + } + } else { + if isAdmin { + return true, nil + } + } + + return false, nil +} diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 229b3fce9..88eacc148 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -156,6 +156,10 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true { return errors.New("privileged mode disabled for non administrator users") } + + if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" { + return errors.New("pid host 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 2a80d788f..ac01bb453 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -156,10 +156,15 @@ 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"` + Privileged bool `json:"Privileged"` + PidMode string `json:"PidMode"` } `json:"HostConfig"` } + forbiddenResponse := &http.Response{ + StatusCode: http.StatusForbidden, + } + tokenData, err := security.RetrieveTokenData(request) if err != nil { return nil, err @@ -188,7 +193,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return nil, err } - if !settings.AllowPrivilegedModeForRegularUsers { + if !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers { body, err := ioutil.ReadAll(request.Body) if err != nil { return nil, err @@ -201,7 +206,11 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req } if partialContainer.HostConfig.Privileged { - return nil, errors.New("forbidden to use privileged mode") + return forbiddenResponse, errors.New("forbidden to use privileged mode") + } + + if partialContainer.HostConfig.PidMode == "host" { + return forbiddenResponse, errors.New("forbidden to use pid host namespace") } request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) diff --git a/api/portainer.go b/api/portainer.go index 471c22928..6c725f95f 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -434,6 +434,7 @@ type ( EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` DisableStackManagementForRegularUsers bool `json:"DisableStackManagementForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 1fc49b34c..bce96696a 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -14,6 +14,7 @@ export function SettingsViewModel(data) { this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; this.DisableStackManagementForRegularUsers = data.DisableStackManagementForRegularUsers; + this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers; } export function PublicSettingsViewModel(settings) { diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 3d52085ae..ac6b5b472 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.updateAllowHostNamespaceForRegularUsers = function (allowHostNamespaceForRegularUsers) { + state.application.allowHostNamespaceForRegularUsers = allowHostNamespaceForRegularUsers; + LocalStorage.storeApplicationState(state.application); + }; + function assignStateFromStatusAndSettings(status, settings) { state.application.authentication = status.Authentication; state.application.analytics = status.Analytics; diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 3875df8a4..f7b0906d8 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -130,6 +130,17 @@ +
+
+ + +
+
diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index 43e326eab..ddb167d1d 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -34,6 +34,7 @@ angular.module('portainer.app').controller('SettingsController', [ enableVolumeBrowser: false, enableEdgeComputeFeatures: false, disableStackManagementForRegularUsers: false, + restrictHostNamespaceForRegularUsers: false, }; $scope.removeFilteredContainerLabel = function (index) { @@ -71,6 +72,7 @@ angular.module('portainer.app').controller('SettingsController', [ settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures; settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures; settings.DisableStackManagementForRegularUsers = $scope.formValues.disableStackManagementForRegularUsers; + settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers; $scope.state.actionInProgress = true; updateSettings(settings); @@ -86,6 +88,7 @@ angular.module('portainer.app').controller('SettingsController', [ StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers); StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures); StateManager.updateDisableStackManagementForRegularUsers(settings.DisableStackManagementForRegularUsers); + StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers); $state.reload(); }) .catch(function error(err) { @@ -113,6 +116,7 @@ angular.module('portainer.app').controller('SettingsController', [ $scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures; $scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; $scope.formValues.disableStackManagementForRegularUsers = settings.DisableStackManagementForRegularUsers; + $scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings');