From fa9eeaf3b1807389bfde903950561d1664eb90cd Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 27 Jul 2020 10:11:32 +0300 Subject: [PATCH] feat(settings): introduce disable stack management setting (#4100) * feat(stacks): add a setting to disable the creation of stacks for non-admin users * feat(settings): introduce a setting to prevent non-admin from stack creation * feat(settings): update stack creation setting * feat(settings): fail stack creation if user is non admin * fix(settings): save preventStackCreation setting to state * feat(stacks): disable add button when settings is enabled * format(stacks): remove line * feat(stacks): setting to hide stacks from users * feat(settings): rename disable stacks setting * refactor(settings): rename setting to disableStackManagementForRegularUsers * feat(settings): hide stacks for non admin when settings is set * refactor(settings): replace disableDeviceMapping with allow * feat(dashboard): hide stacks if settings disabled and non admin * refactor(sidebar): check if user is endpoint admin * feat(settings): set the default value for stack management * feat(settings): rename field label * fix(sidebar): refresh show stacks state * fix(docker): hide stacks when not admin --- api/bolt/init.go | 21 +-- api/bolt/migrator/migrate_dbversion23.go | 1 + api/http/handler/settings/settings_public.go | 42 +++--- api/http/handler/settings/settings_update.go | 39 +++--- api/http/handler/stacks/handler.go | 32 ++--- api/http/handler/stacks/stack_create.go | 23 +++ api/portainer.go | 33 ++--- .../docker-sidebar-content.js | 1 + .../dockerSidebarContent.html | 2 +- app/docker/views/dashboard/dashboard.html | 2 +- .../views/dashboard/dashboardController.js | 22 ++- .../stacks-datatable/stacksDatatable.html | 9 +- .../stacks-datatable/stacksDatatable.js | 1 + app/portainer/models/settings.js | 4 +- app/portainer/services/stateManager.js | 6 + app/portainer/views/settings/settings.html | 10 ++ .../views/settings/settingsController.js | 4 + app/portainer/views/sidebar/sidebar.html | 1 + .../views/sidebar/sidebarController.js | 25 +++- app/portainer/views/stacks/stacks.html | 1 + .../views/stacks/stacksController.js | 132 ++++++++++-------- 21 files changed, 264 insertions(+), 147 deletions(-) diff --git a/api/bolt/init.go b/api/bolt/init.go index 1369e6a1e..885966bbe 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -24,16 +24,17 @@ func (store *Store) Init() error { portainer.LDAPGroupSearchSettings{}, }, }, - OAuthSettings: portainer.OAuthSettings{}, - AllowBindMountsForRegularUsers: true, - AllowPrivilegedModeForRegularUsers: true, - AllowVolumeBrowserForRegularUsers: false, - AllowHostNamespaceForRegularUsers: true, - AllowDeviceMappingForRegularUsers: true, - EnableHostManagementFeatures: false, - EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, - TemplatesURL: portainer.DefaultTemplatesURL, - UserSessionTimeout: portainer.DefaultUserSessionTimeout, + OAuthSettings: portainer.OAuthSettings{}, + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowVolumeBrowserForRegularUsers: false, + AllowHostNamespaceForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + EnableHostManagementFeatures: false, + EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, + TemplatesURL: portainer.DefaultTemplatesURL, + UserSessionTimeout: portainer.DefaultUserSessionTimeout, } err = store.SettingsService.UpdateSettings(defaultSettings) diff --git a/api/bolt/migrator/migrate_dbversion23.go b/api/bolt/migrator/migrate_dbversion23.go index ba38987c5..856e77856 100644 --- a/api/bolt/migrator/migrate_dbversion23.go +++ b/api/bolt/migrator/migrate_dbversion23.go @@ -8,6 +8,7 @@ func (m *Migrator) updateSettingsToDB24() error { legacySettings.AllowHostNamespaceForRegularUsers = true legacySettings.AllowDeviceMappingForRegularUsers = true + legacySettings.AllowStackManagementForRegularUsers = true return m.settingsService.UpdateSettings(legacySettings) } diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 097c14676..6bd3a581b 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -6,20 +6,21 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type publicSettingsResponse struct { - 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"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - 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"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + OAuthLoginURI string `json:"OAuthLoginURI"` } // GET request on /api/settings/public @@ -30,15 +31,16 @@ 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, - EnableHostManagementFeatures: settings.EnableHostManagementFeatures, - EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, + LogoURL: settings.LogoURL, + AuthenticationMethod: settings.AuthenticationMethod, + AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, + AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, + AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, + AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, + AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, + AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, + EnableHostManagementFeatures: settings.EnableHostManagementFeatures, + EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, 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 d92307b08..59fbffd28 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -9,28 +9,29 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/filesystem" ) 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 - EnableHostManagementFeatures *bool - SnapshotInterval *string - TemplatesURL *string - EdgeAgentCheckinInterval *int - EnableEdgeComputeFeatures *bool - UserSessionTimeout *string + 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 + EnableHostManagementFeatures *bool + SnapshotInterval *string + TemplatesURL *string + EdgeAgentCheckinInterval *int + EnableEdgeComputeFeatures *bool + UserSessionTimeout *string } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -131,6 +132,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers } + if payload.AllowStackManagementForRegularUsers != nil { + settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers + } + 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 2be3ddc70..cf80b4ecd 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -58,8 +58,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { } func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID, resourceControl *portainer.ResourceControl) (bool, error) { - if securityContext.IsAdmin { - return true, nil + user, err := handler.DataStore.User().User(securityContext.UserID) + if err != nil { + return false, err } userTeamIDs := make([]portainer.TeamID, 0) @@ -71,23 +72,7 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR return true, nil } - _, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) - if err == bolterrors.ErrObjectNotFound { - return false, nil - } else if err != nil && err != bolterrors.ErrObjectNotFound { - return false, err - } - - user, err := handler.DataStore.User().User(securityContext.UserID) - if err != nil { - return false, err - } - - _, ok := user.EndpointAuthorizations[endpointID][portainer.EndpointResourcesAccess] - if ok { - return true, nil - } - return false, nil + return handler.userIsAdminOrEndpointAdmin(user, endpointID) } func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) { @@ -109,3 +94,12 @@ func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpoin return endpointResourceAccess, nil } + +func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID) (bool, error) { + user, err := handler.DataStore.User().User(securityContext.UserID) + if err != nil { + return false, err + } + + return handler.userIsAdminOrEndpointAdmin(user, endpointID) +} diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index ba3b5388d..23e18bf5c 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -46,6 +46,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.DataStore.Settings().Settings() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} + } + + if !settings.AllowStackManagementForRegularUsers { + 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.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} diff --git a/api/portainer.go b/api/portainer.go index d503e0bbf..a60f244bd 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -510,22 +510,23 @@ 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"` - SnapshotInterval string `json:"SnapshotInterval"` - TemplatesURL string `json:"TemplatesURL"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - UserSessionTimeout string `json:"UserSessionTimeout"` + 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"` + SnapshotInterval string `json:"SnapshotInterval"` + TemplatesURL string `json:"TemplatesURL"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + UserSessionTimeout string `json:"UserSessionTimeout"` // 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 87734c8a2..088165ac3 100644 --- a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js +++ b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js @@ -9,5 +9,6 @@ angular.module('portainer.docker').component('dockerSidebarContent', { toggle: '<', currentRouteName: '<', endpointId: '<', + showStacks: '<', }, }); diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html index 15d739ae2..5af425761 100644 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html @@ -8,7 +8,7 @@ Custom Templates -