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