diff --git a/api/bolt/init.go b/api/bolt/init.go index b67c39df0..7ce23f138 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -40,18 +40,11 @@ func (store *Store) Init() error { portainer.LDAPGroupSearchSettings{}, }, }, - OAuthSettings: portainer.OAuthSettings{}, - AllowBindMountsForRegularUsers: true, - AllowPrivilegedModeForRegularUsers: true, - AllowVolumeBrowserForRegularUsers: false, - AllowHostNamespaceForRegularUsers: true, - AllowDeviceMappingForRegularUsers: true, - AllowStackManagementForRegularUsers: true, - AllowContainerCapabilitiesForRegularUsers: true, - EnableHostManagementFeatures: false, - EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, - TemplatesURL: portainer.DefaultTemplatesURL, - UserSessionTimeout: portainer.DefaultUserSessionTimeout, + OAuthSettings: portainer.OAuthSettings{}, + + EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, + TemplatesURL: portainer.DefaultTemplatesURL, + UserSessionTimeout: portainer.DefaultUserSessionTimeout, } err = store.SettingsService.UpdateSettings(defaultSettings) diff --git a/api/bolt/migrator/migrate_dbversion25.go b/api/bolt/migrator/migrate_dbversion25.go new file mode 100644 index 000000000..98fbb083a --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion25.go @@ -0,0 +1,51 @@ +package migrator + +import ( + portainer "github.com/portainer/portainer/api" +) + +func (m *Migrator) updateEndpointSettingsToDB25() error { + settings, err := m.settingsService.Settings() + if err != nil { + return err + } + + endpoints, err := m.endpointService.Endpoints() + if err != nil { + return err + } + + for i := range endpoints { + endpoint := endpoints[i] + + securitySettings := portainer.EndpointSecuritySettings{} + + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || + endpoint.Type == portainer.AgentOnDockerEnvironment || + endpoint.Type == portainer.DockerEnvironment { + + securitySettings = portainer.EndpointSecuritySettings{ + AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, + AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers, + AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, + AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, + AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, + AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, + } + + if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { + securitySettings.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers + securitySettings.EnableHostManagementFeatures = settings.EnableHostManagementFeatures + } + } + + endpoint.SecuritySettings = securitySettings + + err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 8217dc302..468fe0aa7 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -342,5 +342,13 @@ func (m *Migrator) Migrate() error { } } + // Portainer 2.1.0 + if m.currentDBVersion < 26 { + err := m.updateEndpointSettingsToDB25() + 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 229b0c0a5..d8ddf0cdd 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -248,6 +248,18 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat Status: portainer.EndpointStatusUp, Snapshots: []portainer.DockerSnapshot{}, Kubernetes: portainer.KubernetesDefault(), + + SecuritySettings: portainer.EndpointSecuritySettings{ + AllowVolumeBrowserForRegularUsers: false, + EnableHostManagementFeatures: false, + + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowHostNamespaceForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + }, } if strings.HasPrefix(endpoint.URL, "tcp://") { @@ -297,6 +309,18 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, Status: portainer.EndpointStatusUp, Snapshots: []portainer.DockerSnapshot{}, Kubernetes: portainer.KubernetesDefault(), + + SecuritySettings: portainer.EndpointSecuritySettings{ + AllowVolumeBrowserForRegularUsers: false, + EnableHostManagementFeatures: false, + + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowHostNamespaceForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + }, } err := snapshotService.SnapshotEndpoint(endpoint) diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 728711d87..aadbe6ca0 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -14,7 +14,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/crypto" "github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/internal/edge" @@ -440,6 +440,18 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint) } func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.Endpoint) error { + endpoint.SecuritySettings = portainer.EndpointSecuritySettings{ + AllowVolumeBrowserForRegularUsers: false, + EnableHostManagementFeatures: false, + + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowHostNamespaceForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + } + err := handler.DataStore.Endpoint().CreateEndpoint(endpoint) if err != nil { return err diff --git a/api/http/handler/endpoints/endpoint_settings_update.go b/api/http/handler/endpoints/endpoint_settings_update.go new file mode 100644 index 000000000..75d846c26 --- /dev/null +++ b/api/http/handler/endpoints/endpoint_settings_update.go @@ -0,0 +1,90 @@ +package endpoints + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" +) + +type endpointSettingsUpdatePayload struct { + AllowBindMountsForRegularUsers *bool `json:"allowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers *bool `json:"allowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers *bool `json:"allowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers *bool `json:"allowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers *bool `json:"allowDeviceMappingForRegularUsers"` + AllowStackManagementForRegularUsers *bool `json:"allowStackManagementForRegularUsers"` + AllowContainerCapabilitiesForRegularUsers *bool `json:"allowContainerCapabilitiesForRegularUsers"` + EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures"` +} + +func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error { + return nil +} + +// PUT request on /api/endpoints/:id/settings +func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + } + + var payload endpointSettingsUpdatePayload + err = request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) + if err == errors.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + securitySettings := endpoint.SecuritySettings + + if payload.AllowBindMountsForRegularUsers != nil { + securitySettings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers + } + + if payload.AllowContainerCapabilitiesForRegularUsers != nil { + securitySettings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers + } + + if payload.AllowDeviceMappingForRegularUsers != nil { + securitySettings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers + } + + if payload.AllowHostNamespaceForRegularUsers != nil { + securitySettings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers + } + + if payload.AllowPrivilegedModeForRegularUsers != nil { + securitySettings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers + } + + if payload.AllowStackManagementForRegularUsers != nil { + securitySettings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers + } + + if payload.AllowVolumeBrowserForRegularUsers != nil { + securitySettings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers + } + + if payload.EnableHostManagementFeatures != nil { + securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures + } + + endpoint.SecuritySettings = securitySettings + + err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err} + } + + return response.JSON(w, endpoint) +} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 3dc8689d6..ae23f5c9a 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -39,6 +39,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { h.Handle("/endpoints", bouncer.AdminAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost) + h.Handle("/endpoints/{id}/settings", + bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSettingsUpdate))).Methods(http.MethodPut) h.Handle("/endpoints/snapshot", bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost) h.Handle("/endpoints", diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index e94f501e0..1a9efdb77 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -10,19 +10,11 @@ import ( ) type publicSettingsResponse struct { - LogoURL string `json:"LogoURL"` - AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` - AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` - AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` - AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - OAuthLoginURI string `json:"OAuthLoginURI"` - EnableTelemetry bool `json:"EnableTelemetry"` + LogoURL string `json:"LogoURL"` + AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + OAuthLoginURI string `json:"OAuthLoginURI"` + EnableTelemetry bool `json:"EnableTelemetry"` } // GET request on /api/settings/public @@ -33,18 +25,10 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * } publicSettings := &publicSettingsResponse{ - LogoURL: settings.LogoURL, - AuthenticationMethod: settings.AuthenticationMethod, - AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, - AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, - AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, - AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, - AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, - AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, - AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers, - EnableHostManagementFeatures: settings.EnableHostManagementFeatures, - EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, - EnableTelemetry: settings.EnableTelemetry, + LogoURL: settings.LogoURL, + AuthenticationMethod: settings.AuthenticationMethod, + EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, + EnableTelemetry: settings.EnableTelemetry, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", settings.OAuthSettings.AuthorizationURI, settings.OAuthSettings.ClientID, diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index fbdf47bcf..f89dafc3c 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -14,25 +14,17 @@ import ( ) type settingsUpdatePayload struct { - LogoURL *string - BlackListedLabels []portainer.Pair - AuthenticationMethod *int - LDAPSettings *portainer.LDAPSettings - OAuthSettings *portainer.OAuthSettings - AllowBindMountsForRegularUsers *bool - AllowPrivilegedModeForRegularUsers *bool - AllowHostNamespaceForRegularUsers *bool - AllowVolumeBrowserForRegularUsers *bool - AllowDeviceMappingForRegularUsers *bool - AllowStackManagementForRegularUsers *bool - AllowContainerCapabilitiesForRegularUsers *bool - EnableHostManagementFeatures *bool - SnapshotInterval *string - TemplatesURL *string - EdgeAgentCheckinInterval *int - EnableEdgeComputeFeatures *bool - UserSessionTimeout *string - EnableTelemetry *bool + LogoURL *string + BlackListedLabels []portainer.Pair + AuthenticationMethod *int + LDAPSettings *portainer.LDAPSettings + OAuthSettings *portainer.OAuthSettings + SnapshotInterval *string + TemplatesURL *string + EdgeAgentCheckinInterval *int + EnableEdgeComputeFeatures *bool + UserSessionTimeout *string + EnableTelemetry *bool } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -107,38 +99,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.OAuthSettings.ClientSecret = clientSecret } - if payload.AllowBindMountsForRegularUsers != nil { - settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers - } - - if payload.AllowPrivilegedModeForRegularUsers != nil { - settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers - } - - if payload.AllowVolumeBrowserForRegularUsers != nil { - settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers - } - - if payload.EnableHostManagementFeatures != nil { - settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures - } - if payload.EnableEdgeComputeFeatures != nil { settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures } - if payload.AllowHostNamespaceForRegularUsers != nil { - settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers - } - - if payload.AllowStackManagementForRegularUsers != nil { - settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers - } - - if payload.AllowContainerCapabilitiesForRegularUsers != nil { - settings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers - } - if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval { err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval) if err != nil { @@ -158,10 +122,6 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * handler.JWTService.SetUserSessionDuration(userSessionDuration) } - if payload.AllowDeviceMappingForRegularUsers != nil { - settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers - } - if payload.EnableTelemetry != nil { settings.EnableTelemetry = *payload.EnableTelemetry } diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 52fc2844c..e574f524e 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -339,21 +339,18 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai // clean it. Hence the use of the mutex. // We should contribute to libcompose to support authentication without using the config.json file. func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error { - settings, err := handler.DataStore.Settings().Settings() - if err != nil { - return err - } - isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID) if err != nil { return err } - if (!settings.AllowBindMountsForRegularUsers || - !settings.AllowPrivilegedModeForRegularUsers || - !settings.AllowHostNamespaceForRegularUsers || - !settings.AllowDeviceMappingForRegularUsers || - !settings.AllowContainerCapabilitiesForRegularUsers) && + securitySettings := &config.endpoint.SecuritySettings + + if (!securitySettings.AllowBindMountsForRegularUsers || + !securitySettings.AllowPrivilegedModeForRegularUsers || + !securitySettings.AllowHostNamespaceForRegularUsers || + !securitySettings.AllowDeviceMappingForRegularUsers || + !securitySettings.AllowContainerCapabilitiesForRegularUsers) && !isAdminOrEndpointAdmin { composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) @@ -362,7 +359,7 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) return err } - err = handler.isValidStackFile(stackContent, settings) + err = handler.isValidStackFile(stackContent, securitySettings) if err != nil { return err } diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index f7afbdeb5..1d475b8ad 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -344,16 +344,13 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine } func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error { - settings, err := handler.DataStore.Settings().Settings() - if err != nil { - return err - } - isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID) if err != nil { return err } + settings := &config.endpoint.SecuritySettings + if !settings.AllowBindMountsForRegularUsers && !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 a9fdf2f36..24c5a96bf 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -46,12 +46,14 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} } - settings, err := handler.DataStore.Settings().Settings() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) + if err == bolterrors.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - if !settings.AllowStackManagementForRegularUsers { + if !endpoint.SecuritySettings.AllowStackManagementForRegularUsers { securityContext, err := security.RetrieveRestrictedRequestContext(r) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err} @@ -69,13 +71,6 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt } } - endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} - } - err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} @@ -129,7 +124,7 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)} } -func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error { +func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error { composeConfigYAML, err := loader.ParseYAML(stackFileContent) if err != nil { return err @@ -154,7 +149,7 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port for key := range composeConfig.Services { service := composeConfig.Services[key] - if !settings.AllowBindMountsForRegularUsers { + if !securitySettings.AllowBindMountsForRegularUsers { for _, volume := range service.Volumes { if volume.Type == "bind" { return errors.New("bind-mount disabled for non administrator users") @@ -162,19 +157,19 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port } } - if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true { + if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged == true { return errors.New("privileged mode disabled for non administrator users") } - if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" { + if !securitySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" { return errors.New("pid host disabled for non administrator users") } - if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 { + if !securitySettings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 { return errors.New("device mapping disabled for non administrator users") } - if !settings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) { + if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) { return errors.New("container capabilities disabled for non administrator users") } } diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index 3f9ecf9a1..dcce9c725 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -9,7 +9,7 @@ import ( "net/http" "github.com/docker/docker/client" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" @@ -181,7 +181,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req } if !isAdminOrEndpointAdmin { - settings, err := transport.dataStore.Settings().Settings() + securitySettings, err := transport.fetchEndpointSecuritySettings() if err != nil { return nil, err } @@ -197,23 +197,23 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return nil, err } - if !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged { + if !securitySettings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged { return forbiddenResponse, errors.New("forbidden to use privileged mode") } - if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" { + if !securitySettings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" { return forbiddenResponse, errors.New("forbidden to use pid host namespace") } - if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 { + if !securitySettings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 { return forbiddenResponse, errors.New("forbidden to use device mapping") } - if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { + if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { return nil, errors.New("forbidden to use container capabilities") } - if !settings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) { + if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) { return forbiddenResponse, errors.New("forbidden to use bind mounts") } diff --git a/api/http/proxy/factory/docker/services.go b/api/http/proxy/factory/docker/services.go index 08f01a23c..5453f8ed8 100644 --- a/api/http/proxy/factory/docker/services.go +++ b/api/http/proxy/factory/docker/services.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/client" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/internal/authorization" ) @@ -111,7 +111,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque } if !isAdminOrEndpointAdmin { - settings, err := transport.dataStore.Settings().Settings() + securitySettings, err := transport.fetchEndpointSecuritySettings() if err != nil { return nil, err } @@ -127,7 +127,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque return nil, err } - if !settings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) { + if !securitySettings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) { for _, mount := range partialService.TaskTemplate.ContainerSpec.Mounts { if mount.Type == "bind" { return forbiddenResponse, errors.New("forbidden to use bind mounts") diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index 623875f53..5ac4efb4f 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -11,7 +11,7 @@ import ( "strings" "github.com/docker/docker/client" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/security" @@ -407,12 +407,12 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r if tokenData.Role != portainer.AdministratorRole { if volumeBrowseRestrictionCheck { - settings, err := transport.dataStore.Settings().Settings() + securitySettings, err := transport.fetchEndpointSecuritySettings() if err != nil { return nil, err } - if !settings.AllowVolumeBrowserForRegularUsers { + if !securitySettings.AllowVolumeBrowserForRegularUsers { return responseutils.WriteAccessDeniedResponse() } } @@ -682,3 +682,12 @@ func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool, return tokenData.Role == portainer.AdministratorRole, nil } + +func (transport *Transport) fetchEndpointSecuritySettings() (*portainer.EndpointSecuritySettings, error) { + endpoint, err := transport.dataStore.Endpoint().Endpoint(portainer.EndpointID(transport.endpoint.ID)) + if err != nil { + return nil, err + } + + return &endpoint.SecuritySettings, nil +} diff --git a/api/portainer.go b/api/portainer.go index 00e1c32de..d03a55b41 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -209,6 +209,7 @@ type ( EdgeCheckinInterval int `json:"EdgeCheckinInterval"` Kubernetes KubernetesData `json:"Kubernetes"` ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion"` + SecuritySettings EndpointSecuritySettings // Deprecated fields // Deprecated in DBVersion == 4 @@ -272,6 +273,18 @@ type ( // Deprecated EndpointSyncJob struct{} + // EndpointSecuritySettings represents settings for an endpoint + EndpointSecuritySettings struct { + AllowBindMountsForRegularUsers bool `json:"allowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"allowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"allowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"allowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"allowDeviceMappingForRegularUsers"` + AllowStackManagementForRegularUsers bool `json:"allowStackManagementForRegularUsers"` + AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers"` + EnableHostManagementFeatures bool `json:"enableHostManagementFeatures"` + } + // EndpointType represents the type of an endpoint EndpointType int @@ -516,29 +529,31 @@ type ( // Settings represents the application settings Settings struct { - LogoURL string `json:"LogoURL"` - BlackListedLabels []Pair `json:"BlackListedLabels"` - AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` - LDAPSettings LDAPSettings `json:"LDAPSettings"` - OAuthSettings OAuthSettings `json:"OAuthSettings"` - AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` - AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` - AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` - SnapshotInterval string `json:"SnapshotInterval"` - TemplatesURL string `json:"TemplatesURL"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - UserSessionTimeout string `json:"UserSessionTimeout"` - EnableTelemetry bool `json:"EnableTelemetry"` + LogoURL string `json:"LogoURL"` + BlackListedLabels []Pair `json:"BlackListedLabels"` + AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` + LDAPSettings LDAPSettings `json:"LDAPSettings"` + OAuthSettings OAuthSettings `json:"OAuthSettings"` + SnapshotInterval string `json:"SnapshotInterval"` + TemplatesURL string `json:"TemplatesURL"` + EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + UserSessionTimeout string `json:"UserSessionTimeout"` + EnableTelemetry bool `json:"EnableTelemetry"` // Deprecated fields DisplayDonationHeader bool DisplayExternalContributors bool + + // Deprecated fields v26 + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` } // SnapshotJob represents a scheduled job that can create endpoint snapshots @@ -1127,7 +1142,7 @@ const ( // APIVersion is the version number of the Portainer API APIVersion = "2.1.0" // DBVersion is the version number of the Portainer database - DBVersion = 25 + DBVersion = 26 // ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax ComposeSyntaxMaxVersion = "3.9" // AssetsServerURL represents the URL of the Portainer asset server diff --git a/app/assets/css/app.css b/app/assets/css/app.css index f813dbb2c..4ce4aab3c 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -625,57 +625,6 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { margin-left: 21px; } -/* switch box */ -:root { - --switch-size: 24px; -} - -.switch input { - display: none; -} - -.switch i, -.bootbox-form .checkbox i { - display: inline-block; - vertical-align: middle; - cursor: pointer; - padding-right: var(--switch-size); - transition: all ease 0.2s; - -webkit-transition: all ease 0.2s; - -moz-transition: all ease 0.2s; - -o-transition: all ease 0.2s; - border-radius: var(--switch-size); - box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5); -} - -.switch i:before, -.bootbox-form .checkbox i:before { - display: block; - content: ''; - width: var(--switch-size); - height: var(--switch-size); - border-radius: var(--switch-size); - background: white; - box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5); -} - -.switch :checked + i, -.bootbox-form .checkbox :checked ~ i { - padding-right: 0; - padding-left: var(--switch-size); - -webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; - -moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; - box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; -} -/* !switch box */ - -/* small switch box */ -.switch.small { - --switch-size: 12px; -} - -/* !small switch box */ - .boxselector_wrapper { display: flex; flex-flow: row wrap; diff --git a/app/docker/__module.js b/app/docker/__module.js index 33495ab47..516134d7d 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -581,6 +581,16 @@ angular.module('portainer.docker', ['portainer.app']).config([ }, }; + const dockerFeaturesConfiguration = { + name: 'docker.featuresConfiguration', + url: '/feat-config', + views: { + 'content@': { + component: 'dockerFeaturesConfigurationView', + }, + }, + }; + $stateRegistryProvider.register(configs); $stateRegistryProvider.register(config); $stateRegistryProvider.register(configCreation); @@ -630,5 +640,6 @@ angular.module('portainer.docker', ['portainer.app']).config([ $stateRegistryProvider.register(volume); $stateRegistryProvider.register(volumeBrowse); $stateRegistryProvider.register(volumeCreation); + $stateRegistryProvider.register(dockerFeaturesConfiguration); }, ]); diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html index 5af425761..484451142 100644 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html @@ -37,7 +37,15 @@ diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index a59371819..ce5f65ecf 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -27,9 +27,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [ 'ModalService', 'RegistryService', 'SystemService', - 'SettingsService', 'PluginService', 'HttpRequestHelper', + 'endpoint', function ( $q, $scope, @@ -53,9 +53,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [ ModalService, RegistryService, SystemService, - SettingsService, PluginService, - HttpRequestHelper + HttpRequestHelper, + endpoint ) { $scope.create = create; @@ -709,14 +709,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [ Notifications.error('Failure', err, 'Unable to retrieve engine details'); }); - SettingsService.publicSettings() - .then(function success(data) { - $scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || data.AllowBindMountsForRegularUsers; - $scope.allowPrivilegedMode = data.AllowPrivilegedModeForRegularUsers; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve application settings'); - }); + $scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers; + $scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers; PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) { $scope.availableLoggingDrivers = loggingDrivers; @@ -933,15 +927,11 @@ angular.module('portainer.docker').controller('CreateContainerController', [ } async function shouldShowDevices() { - const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application; - - return allowDeviceMappingForRegularUsers || Authentication.isAdmin(); + return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin(); } async function checkIfContainerCapabilitiesEnabled() { - const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application; - - return allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin(); + return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin(); } initView(); diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index 1b0cf5a81..95affc028 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -22,6 +22,7 @@ angular.module('portainer.docker').controller('ContainerController', [ 'HttpRequestHelper', 'Authentication', 'StateManager', + 'endpoint', function ( $q, $scope, @@ -41,7 +42,8 @@ angular.module('portainer.docker').controller('ContainerController', [ ImageService, HttpRequestHelper, Authentication, - StateManager + StateManager, + endpoint ) { $scope.activityTime = 0; $scope.portBindings = []; @@ -97,14 +99,13 @@ angular.module('portainer.docker').controller('ContainerController', [ const inSwarm = $scope.container.Config.Labels['com.docker.swarm.service.id']; const autoRemove = $scope.container.HostConfig.AutoRemove; const admin = Authentication.isAdmin(); - const appState = StateManager.getState(); const { allowContainerCapabilitiesForRegularUsers, allowHostNamespaceForRegularUsers, allowDeviceMappingForRegularUsers, allowBindMountsForRegularUsers, allowPrivilegedModeForRegularUsers, - } = appState.application; + } = endpoint.SecuritySettings; const settingRestrictsRegularUsers = !allowContainerCapabilitiesForRegularUsers || diff --git a/app/docker/views/dashboard/dashboardController.js b/app/docker/views/dashboard/dashboardController.js index dfcac9981..0e09b30d1 100644 --- a/app/docker/views/dashboard/dashboardController.js +++ b/app/docker/views/dashboard/dashboardController.js @@ -17,6 +17,7 @@ angular.module('portainer.docker').controller('DashboardController', [ 'EndpointProvider', 'StateManager', 'TagService', + 'endpoint', function ( $scope, $q, @@ -32,7 +33,8 @@ angular.module('portainer.docker').controller('DashboardController', [ Notifications, EndpointProvider, StateManager, - TagService + TagService, + endpoint ) { $scope.dismissInformationPanel = function (id) { StateManager.dismissInformationPanel(id); @@ -89,9 +91,8 @@ angular.module('portainer.docker').controller('DashboardController', [ async function shouldShowStacks() { const isAdmin = Authentication.isAdmin(); - const { allowStackManagementForRegularUsers } = $scope.applicationState.application; - return isAdmin || allowStackManagementForRegularUsers; + return isAdmin || endpoint.SecuritySettings.allowStackManagementForRegularUsers; } initView(); diff --git a/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js b/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js new file mode 100644 index 000000000..bedec4867 --- /dev/null +++ b/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js @@ -0,0 +1,94 @@ +export default class DockerFeaturesConfigurationController { + /* @ngInject */ + constructor($async, EndpointService, Notifications, StateManager) { + this.$async = $async; + this.EndpointService = EndpointService; + this.Notifications = Notifications; + this.StateManager = StateManager; + + this.formValues = { + enableHostManagementFeatures: false, + allowVolumeBrowserForRegularUsers: false, + disableBindMountsForRegularUsers: false, + disablePrivilegedModeForRegularUsers: false, + disableHostNamespaceForRegularUsers: false, + disableStackManagementForRegularUsers: false, + disableDeviceMappingForRegularUsers: false, + disableContainerCapabilitiesForRegularUsers: false, + }; + + this.isAgent = false; + + this.state = { + actionInProgress: false, + }; + + this.save = this.save.bind(this); + } + + isContainerEditDisabled() { + const { + disableBindMountsForRegularUsers, + disableHostNamespaceForRegularUsers, + disablePrivilegedModeForRegularUsers, + disableDeviceMappingForRegularUsers, + disableContainerCapabilitiesForRegularUsers, + } = this.formValues; + return ( + disableBindMountsForRegularUsers || + disableHostNamespaceForRegularUsers || + disablePrivilegedModeForRegularUsers || + disableDeviceMappingForRegularUsers || + disableContainerCapabilitiesForRegularUsers + ); + } + + async save() { + return this.$async(async () => { + try { + this.state.actionInProgress = true; + const securitySettings = { + enableHostManagementFeatures: this.formValues.enableHostManagementFeatures, + allowBindMountsForRegularUsers: !this.formValues.disableBindMountsForRegularUsers, + allowPrivilegedModeForRegularUsers: !this.formValues.disablePrivilegedModeForRegularUsers, + allowVolumeBrowserForRegularUsers: this.formValues.allowVolumeBrowserForRegularUsers, + allowHostNamespaceForRegularUsers: !this.formValues.disableHostNamespaceForRegularUsers, + allowDeviceMappingForRegularUsers: !this.formValues.disableDeviceMappingForRegularUsers, + allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers, + allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers, + }; + + await this.EndpointService.updateSecuritySettings(this.endpoint.Id, securitySettings); + + this.endpoint.SecuritySettings = securitySettings; + this.Notifications.success('Saved settings successfully'); + } catch (e) { + this.Notifications.error('Failure', e, 'Failed saving settings'); + } + this.state.actionInProgress = false; + }); + } + + checkAgent() { + const applicationState = this.StateManager.getState(); + return applicationState.endpoint.mode.agentProxy; + } + + $onInit() { + const securitySettings = this.endpoint.SecuritySettings; + + const isAgent = this.checkAgent(); + this.isAgent = isAgent; + + this.formValues = { + enableHostManagementFeatures: isAgent && securitySettings.enableHostManagementFeatures, + allowVolumeBrowserForRegularUsers: isAgent && securitySettings.allowVolumeBrowserForRegularUsers, + disableBindMountsForRegularUsers: !securitySettings.allowBindMountsForRegularUsers, + disablePrivilegedModeForRegularUsers: !securitySettings.allowPrivilegedModeForRegularUsers, + disableHostNamespaceForRegularUsers: !securitySettings.allowHostNamespaceForRegularUsers, + disableDeviceMappingForRegularUsers: !securitySettings.allowDeviceMappingForRegularUsers, + disableStackManagementForRegularUsers: !securitySettings.allowStackManagementForRegularUsers, + disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers, + }; + } +} diff --git a/app/docker/views/docker-features-configuration/docker-features-configuration.html b/app/docker/views/docker-features-configuration/docker-features-configuration.html new file mode 100644 index 000000000..0c5f033de --- /dev/null +++ b/app/docker/views/docker-features-configuration/docker-features-configuration.html @@ -0,0 +1,137 @@ + + + Docker configuration + + +
+
+ + +
+
+ Host and Filesystem +
+
+ + + These features are only available for an Agent enabled endpoints. + +
+
+
+ +
+
+
+
+ +
+
+ +
+ Docker Security Settings +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+ + + Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings. + +
+ + + +
+ Actions +
+
+
+ +
+
+ +
+
+
+
+
diff --git a/app/docker/views/docker-features-configuration/index.js b/app/docker/views/docker-features-configuration/index.js new file mode 100644 index 000000000..5aec2152e --- /dev/null +++ b/app/docker/views/docker-features-configuration/index.js @@ -0,0 +1,11 @@ +import angular from 'angular'; + +import controller from './docker-features-configuration.controller'; + +angular.module('portainer.docker').component('dockerFeaturesConfigurationView', { + templateUrl: './docker-features-configuration.html', + controller, + bindings: { + endpoint: '<', + }, +}); diff --git a/app/docker/views/host/host-view-controller.js b/app/docker/views/host/host-view-controller.js index ebb5a6975..b7433ed0f 100644 --- a/app/docker/views/host/host-view-controller.js +++ b/app/docker/views/host/host-view-controller.js @@ -29,7 +29,7 @@ angular.module('portainer.docker').controller('HostViewController', [ ctrl.state.isAdmin = Authentication.isAdmin(); var agentApiVersion = applicationState.endpoint.agentApiVersion; ctrl.state.agentApiVersion = agentApiVersion; - ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures; + ctrl.state.enableHostManagementFeatures = ctrl.endpoint.SecuritySettings.enableHostManagementFeatures; $q.all({ version: SystemService.version(), diff --git a/app/docker/views/host/host-view.js b/app/docker/views/host/host-view.js index eec3d03e5..db3904bb0 100644 --- a/app/docker/views/host/host-view.js +++ b/app/docker/views/host/host-view.js @@ -1,4 +1,7 @@ angular.module('portainer.docker').component('hostView', { templateUrl: './host-view.html', controller: 'HostViewController', + bindings: { + endpoint: '<', + }, }); diff --git a/app/docker/views/nodes/node-details/node-details-view-controller.js b/app/docker/views/nodes/node-details/node-details-view-controller.js index 020c3b586..bed849412 100644 --- a/app/docker/views/nodes/node-details/node-details-view-controller.js +++ b/app/docker/views/nodes/node-details/node-details-view-controller.js @@ -20,7 +20,7 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [ var applicationState = StateManager.getState(); ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy; ctrl.state.isAdmin = Authentication.isAdmin(); - ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures; + ctrl.state.enableHostManagementFeatures = ctrl.endpoint.SecuritySettings.enableHostManagementFeatures; var fetchJobs = ctrl.state.isAdmin && ctrl.state.isAgent; diff --git a/app/docker/views/nodes/node-details/node-details-view.js b/app/docker/views/nodes/node-details/node-details-view.js index 929b4f4ad..be55e2a80 100644 --- a/app/docker/views/nodes/node-details/node-details-view.js +++ b/app/docker/views/nodes/node-details/node-details-view.js @@ -1,4 +1,7 @@ angular.module('portainer.docker').component('nodeDetailsView', { templateUrl: './node-details-view.html', controller: 'NodeDetailsViewController', + bindings: { + endpoint: '<', + }, }); diff --git a/app/docker/views/services/create/createServiceController.js b/app/docker/views/services/create/createServiceController.js index a59d74207..d61cfdfff 100644 --- a/app/docker/views/services/create/createServiceController.js +++ b/app/docker/views/services/create/createServiceController.js @@ -30,9 +30,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [ 'RegistryService', 'HttpRequestHelper', 'NodeService', - 'SettingsService', 'WebhookService', 'EndpointProvider', + 'endpoint', function ( $q, $scope, @@ -56,9 +56,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [ RegistryService, HttpRequestHelper, NodeService, - SettingsService, WebhookService, - EndpointProvider + EndpointProvider, + endpoint ) { $scope.formValues = { Name: '', @@ -593,10 +593,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [ async function checkIfAllowedBindMounts() { const isAdmin = Authentication.isAdmin(); - const settings = await SettingsService.publicSettings(); - const { AllowBindMountsForRegularUsers } = settings; + const { allowBindMountsForRegularUsers } = endpoint.SecuritySettings; - return isAdmin || AllowBindMountsForRegularUsers; + return isAdmin || allowBindMountsForRegularUsers; } }, ]); diff --git a/app/docker/views/services/edit/serviceController.js b/app/docker/views/services/edit/serviceController.js index b73938d74..6a44c4d00 100644 --- a/app/docker/views/services/edit/serviceController.js +++ b/app/docker/views/services/edit/serviceController.js @@ -34,7 +34,6 @@ angular.module('portainer.docker').controller('ServiceController', [ 'SecretService', 'ImageService', 'SecretHelper', - 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', @@ -45,7 +44,6 @@ angular.module('portainer.docker').controller('ServiceController', [ 'ModalService', 'PluginService', 'Authentication', - 'SettingsService', 'VolumeService', 'ImageHelper', 'WebhookService', @@ -53,6 +51,7 @@ angular.module('portainer.docker').controller('ServiceController', [ 'clipboard', 'WebhookHelper', 'NetworkService', + 'endpoint', function ( $q, $scope, @@ -67,7 +66,6 @@ angular.module('portainer.docker').controller('ServiceController', [ SecretService, ImageService, SecretHelper, - Service, ServiceHelper, LabelHelper, TaskService, @@ -78,14 +76,14 @@ angular.module('portainer.docker').controller('ServiceController', [ ModalService, PluginService, Authentication, - SettingsService, VolumeService, ImageHelper, WebhookService, EndpointProvider, clipboard, WebhookHelper, - NetworkService + NetworkService, + endpoint ) { $scope.state = { updateInProgress: false, @@ -666,7 +664,6 @@ angular.module('portainer.docker').controller('ServiceController', [ availableImages: ImageService.images(), availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25), availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25), - settings: SettingsService.publicSettings(), webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()), }); }) @@ -677,7 +674,7 @@ angular.module('portainer.docker').controller('ServiceController', [ $scope.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages); $scope.availableLoggingDrivers = data.availableLoggingDrivers; $scope.availableVolumes = data.volumes; - $scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers; + $scope.allowBindMounts = endpoint.SecuritySettings.allowBindMountsForRegularUsers; $scope.isAdmin = Authentication.isAdmin(); $scope.availableNetworks = data.availableNetworks; $scope.swarmNetworks = _.filter($scope.availableNetworks, (network) => network.Scope === 'swarm'); diff --git a/app/docker/views/volumes/volumesController.js b/app/docker/views/volumes/volumesController.js index 8496a9e22..2044adf9d 100644 --- a/app/docker/views/volumes/volumesController.js +++ b/app/docker/views/volumes/volumesController.js @@ -10,7 +10,8 @@ angular.module('portainer.docker').controller('VolumesController', [ 'EndpointProvider', 'Authentication', 'ModalService', - function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ModalService) { + 'endpoint', + function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ModalService, endpoint) { $scope.removeAction = function (selectedItems) { ModalService.confirmDeletion('Do you want to remove the selected volume(s)?', (confirmed) => { if (confirmed) { @@ -75,8 +76,7 @@ angular.module('portainer.docker').controller('VolumesController', [ function initView() { getVolumes(); - $scope.showBrowseAction = - $scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || $scope.applicationState.application.enableVolumeBrowserForNonAdminUsers); + $scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || endpoint.SecuritySettings.allowVolumeBrowserForRegularUsers); } initView(); diff --git a/app/portainer/components/forms/por-switch-field/por-switch-field.css b/app/portainer/components/forms/por-switch-field/por-switch-field.css new file mode 100644 index 000000000..5fa6465b9 --- /dev/null +++ b/app/portainer/components/forms/por-switch-field/por-switch-field.css @@ -0,0 +1,52 @@ +/* switch box */ + +.switch { + --switch-size: 24px; +} + +.switch.small { + --switch-size: 12px; +} + +.switch input { + display: none; +} + +.switch i, +.bootbox-form .checkbox i { + display: inline-block; + vertical-align: middle; + cursor: pointer; + padding-right: var(--switch-size); + transition: all ease 0.2s; + -webkit-transition: all ease 0.2s; + -moz-transition: all ease 0.2s; + -o-transition: all ease 0.2s; + border-radius: var(--switch-size); + box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5); +} + +.switch i:before, +.bootbox-form .checkbox i:before { + display: block; + content: ''; + width: var(--switch-size); + height: var(--switch-size); + border-radius: var(--switch-size); + background: white; + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5); +} + +.switch :checked + i, +.bootbox-form .checkbox :checked ~ i { + padding-right: 0; + padding-left: var(--switch-size); + -webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; + -moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; + box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; +} + +.switch :disabled + i { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/app/portainer/components/forms/por-switch-field/por-switch-field.html b/app/portainer/components/forms/por-switch-field/por-switch-field.html new file mode 100644 index 000000000..d20ae2776 --- /dev/null +++ b/app/portainer/components/forms/por-switch-field/por-switch-field.html @@ -0,0 +1,14 @@ + diff --git a/app/portainer/components/forms/por-switch-field/por-switch-field.js b/app/portainer/components/forms/por-switch-field/por-switch-field.js new file mode 100644 index 000000000..800604209 --- /dev/null +++ b/app/portainer/components/forms/por-switch-field/por-switch-field.js @@ -0,0 +1,18 @@ +import angular from 'angular'; + +import './por-switch-field.css'; + +export const porSwitchField = { + templateUrl: './por-switch-field.html', + bindings: { + tooltip: '@', + ngModel: '=', + label: '@', + name: '@', + labelClass: '@', + disabled: '<', + onChange: '<', + }, +}; + +angular.module('portainer.app').component('porSwitchField', porSwitchField); diff --git a/app/portainer/components/forms/por-switch/por-switch.html b/app/portainer/components/forms/por-switch/por-switch.html new file mode 100644 index 000000000..92e8ab1fe --- /dev/null +++ b/app/portainer/components/forms/por-switch/por-switch.html @@ -0,0 +1,3 @@ + diff --git a/app/portainer/components/forms/por-switch/por-switch.js b/app/portainer/components/forms/por-switch/por-switch.js new file mode 100644 index 000000000..5382b437f --- /dev/null +++ b/app/portainer/components/forms/por-switch/por-switch.js @@ -0,0 +1,15 @@ +import angular from 'angular'; + +const porSwitch = { + templateUrl: './por-switch.html', + bindings: { + ngModel: '=', + id: '@', + className: '@', + name: '@', + disabled: '<', + onChange: '<', + }, +}; + +angular.module('portainer.app').component('porSwitch', porSwitch); diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 40f0003f4..a15b6ba04 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -4,16 +4,8 @@ export function SettingsViewModel(data) { this.AuthenticationMethod = data.AuthenticationMethod; this.LDAPSettings = data.LDAPSettings; this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings); - this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers; - this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers; - this.AllowVolumeBrowserForRegularUsers = data.AllowVolumeBrowserForRegularUsers; - this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers; - this.AllowDeviceMappingForRegularUsers = data.AllowDeviceMappingForRegularUsers; - this.AllowStackManagementForRegularUsers = data.AllowStackManagementForRegularUsers; - this.AllowContainerCapabilitiesForRegularUsers = data.AllowContainerCapabilitiesForRegularUsers; this.SnapshotInterval = data.SnapshotInterval; this.TemplatesURL = data.TemplatesURL; - this.EnableHostManagementFeatures = data.EnableHostManagementFeatures; this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; this.UserSessionTimeout = data.UserSessionTimeout; @@ -21,15 +13,7 @@ export function SettingsViewModel(data) { } export function PublicSettingsViewModel(settings) { - this.AllowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers; - this.AllowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers; - this.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers; - this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; - this.AllowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers; - this.AllowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers; - this.AllowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers; this.AuthenticationMethod = settings.AuthenticationMethod; - this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures; this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; this.LogoURL = settings.LogoURL; this.OAuthLoginURI = settings.OAuthLoginURI; diff --git a/app/portainer/rest/endpoint.js b/app/portainer/rest/endpoint.js index 65a6b82d4..71842f9a1 100644 --- a/app/portainer/rest/endpoint.js +++ b/app/portainer/rest/endpoint.js @@ -21,6 +21,7 @@ angular.module('portainer.app').factory('Endpoints', [ snapshots: { method: 'POST', params: { action: 'snapshot' } }, snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' } }, status: { method: 'GET', params: { id: '@id', action: 'status' } }, + updateSecuritySettings: { method: 'PUT', params: { id: '@id', action: 'settings' } }, } ); }, diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 43f55201d..a2cc0fef8 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -6,7 +6,9 @@ angular.module('portainer.app').factory('EndpointService', [ 'FileUploadService', function EndpointServiceFactory($q, Endpoints, FileUploadService) { 'use strict'; - var service = {}; + var service = { + updateSecuritySettings, + }; service.endpoint = function (endpointID) { return Endpoints.get({ id: endpointID }).$promise; @@ -146,5 +148,9 @@ angular.module('portainer.app').factory('EndpointService', [ }; return service; + + function updateSecuritySettings(id, securitySettings) { + return Endpoints.updateSecuritySettings({ id }, securitySettings).$promise; + } }, ]); diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index c98185fa9..401ae1422 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -72,51 +72,11 @@ angular.module('portainer.app').factory('StateManager', [ LocalStorage.storeApplicationState(state.application); }; - manager.updateEnableHostManagementFeatures = function (enableHostManagementFeatures) { - state.application.enableHostManagementFeatures = enableHostManagementFeatures; - LocalStorage.storeApplicationState(state.application); - }; - - manager.updateEnableVolumeBrowserForNonAdminUsers = function (enableVolumeBrowserForNonAdminUsers) { - state.application.enableVolumeBrowserForNonAdminUsers = enableVolumeBrowserForNonAdminUsers; - LocalStorage.storeApplicationState(state.application); - }; - manager.updateEnableEdgeComputeFeatures = function updateEnableEdgeComputeFeatures(enableEdgeComputeFeatures) { state.application.enableEdgeComputeFeatures = enableEdgeComputeFeatures; LocalStorage.storeApplicationState(state.application); }; - 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); - }; - - manager.updateAllowStackManagementForRegularUsers = function updateAllowStackManagementForRegularUsers(allowStackManagementForRegularUsers) { - state.application.allowStackManagementForRegularUsers = allowStackManagementForRegularUsers; - LocalStorage.storeApplicationState(state.application); - }; - - manager.updateAllowContainerCapabilitiesForRegularUsers = function updateAllowContainerCapabilitiesForRegularUsers(allowContainerCapabilitiesForRegularUsers) { - state.application.allowContainerCapabilitiesForRegularUsers = allowContainerCapabilitiesForRegularUsers; - LocalStorage.storeApplicationState(state.application); - }; - - manager.updateAllowBindMountsForRegularUsers = function updateAllowBindMountsForRegularUsers(allowBindMountsForRegularUsers) { - state.application.allowBindMountsForRegularUsers = allowBindMountsForRegularUsers; - LocalStorage.storeApplicationState(state.application); - }; - - manager.updateAllowPrivilegedModeForRegularUsers = function (AllowPrivilegedModeForRegularUsers) { - state.application.allowPrivilegedModeForRegularUsers = AllowPrivilegedModeForRegularUsers; - LocalStorage.storeApplicationState(state.application); - }; - manager.updateEnableTelemetry = function updateEnableTelemetry(enableTelemetry) { state.application.enableTelemetry = enableTelemetry; $analytics.setOptOut(!enableTelemetry); @@ -128,15 +88,7 @@ angular.module('portainer.app').factory('StateManager', [ state.application.enableTelemetry = settings.EnableTelemetry; state.application.logo = settings.LogoURL; state.application.snapshotInterval = settings.SnapshotInterval; - state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures; - state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers; state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; - state.application.allowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; - state.application.allowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers; - state.application.allowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers; - state.application.allowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers; - state.application.allowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers; - state.application.allowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers; state.application.validity = moment().unix(); } diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 01008afb3..39644cb58 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -76,100 +76,7 @@ -
- Host and Filesystem -
-
-
- - -
-
-
-
- - -
-
- - -
- Docker Endpoint Security Options -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings. - -
-
Edge Compute diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index 5bd78d9f0..86b352ced 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -25,33 +25,12 @@ angular.module('portainer.app').controller('SettingsController', [ $scope.formValues = { customLogo: false, - restrictBindMounts: false, - restrictPrivilegedMode: false, labelName: '', labelValue: '', - enableHostManagementFeatures: false, - enableVolumeBrowser: false, enableEdgeComputeFeatures: false, - restrictHostNamespaceForRegularUsers: false, - allowDeviceMappingForRegularUsers: false, - allowStackManagementForRegularUsers: false, - disableContainerCapabilitiesForRegularUsers: false, enableTelemetry: false, }; - $scope.isContainerEditDisabled = function isContainerEditDisabled() { - const { - restrictBindMounts, - restrictHostNamespaceForRegularUsers, - restrictPrivilegedMode, - disableDeviceMappingForRegularUsers, - disableContainerCapabilitiesForRegularUsers, - } = this.formValues; - return ( - restrictBindMounts || restrictHostNamespaceForRegularUsers || restrictPrivilegedMode || disableDeviceMappingForRegularUsers || disableContainerCapabilitiesForRegularUsers - ); - }; - $scope.removeFilteredContainerLabel = function (index) { var settings = $scope.settings; settings.BlackListedLabels.splice(index, 1); @@ -77,15 +56,7 @@ angular.module('portainer.app').controller('SettingsController', [ settings.LogoURL = ''; } - settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts; - settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode; - settings.AllowVolumeBrowserForRegularUsers = $scope.formValues.enableVolumeBrowser; - settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures; settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures; - settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers; - settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers; - settings.AllowStackManagementForRegularUsers = !$scope.formValues.disableStackManagementForRegularUsers; - settings.AllowContainerCapabilitiesForRegularUsers = !$scope.formValues.disableContainerCapabilitiesForRegularUsers; settings.EnableTelemetry = $scope.formValues.enableTelemetry; $scope.state.actionInProgress = true; @@ -98,15 +69,7 @@ angular.module('portainer.app').controller('SettingsController', [ Notifications.success('Settings updated'); StateManager.updateLogo(settings.LogoURL); StateManager.updateSnapshotInterval(settings.SnapshotInterval); - StateManager.updateEnableHostManagementFeatures(settings.EnableHostManagementFeatures); - StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers); - StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers); StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures); - StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers); - StateManager.updateAllowStackManagementForRegularUsers(settings.AllowStackManagementForRegularUsers); - StateManager.updateAllowContainerCapabilitiesForRegularUsers(settings.AllowContainerCapabilitiesForRegularUsers); - StateManager.updateAllowPrivilegedModeForRegularUsers(settings.AllowPrivilegedModeForRegularUsers); - StateManager.updateAllowBindMountsForRegularUsers(settings.AllowBindMountsForRegularUsers); StateManager.updateEnableTelemetry(settings.EnableTelemetry); $state.reload(); }) @@ -127,15 +90,7 @@ angular.module('portainer.app').controller('SettingsController', [ if (settings.LogoURL !== '') { $scope.formValues.customLogo = true; } - $scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers; - $scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers; - $scope.formValues.enableVolumeBrowser = settings.AllowVolumeBrowserForRegularUsers; - $scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures; $scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; - $scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers; - $scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers; - $scope.formValues.disableStackManagementForRegularUsers = !settings.AllowStackManagementForRegularUsers; - $scope.formValues.disableContainerCapabilitiesForRegularUsers = !settings.AllowContainerCapabilitiesForRegularUsers; $scope.formValues.enableTelemetry = settings.EnableTelemetry; }) .catch(function error(err) { diff --git a/app/portainer/views/sidebar/sidebarController.js b/app/portainer/views/sidebar/sidebarController.js index 24b357fec..6e1d8ecef 100644 --- a/app/portainer/views/sidebar/sidebarController.js +++ b/app/portainer/views/sidebar/sidebarController.js @@ -46,9 +46,17 @@ angular.module('portainer.app').controller('SidebarController', [ async function shouldShowStacks() { const isAdmin = Authentication.isAdmin(); - const { allowStackManagementForRegularUsers } = $scope.applicationState.application; - return isAdmin || allowStackManagementForRegularUsers; + if (isAdmin) { + return true; + } + + const endpoint = EndpointProvider.currentEndpoint(); + if (!endpoint || !endpoint.SecuritySettings) { + return false; + } + + return endpoint.SecuritySettings.allowStackManagementForRegularUsers; } $transitions.onEnter({}, async () => { diff --git a/app/portainer/views/stacks/stacksController.js b/app/portainer/views/stacks/stacksController.js index c80bb3496..32506459c 100644 --- a/app/portainer/views/stacks/stacksController.js +++ b/app/portainer/views/stacks/stacksController.js @@ -1,7 +1,7 @@ angular.module('portainer.app').controller('StacksController', StacksController); /* @ngInject */ -function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, StateManager) { +function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, endpoint) { $scope.removeAction = function (selectedItems) { ModalService.confirmDeletion('Do you want to remove the selected stack(s)? Associated services will be removed as well.', function onConfirm(confirmed) { if (!confirmed) { @@ -55,8 +55,7 @@ function StacksController($scope, $state, Notifications, StackService, ModalServ } async function loadCreateEnabled() { - const appState = StateManager.getState().application; - return appState.allowStackManagementForRegularUsers || Authentication.isAdmin(); + return endpoint.SecuritySettings.allowStackManagementForRegularUsers || Authentication.isAdmin(); } async function initView() { diff --git a/app/portainer/views/templates/templatesController.js b/app/portainer/views/templates/templatesController.js index 04f9d224a..1b27ae9af 100644 --- a/app/portainer/views/templates/templatesController.js +++ b/app/portainer/views/templates/templatesController.js @@ -16,8 +16,8 @@ angular.module('portainer.app').controller('TemplatesController', [ 'ResourceControlService', 'Authentication', 'FormValidator', - 'SettingsService', 'StackService', + 'endpoint', function ( $scope, $q, @@ -33,8 +33,8 @@ angular.module('portainer.app').controller('TemplatesController', [ ResourceControlService, Authentication, FormValidator, - SettingsService, - StackService + StackService, + endpoint ) { $scope.state = { selectedTemplate: null, @@ -263,7 +263,6 @@ angular.module('portainer.app').controller('TemplatesController', [ false, endpointMode.provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25 ), - settings: SettingsService.publicSettings(), }) .then(function success(data) { var templates = data.templates; @@ -271,8 +270,7 @@ angular.module('portainer.app').controller('TemplatesController', [ $scope.availableVolumes = _.orderBy(data.volumes.Volumes, [(volume) => volume.Name.toLowerCase()], ['asc']); var networks = data.networks; $scope.availableNetworks = networks; - var settings = data.settings; - $scope.allowBindMounts = settings.AllowBindMountsForRegularUsers; + $scope.allowBindMounts = endpoint.SecuritySettings.allowBindMountsForRegularUsers; }) .catch(function error(err) { $scope.templates = [];