diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index e70125afe..0f207b240 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -10,15 +10,16 @@ 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"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - ExternalTemplates bool `json:"ExternalTemplates"` - OAuthLoginURI string `json:"OAuthLoginURI"` + LogoURL string `json:"LogoURL"` + AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + ExternalTemplates bool `json:"ExternalTemplates"` + OAuthLoginURI string `json:"OAuthLoginURI"` + DisableStackManagementForRegularUsers bool `json:"DisableStackManagementForRegularUsers"` } // GET request on /api/settings/public @@ -42,6 +43,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * settings.OAuthSettings.ClientID, settings.OAuthSettings.RedirectURI, settings.OAuthSettings.Scopes), + DisableStackManagementForRegularUsers: settings.DisableStackManagementForRegularUsers, } if settings.TemplatesURL != "" { diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index cef3bdf0f..242a115c3 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -12,19 +12,20 @@ import ( ) type settingsUpdatePayload struct { - LogoURL *string - BlackListedLabels []portainer.Pair - AuthenticationMethod *int - LDAPSettings *portainer.LDAPSettings - OAuthSettings *portainer.OAuthSettings - AllowBindMountsForRegularUsers *bool - AllowPrivilegedModeForRegularUsers *bool - AllowVolumeBrowserForRegularUsers *bool - EnableHostManagementFeatures *bool - SnapshotInterval *string - TemplatesURL *string - EdgeAgentCheckinInterval *int - EnableEdgeComputeFeatures *bool + LogoURL *string + BlackListedLabels []portainer.Pair + AuthenticationMethod *int + LDAPSettings *portainer.LDAPSettings + OAuthSettings *portainer.OAuthSettings + AllowBindMountsForRegularUsers *bool + AllowPrivilegedModeForRegularUsers *bool + AllowVolumeBrowserForRegularUsers *bool + EnableHostManagementFeatures *bool + SnapshotInterval *string + TemplatesURL *string + EdgeAgentCheckinInterval *int + EnableEdgeComputeFeatures *bool + DisableStackManagementForRegularUsers *bool } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -114,6 +115,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures } + if payload.DisableStackManagementForRegularUsers != nil { + settings.DisableStackManagementForRegularUsers = *payload.DisableStackManagementForRegularUsers + } + if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval { err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval) if err != nil { diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index d0a6b4ea5..e9c6c241c 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -87,3 +87,27 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR } return false, nil } + +func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID) (bool, error) { + if securityContext.IsAdmin { + return true, nil + } + + _, err := handler.ExtensionService.Extension(portainer.RBACExtension) + if err == portainer.ErrObjectNotFound { + return false, nil + } else if err != nil && err != portainer.ErrObjectNotFound { + return false, err + } + + user, err := handler.UserService.User(securityContext.UserID) + if err != nil { + return false, err + } + + _, ok := user.EndpointAuthorizations[endpointID][portainer.EndpointResourcesAccess] + if ok { + return true, nil + } + return false, nil +} diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 43831a200..229b3fce9 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -43,6 +43,29 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} } + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} + } + + if settings.DisableStackManagementForRegularUsers { + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err} + } + + canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpointID)) + + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack creation", err} + } + + if !canCreate { + errMsg := "Stack creation is disabled for non-admin users" + return &httperror.HandlerError{http.StatusForbidden, errMsg, errors.New(errMsg)} + } + } + endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/portainer.go b/api/portainer.go index 22806c191..471c22928 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -420,19 +420,20 @@ 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"` - SnapshotInterval string `json:"SnapshotInterval"` - TemplatesURL string `json:"TemplatesURL"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + 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"` + SnapshotInterval string `json:"SnapshotInterval"` + TemplatesURL string `json:"TemplatesURL"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + DisableStackManagementForRegularUsers bool `json:"DisableStackManagementForRegularUsers"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js index b57e4cdcb..5c3eaac3f 100644 --- a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js +++ b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js @@ -6,5 +6,6 @@ angular.module('portainer.docker').component('dockerSidebarContent', { standaloneManagement: '<', adminAccess: '<', offlineMode: '<', + hideStacks: '<', }, }); diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html index c5aec5434..2b7915c6a 100644 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html @@ -4,7 +4,7 @@ -