feat(endpoint): relocate docker security settings (#4657)

* feat(endpoint): migrate security settings to endpoint

* feat(endpoint): check for specific endpoint settings

* feat(endpoint): check security settings

* feat(docker): add config page

* feat(endpoint): save settings page

* feat(endpoints): disable features when not agent

* feat(sidebar): hide docker settings for regular user

* fix(docker): small fixes in configs

* fix(volumes): hide browse button for non admins

* refactor(docker): introduce switch component

* refactor(components/switch): seprate label from switch

* feat(app/components): align switch label

* refactor(app/components): move switch css

* fix(docker/settings): add ngijnect

* feat(endpoints): set default security values

* style(portainer): sort types

* fix(endpoint): rename security heading

* fix(endpoints): update endpoints settings
pull/4841/head
Chaim Lev-Ari 2021-02-09 10:09:06 +02:00 committed by GitHub
parent e401724d43
commit 46dec01fe3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 714 additions and 461 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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")
}
}

View File

@ -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")
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
},
]);

View File

@ -37,7 +37,15 @@
</li>
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
<a ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.swarm'].includes($ctrl.currentRouteName)">
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
</li>
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement">
<a ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.host'].includes($ctrl.currentRouteName)">
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
</li>

View File

@ -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();

View File

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

View File

@ -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();

View File

@ -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,
};
}
}

View File

@ -0,0 +1,137 @@
<rd-header>
<rd-header-title title-text="Docker features configuration"></rd-header-title>
<rd-header-content>Docker configuration</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="$ctrl.form">
<div class="col-sm-12 form-section-title">
Host and Filesystem
</div>
<div ng-if="!$ctrl.isAgent" class="form-group">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
These features are only available for an Agent enabled endpoints.
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.enableHostManagementFeatures"
name="enableHostManagementFeatures"
label="Enable host management features"
tooltip="Enable host management features: host system browsing and advanced host details."
disabled="!$ctrl.isAgent"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.allowVolumeBrowserForRegularUsers"
name="allowVolumeBrowserForRegularUsers"
label="Enable volume management for non-administrators"
tooltip="When enabled, regular users will be able to use Portainer volume management features."
disabled="!$ctrl.isAgent"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<!-- security -->
<div class="col-sm-12 form-section-title">
Docker Security Settings
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableBindMountsForRegularUsers"
name="disableBindMountsForRegularUsers"
label="Disable bind mounts for non-administrators"
tooltip="When enabled, regular users will not be able to use bind mounts when creating containers."
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disablePrivilegedModeForRegularUsers"
name="disablePrivilegedModeForRegularUsers"
label="Disable privileged mode for non-administrators"
tooltip="When enabled, regular users will not be able to use privileged mode when creating containers."
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableHostNamespaceForRegularUsers"
name="disableHostNamespaceForRegularUsers"
label="Disable the use of host PID 1 for non-administrators"
tooltip="Prevent users from accessing the host filesystem through the host PID namespace."
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableStackManagementForRegularUsers"
name="disableStackManagementForRegularUsers"
label="Disable the use of Stacks for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableDeviceMappingForRegularUsers"
name="disableDeviceMappingForRegularUsers"
label="Disable device mappings for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableContainerCapabilitiesForRegularUsers"
name="disableContainerCapabilitiesForRegularUsers"
label="Disable container capabilities for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group" ng-if="$ctrl.isContainerEditDisabled()">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings.
</span>
</div>
<!-- !security -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.save()" ng-disabled="$ctrl.state.actionInProgress" button-spinner="$ctrl.state.actionInProgress">
<span ng-hide="$ctrl.state.actionInProgress">Save configuration</span>
<span ng-show="$ctrl.state.actionInProgress">Saving...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -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: '<',
},
});

View File

@ -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(),

View File

@ -1,4 +1,7 @@
angular.module('portainer.docker').component('hostView', {
templateUrl: './host-view.html',
controller: 'HostViewController',
bindings: {
endpoint: '<',
},
});

View File

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

View File

@ -1,4 +1,7 @@
angular.module('portainer.docker').component('nodeDetailsView', {
templateUrl: './node-details-view.html',
controller: 'NodeDetailsViewController',
bindings: {
endpoint: '<',
},
});

View File

@ -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;
}
},
]);

View File

@ -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');

View File

@ -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();

View File

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

View File

@ -0,0 +1,14 @@
<label style="display: flex; align-items: center; margin: 0;">
<span for="toggle_{{::$ctrl.name}}" class="control-label text-left space-right" ng-class="$ctrl.labelClass" style="padding: 0;">
{{::$ctrl.label}}
<portainer-tooltip ng-if="$ctrl.tooltip" position="bottom" message="{{::$ctrl.tooltip}}"></portainer-tooltip>
</span>
<por-switch
class-name="space-right"
name="toggle_{{::$ctrl.name}}"
id="toggle_{{::$ctrl.name}}"
ng-model="$ctrl.ngModel"
disabled="$ctrl.disabled"
on-change="($ctrl.onChange)"
></por-switch>
</label>

View File

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

View File

@ -0,0 +1,3 @@
<label class="switch" ng-class="$ctrl.className" style="margin-bottom: 0;">
<input type="checkbox" name="{{::$ctrl.name}}" id="{{::$ctrl.id}}" ng-model="$ctrl.ngModel" ng-disabled="$ctrl.disabled" ng-change="$ctrl.onChange($ctrl.ngModel)" /><i></i>
</label>

View File

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

View File

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

View File

@ -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' } },
}
);
},

View File

@ -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;
}
},
]);

View File

@ -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();
}

View File

@ -76,100 +76,7 @@
</div>
<!-- !templates -->
<!-- host-filesystem -->
<div class="col-sm-12 form-section-title">
Host and Filesystem
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_enableHostManagementFeatures" class="control-label text-left">
Enable host management features
<portainer-tooltip position="bottom" message="Enable host management features: host system browsing and advanced host details."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_enableHostManagementFeatures" ng-model="formValues.enableHostManagementFeatures" /><i></i>
</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_allowvolumebrowser" class="control-label text-left">
Enable volume management for non-administrators
<portainer-tooltip position="bottom" message="When enabled, regular users will be able to use Portainer volume management features."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_allowvolumebrowser" ng-model="formValues.enableVolumeBrowser" /><i></i> </label>
</div>
</div>
<!-- !host-filesystem -->
<!-- security -->
<div class="col-sm-12 form-section-title">
Docker Endpoint Security Options
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_allowbindmounts" class="control-label text-left">
Disable bind mounts for non-administrators
<portainer-tooltip position="bottom" message="When enabled, regular users will not be able to use bind mounts when creating containers."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_allowbindmounts" ng-model="formValues.restrictBindMounts" /><i></i> </label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_allowbindmounts" class="control-label text-left">
Disable privileged mode for non-administrators
<portainer-tooltip position="bottom" message="When enabled, regular users will not be able to use privileged mode when creating containers."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_allowbindmounts" ng-model="formValues.restrictPrivilegedMode" /><i></i> </label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_allowHostNamespaceForRegularUsers" class="control-label text-left">
Disable the use of host PID 1 for non-administrators
<portainer-tooltip position="bottom" message="Prevent users from accessing the host filesystem through the host PID namespace."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_allowHostNamespaceForRegularUsers" ng-model="formValues.restrictHostNamespaceForRegularUsers" /><i></i>
</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_disableStackManagementForRegularUsers" class="control-label text-left">
Disable the use of Stacks for non-administrators
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_disableStackManagementForRegularUsers" ng-model="formValues.disableStackManagementForRegularUsers" /><i></i>
</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_disableDeviceMappingForRegularUsers" class="control-label text-left">
Disable device mappings for non-administrators
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_disableDeviceMappingForRegularUsers" ng-model="formValues.disableDeviceMappingForRegularUsers" /><i></i>
</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_disableContainerCapabilitiesForRegularUsers" class="control-label text-left">
Disable container capabilities for non-administrators
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_disableContainerCapabilitiesForRegularUsers" ng-model="formValues.disableContainerCapabilitiesForRegularUsers" /><i></i>
</label>
</div>
</div>
<div class="form-group" ng-if="isContainerEditDisabled()">
<span class="col-sm-12 text-muted small">
Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings.
</span>
</div>
<!-- !security -->
<!-- edge -->
<div class="col-sm-12 form-section-title">
Edge Compute

View File

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

View File

@ -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 () => {

View File

@ -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() {

View File

@ -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 = [];