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 <william.conquest@portainer.io>
pull/3240/head
Anthony Lapenna 2019-10-08 13:17:58 +13:00 committed by GitHub
parent ef4c138e03
commit 9aa52a6975
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 239 additions and 48 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -149,9 +149,11 @@
</span>
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.volumes.volume({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a>
<span ng-if="$ctrl.offlineMode">{{ item.Id | truncate:40 }}</span>
<a authorization="DockerAgentBrowseList" ui-sref="docker.volumes.volume.browse({ id: item.Id, nodeName: item.NodeName })" class="btn btn-xs btn-primary space-left" ng-if="$ctrl.showBrowseAction && !$ctrl.offlineMode">
<i class="fa fa-search"></i> browse</a>
</a>
<btn authorization="DockerAgentBrowseList" ng-if="$ctrl.showBrowseAction && !$ctrl.offlineMode">
<a ui-sref="docker.volumes.volume.browse({ id: item.Id, nodeName: item.NodeName })" class="btn btn-xs btn-primary space-left">
<i class="fa fa-search"></i> browse
</a>
</btn>
<span style="margin-left: 10px;" class="label label-warning image-tag space-left" ng-if="item.dangling">Unused</span>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>

View File

@ -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"
></volumes-datatable>

View File

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

View File

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

View File

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

View File

@ -101,6 +101,17 @@
</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, non-admin users & users with helpdesk, standard and read-only roles from the RBAC extension 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>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_enableHostManagementFeatures" class="control-label text-left">

View File

@ -27,7 +27,8 @@ function ($scope, $state, Notifications, SettingsService, StateManager) {
restrictPrivilegedMode: false,
labelName: '',
labelValue: '',
enableHostManagementFeatures: false
enableHostManagementFeatures: false,
enableVolumeBrowser: false,
};
$scope.removeFilteredContainerLabel = function(index) {
@ -61,6 +62,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager) {
settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts;
settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode;
settings.AllowVolumeBrowserForRegularUsers = $scope.formValues.enableVolumeBrowser;
settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures;
$scope.state.actionInProgress = true;
@ -74,6 +76,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager) {
StateManager.updateLogo(settings.LogoURL);
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
StateManager.updateEnableHostManagementFeatures(settings.EnableHostManagementFeatures);
StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers);
$state.reload();
})
.catch(function error(err) {
@ -97,6 +100,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager) {
}
$scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers;
$scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers;
$scope.formValues.enableVolumeBrowser = settings.AllowVolumeBrowserForRegularUsers;
$scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
})
.catch(function error(err) {