From 9aa52a69753e76ee1725eaff03850a93117d0433 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 8 Oct 2019 13:17:58 +1300 Subject: [PATCH] feat(settings): add new settings to disable volume browser (#3239) * feat(settings): add new settings to disable volume browser * feat(api): update setting to be compliant with RBAC * refactor(api): update method comment * fix(api): remove volume browsing authorizations by default * feat(settings): rewrite volume management setting description * feat(settings): rewrite volume management setting tooltip * Update app/portainer/views/settings/settings.html Co-Authored-By: William --- api/authorizations.go | 46 ++++++++++++++ api/bolt/init.go | 9 --- api/bolt/migrator/migrate_dbversion19.go | 11 ++++ api/bolt/migrator/migrator.go | 5 ++ api/bolt/role/role.go | 14 +++-- api/cmd/portainer/main.go | 13 ++-- api/http/handler/extensions/upgrade.go | 12 ---- api/http/handler/settings/handler.go | 13 ++-- api/http/handler/settings/settings_public.go | 2 + api/http/handler/settings/settings_update.go | 35 +++++++++++ api/http/proxy/docker_transport.go | 60 ++++++++++++++++++- api/http/proxy/factory.go | 2 + api/http/proxy/factory_local.go | 1 + api/http/proxy/factory_local_windows.go | 1 + api/http/proxy/manager.go | 2 + api/http/server.go | 4 ++ api/portainer.go | 6 +- .../volumes-datatable/volumesDatatable.html | 8 ++- app/docker/views/volumes/volumes.html | 2 +- app/docker/views/volumes/volumesController.js | 16 ++++- app/portainer/models/settings.js | 2 + app/portainer/services/stateManager.js | 6 ++ app/portainer/views/settings/settings.html | 11 ++++ .../views/settings/settingsController.js | 6 +- 24 files changed, 239 insertions(+), 48 deletions(-) diff --git a/api/authorizations.go b/api/authorizations.go index c5dc5c967..e2101bdae 100644 --- a/api/authorizations.go +++ b/api/authorizations.go @@ -56,6 +56,52 @@ func DefaultPortainerAuthorizations() Authorizations { } } +// UpdateVolumeBrowsingAuthorizations will update all the volume browsing authorizations for each role (except endpoint administrator) +// based on the specified removeAuthorizations parameter. If removeAuthorizations is set to true, all +// the authorizations will be dropped for the each role. If removeAuthorizations is set to false, the authorizations +// will be reset based for each role. +func (service AuthorizationService) UpdateVolumeBrowsingAuthorizations(remove bool) error { + roles, err := service.roleService.Roles() + if err != nil { + return err + } + + for _, role := range roles { + // all roles except endpoint administrator + if role.ID != RoleID(1) { + updateRoleVolumeBrowsingAuthorizations(&role, remove) + + err := service.roleService.UpdateRole(role.ID, &role) + if err != nil { + return err + } + } + } + + return nil +} + +func updateRoleVolumeBrowsingAuthorizations(role *Role, removeAuthorizations bool) { + if !removeAuthorizations { + delete(role.Authorizations, OperationDockerAgentBrowseDelete) + delete(role.Authorizations, OperationDockerAgentBrowseGet) + delete(role.Authorizations, OperationDockerAgentBrowseList) + delete(role.Authorizations, OperationDockerAgentBrowsePut) + delete(role.Authorizations, OperationDockerAgentBrowseRename) + return + } + + role.Authorizations[OperationDockerAgentBrowseGet] = true + role.Authorizations[OperationDockerAgentBrowseList] = true + + // Standard-user + if role.ID == RoleID(3) { + role.Authorizations[OperationDockerAgentBrowseDelete] = true + role.Authorizations[OperationDockerAgentBrowsePut] = true + role.Authorizations[OperationDockerAgentBrowseRename] = true + } +} + // RemoveTeamAccessPolicies will remove all existing access policies associated to the specified team func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) error { endpoints, err := service.endpointService.Endpoints() diff --git a/api/bolt/init.go b/api/bolt/init.go index b8b731bf2..a9b941179 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -218,8 +218,6 @@ func (store *Store) Init() error { portainer.OperationDockerAgentPing: true, portainer.OperationDockerAgentList: true, portainer.OperationDockerAgentHostInfo: true, - portainer.OperationDockerAgentBrowseGet: true, - portainer.OperationDockerAgentBrowseList: true, portainer.OperationPortainerStackList: true, portainer.OperationPortainerStackInspect: true, portainer.OperationPortainerStackFile: true, @@ -342,11 +340,6 @@ func (store *Store) Init() error { portainer.OperationDockerAgentPing: true, portainer.OperationDockerAgentList: true, portainer.OperationDockerAgentHostInfo: true, - portainer.OperationDockerAgentBrowseDelete: true, - portainer.OperationDockerAgentBrowseGet: true, - portainer.OperationDockerAgentBrowseList: true, - portainer.OperationDockerAgentBrowsePut: true, - portainer.OperationDockerAgentBrowseRename: true, portainer.OperationDockerAgentUndefined: true, portainer.OperationPortainerResourceControlCreate: true, portainer.OperationPortainerResourceControlUpdate: true, @@ -413,8 +406,6 @@ func (store *Store) Init() error { portainer.OperationDockerAgentPing: true, portainer.OperationDockerAgentList: true, portainer.OperationDockerAgentHostInfo: true, - portainer.OperationDockerAgentBrowseGet: true, - portainer.OperationDockerAgentBrowseList: true, portainer.OperationPortainerStackList: true, portainer.OperationPortainerStackInspect: true, portainer.OperationPortainerStackFile: true, diff --git a/api/bolt/migrator/migrate_dbversion19.go b/api/bolt/migrator/migrate_dbversion19.go index 569bc43a9..f98d78ccc 100644 --- a/api/bolt/migrator/migrate_dbversion19.go +++ b/api/bolt/migrator/migrate_dbversion19.go @@ -15,3 +15,14 @@ func (m *Migrator) updateUsersToDBVersion20() error { authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters) return authorizationService.UpdateUsersAuthorizations() } + +func (m *Migrator) updateSettingsToDBVersion20() error { + legacySettings, err := m.settingsService.Settings() + if err != nil { + return err + } + + legacySettings.AllowVolumeBrowserForRegularUsers = false + + return m.settingsService.UpdateSettings(legacySettings) +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 409343cf1..52e4ee03f 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -271,6 +271,11 @@ func (m *Migrator) Migrate() error { if err != nil { return err } + + err = m.updateSettingsToDBVersion20() + if err != nil { + return err + } } return m.versionService.StoreDBVersion(portainer.DBVersion) diff --git a/api/bolt/role/role.go b/api/bolt/role/role.go index 8a4e3e975..36cd8e7d1 100644 --- a/api/bolt/role/role.go +++ b/api/bolt/role/role.go @@ -66,18 +66,24 @@ func (service *Service) Roles() ([]portainer.Role, error) { } // CreateRole creates a new Role. -func (service *Service) CreateRole(set *portainer.Role) error { +func (service *Service) CreateRole(role *portainer.Role) error { return service.db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(BucketName)) id, _ := bucket.NextSequence() - set.ID = portainer.RoleID(id) + role.ID = portainer.RoleID(id) - data, err := internal.MarshalObject(set) + data, err := internal.MarshalObject(role) if err != nil { return err } - return bucket.Put(internal.Itob(int(set.ID)), data) + return bucket.Put(internal.Itob(int(role.ID)), data) }) } + +// UpdateRole updates a role. +func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error { + identifier := internal.Itob(int(ID)) + return internal.UpdateObject(service.db, BucketName, identifier, role) +} diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 278ab0faa..2d83d10c1 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -259,18 +259,15 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL LogoURL: *flags.Logo, AuthenticationMethod: portainer.AuthenticationInternal, LDAPSettings: portainer.LDAPSettings{ - AutoCreateUsers: true, - TLSConfig: portainer.TLSConfiguration{}, - SearchSettings: []portainer.LDAPSearchSettings{ - portainer.LDAPSearchSettings{}, - }, - GroupSearchSettings: []portainer.LDAPGroupSearchSettings{ - portainer.LDAPGroupSearchSettings{}, - }, + AutoCreateUsers: true, + TLSConfig: portainer.TLSConfiguration{}, + SearchSettings: []portainer.LDAPSearchSettings{}, + GroupSearchSettings: []portainer.LDAPGroupSearchSettings{}, }, OAuthSettings: portainer.OAuthSettings{}, AllowBindMountsForRegularUsers: true, AllowPrivilegedModeForRegularUsers: true, + AllowVolumeBrowserForRegularUsers: false, EnableHostManagementFeatures: false, SnapshotInterval: *flags.SnapshotInterval, EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, diff --git a/api/http/handler/extensions/upgrade.go b/api/http/handler/extensions/upgrade.go index 2d314638a..b0f37f5d7 100644 --- a/api/http/handler/extensions/upgrade.go +++ b/api/http/handler/extensions/upgrade.go @@ -35,11 +35,6 @@ func (handler *Handler) upgradeRBACData() error { if err != nil { return err } - - //err = handler.AuthorizationService.UpdateUserAuthorizationsFromPolicies(&endpointGroup.UserAccessPolicies, &endpointGroup.TeamAccessPolicies) - //if err != nil { - // return err - //} } endpoints, err := handler.EndpointService.Endpoints() @@ -60,14 +55,7 @@ func (handler *Handler) upgradeRBACData() error { if err != nil { return err } - - //err = handler.AuthorizationService.UpdateUserAuthorizationsFromPolicies(&endpoint.UserAccessPolicies, &endpoint.TeamAccessPolicies) - //if err != nil { - // return err - //} } return handler.AuthorizationService.UpdateUsersAuthorizations() - - //return nil } diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index db22c92ab..1f688f343 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -17,11 +17,14 @@ func hideFields(settings *portainer.Settings) { // Handler is the HTTP handler used to handle settings operations. type Handler struct { *mux.Router - SettingsService portainer.SettingsService - LDAPService portainer.LDAPService - FileService portainer.FileService - JobScheduler portainer.JobScheduler - ScheduleService portainer.ScheduleService + SettingsService portainer.SettingsService + LDAPService portainer.LDAPService + FileService portainer.FileService + JobScheduler portainer.JobScheduler + ScheduleService portainer.ScheduleService + RoleService portainer.RoleService + ExtensionService portainer.ExtensionService + AuthorizationService *portainer.AuthorizationService } // NewHandler creates a handler to manage settings operations. diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 38eb8d4e3..afa8e85dd 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -14,6 +14,7 @@ type publicSettingsResponse struct { AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` ExternalTemplates bool `json:"ExternalTemplates"` OAuthLoginURI string `json:"OAuthLoginURI"` @@ -31,6 +32,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * AuthenticationMethod: settings.AuthenticationMethod, AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, + AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, EnableHostManagementFeatures: settings.EnableHostManagementFeatures, ExternalTemplates: false, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 6de707c38..cf0994648 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -19,6 +19,7 @@ type settingsUpdatePayload struct { OAuthSettings *portainer.OAuthSettings AllowBindMountsForRegularUsers *bool AllowPrivilegedModeForRegularUsers *bool + AllowVolumeBrowserForRegularUsers *bool EnableHostManagementFeatures *bool SnapshotInterval *string TemplatesURL *string @@ -93,6 +94,12 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers } + updateAuthorizations := false + if payload.AllowVolumeBrowserForRegularUsers != nil { + settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers + updateAuthorizations = true + } + if payload.EnableHostManagementFeatures != nil { settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures } @@ -118,9 +125,37 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err} } + if updateAuthorizations { + err := handler.updateVolumeBrowserSetting(settings) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update RBAC authorizations", err} + } + } + return response.JSON(w, settings) } +func (handler *Handler) updateVolumeBrowserSetting(settings *portainer.Settings) error { + err := handler.AuthorizationService.UpdateVolumeBrowsingAuthorizations(settings.AllowVolumeBrowserForRegularUsers) + if err != nil { + return err + } + + extension, err := handler.ExtensionService.Extension(portainer.RBACExtension) + if err != nil && err != portainer.ErrObjectNotFound { + return err + } + + if extension != nil { + err = handler.AuthorizationService.UpdateUsersAuthorizations() + if err != nil { + return err + } + } + + return nil +} + func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, snapshotInterval string) error { settings.SnapshotInterval = snapshotInterval diff --git a/api/http/proxy/docker_transport.go b/api/http/proxy/docker_transport.go index 6443a8268..8abb21ce3 100644 --- a/api/http/proxy/docker_transport.go +++ b/api/http/proxy/docker_transport.go @@ -26,6 +26,7 @@ type ( SettingsService portainer.SettingsService SignatureService portainer.DigitalSignatureService ReverseTunnelService portainer.ReverseTunnelService + ExtensionService portainer.ExtensionService endpointIdentifier portainer.EndpointID endpointType portainer.EndpointType } @@ -129,7 +130,8 @@ func (p *proxyTransport) proxyAgentRequest(r *http.Request) (*http.Response, err if !found || len(volumeIDParameter) < 1 { return p.administratorOperation(r) } - return p.restrictedOperation(r, volumeIDParameter[0]) + + return p.restrictedVolumeBrowserOperation(r, volumeIDParameter[0]) } return p.executeDockerRequest(r) @@ -386,6 +388,62 @@ func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID s return p.executeDockerRequest(request) } +// restrictedVolumeBrowserOperation is similar to restrictedOperation but adds an extra check on a specific setting +func (p *proxyTransport) restrictedVolumeBrowserOperation(request *http.Request, resourceID string) (*http.Response, error) { + var err error + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + if tokenData.Role != portainer.AdministratorRole { + settings, err := p.SettingsService.Settings() + if err != nil { + return nil, err + } + + _, err = p.ExtensionService.Extension(portainer.RBACExtension) + if err == portainer.ErrObjectNotFound && !settings.AllowVolumeBrowserForRegularUsers { + return writeAccessDeniedResponse() + } else if err != nil && err != portainer.ErrObjectNotFound { + return nil, err + } + + user, err := p.UserService.User(tokenData.ID) + if err != nil { + return nil, err + } + + endpointResourceAccess := false + _, ok := user.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess] + if ok { + endpointResourceAccess = true + } + + teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID) + if err != nil { + return nil, err + } + + userTeamIDs := make([]portainer.TeamID, 0) + for _, membership := range teamMemberships { + userTeamIDs = append(userTeamIDs, membership.TeamID) + } + + resourceControls, err := p.ResourceControlService.ResourceControls() + if err != nil { + return nil, err + } + + resourceControl := getResourceControlByResourceID(resourceID, resourceControls) + if !endpointResourceAccess && (resourceControl == nil || !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl)) { + return writeAccessDeniedResponse() + } + } + + return p.executeDockerRequest(request) +} + // rewriteOperationWithLabelFiltering will create a new operation context with data that will be used // to decorate the original request's response as well as retrieve all the black listed labels // to filter the resources. diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go index a1f4e1e32..b9416707d 100644 --- a/api/http/proxy/factory.go +++ b/api/http/proxy/factory.go @@ -23,6 +23,7 @@ type proxyFactory struct { DockerHubService portainer.DockerHubService SignatureService portainer.DigitalSignatureService ReverseTunnelService portainer.ReverseTunnelService + ExtensionService portainer.ExtensionService } func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler { @@ -77,6 +78,7 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, endpoint *port RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, ReverseTunnelService: factory.ReverseTunnelService, + ExtensionService: factory.ExtensionService, dockerTransport: &http.Transport{}, endpointIdentifier: endpoint.ID, endpointType: endpoint.Type, diff --git a/api/http/proxy/factory_local.go b/api/http/proxy/factory_local.go index 37b7f5401..c9ab44b81 100644 --- a/api/http/proxy/factory_local.go +++ b/api/http/proxy/factory_local.go @@ -18,6 +18,7 @@ func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endp SettingsService: factory.SettingsService, RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, + ExtensionService: factory.ExtensionService, dockerTransport: newSocketTransport(path), ReverseTunnelService: factory.ReverseTunnelService, endpointIdentifier: endpoint.ID, diff --git a/api/http/proxy/factory_local_windows.go b/api/http/proxy/factory_local_windows.go index 0c6726a8d..3f1d860d7 100644 --- a/api/http/proxy/factory_local_windows.go +++ b/api/http/proxy/factory_local_windows.go @@ -22,6 +22,7 @@ func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endp RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, ReverseTunnelService: factory.ReverseTunnelService, + ExtensionService: factory.ExtensionService, dockerTransport: newNamedPipeTransport(path), endpointIdentifier: endpoint.ID, endpointType: endpoint.Type, diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 5d87f2393..7a1f38580 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -38,6 +38,7 @@ type ( DockerHubService portainer.DockerHubService SignatureService portainer.DigitalSignatureService ReverseTunnelService portainer.ReverseTunnelService + ExtensionService portainer.ExtensionService } ) @@ -56,6 +57,7 @@ func NewManager(parameters *ManagerParams) *Manager { DockerHubService: parameters.DockerHubService, SignatureService: parameters.SignatureService, ReverseTunnelService: parameters.ReverseTunnelService, + ExtensionService: parameters.ExtensionService, }, reverseTunnelService: parameters.ReverseTunnelService, } diff --git a/api/http/server.go b/api/http/server.go index a2a48cfe8..664c1989f 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -91,6 +91,7 @@ func (server *Server) Start() error { DockerHubService: server.DockerHubService, SignatureService: server.SignatureService, ReverseTunnelService: server.ReverseTunnelService, + ExtensionService: server.ExtensionService, } proxyManager := proxy.NewManager(proxyManagerParameters) @@ -196,6 +197,9 @@ func (server *Server) Start() error { settingsHandler.FileService = server.FileService settingsHandler.JobScheduler = server.JobScheduler settingsHandler.ScheduleService = server.ScheduleService + settingsHandler.RoleService = server.RoleService + settingsHandler.ExtensionService = server.ExtensionService + settingsHandler.AuthorizationService = authorizationService var stackHandler = stacks.NewHandler(requestBouncer) stackHandler.FileService = server.FileService diff --git a/api/portainer.go b/api/portainer.go index d5d546bf5..be332f7e8 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -106,6 +106,7 @@ type ( 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"` @@ -637,7 +638,8 @@ type ( RoleService interface { Role(ID RoleID) (*Role, error) Roles() ([]Role, error) - CreateRole(set *Role) error + CreateRole(role *Role) error + UpdateRole(ID RoleID, role *Role) error } // TeamService represents a service for managing user data @@ -903,7 +905,7 @@ const ( // APIVersion is the version number of the Portainer API APIVersion = "1.22.0" // DBVersion is the version number of the Portainer database - DBVersion = 20 + DBVersion = 21 // AssetsServerURL represents the URL of the Portainer asset server AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html index ca115011d..1266ea5be 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html @@ -149,9 +149,11 @@ {{ item.Id | truncate:40 }} {{ item.Id | truncate:40 }} - - browse - + + + browse + + Unused {{ item.StackName ? item.StackName : '-' }} diff --git a/app/docker/views/volumes/volumes.html b/app/docker/views/volumes/volumes.html index c31b7ac3b..425d93a75 100644 --- a/app/docker/views/volumes/volumes.html +++ b/app/docker/views/volumes/volumes.html @@ -16,7 +16,7 @@ remove-action="removeAction" show-ownership-column="applicationState.application.authentication" show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" - show-browse-action="applicationState.endpoint.mode.agentProxy" + show-browse-action="showBrowseAction" offline-mode="offlineMode" refresh-callback="getVolumes" > diff --git a/app/docker/views/volumes/volumesController.js b/app/docker/views/volumes/volumesController.js index 0cad1b914..6cde69eb2 100644 --- a/app/docker/views/volumes/volumesController.js +++ b/app/docker/views/volumes/volumesController.js @@ -1,6 +1,6 @@ angular.module('portainer.docker') -.controller('VolumesController', ['$q', '$scope', '$state', 'VolumeService', 'ServiceService', 'VolumeHelper', 'Notifications', 'HttpRequestHelper', 'EndpointProvider', -function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider) { +.controller('VolumesController', ['$q', '$scope', '$state', 'VolumeService', 'ServiceService', 'VolumeHelper', 'Notifications', 'HttpRequestHelper', 'EndpointProvider', 'Authentication', 'ExtensionService', +function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ExtensionService) { $scope.removeAction = function (selectedItems) { var actionCount = selectedItems.length; @@ -56,6 +56,18 @@ function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notif function initView() { getVolumes(); + + $scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy; + + ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC) + .then(function success(extensionEnabled) { + if (!extensionEnabled) { + var isAdmin = Authentication.isAdmin(); + if (!$scope.applicationState.application.enableVolumeBrowserForNonAdminUsers && !isAdmin) { + $scope.showBrowseAction = false; + } + } + }); } initView(); diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 4ac71e8a3..aa58dfd8e 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -6,6 +6,7 @@ export function SettingsViewModel(data) { this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings); this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers; this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers; + this.AllowVolumeBrowserForRegularUsers = data.AllowVolumeBrowserForRegularUsers; this.SnapshotInterval = data.SnapshotInterval; this.TemplatesURL = data.TemplatesURL; this.ExternalTemplates = data.ExternalTemplates; @@ -16,6 +17,7 @@ export function SettingsViewModel(data) { export function PublicSettingsViewModel(settings) { this.AllowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers; this.AllowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers; + this.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers; this.AuthenticationMethod = settings.AuthenticationMethod; this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures; this.ExternalTemplates = settings.ExternalTemplates; diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 070d32e72..65ee29476 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -58,6 +58,11 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin LocalStorage.storeApplicationState(state.application); }; + manager.updateEnableVolumeBrowserForNonAdminUsers = function(enableVolumeBrowserForNonAdminUsers) { + state.application.enableVolumeBrowserForNonAdminUsers = enableVolumeBrowserForNonAdminUsers; + LocalStorage.storeApplicationState(state.application); + }; + function assignStateFromStatusAndSettings(status, settings) { state.application.authentication = status.Authentication; state.application.analytics = status.Analytics; @@ -67,6 +72,7 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin state.application.logo = settings.LogoURL; state.application.snapshotInterval = settings.SnapshotInterval; state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures; + state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers; state.application.validity = moment().unix(); } diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 6dc4a7750..c80f69aab 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -101,6 +101,17 @@ +
+
+ + +
+